Java tutorial
/* * Autopsy Forensic Browser * * Copyright 2014 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sleuthkit.autopsy.timeline.ui; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.property.ReadOnlyListProperty; import javafx.beans.property.ReadOnlyListWrapper; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.concurrent.Task; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.chart.Axis; import javafx.scene.chart.BarChart; import javafx.scene.chart.Chart; import javafx.scene.chart.XYChart; import javafx.scene.control.Label; import javafx.scene.control.OverrunStyle; import javafx.scene.effect.Effect; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; import javax.annotation.concurrent.Immutable; import org.apache.commons.lang3.StringUtils; import org.openide.util.Exceptions; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineView; import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; /** Abstract base class for {@link Chart} based {@link TimeLineView}s used in * the main visualization area. * * @param <X> the type of data plotted along the x axis * @param <Y> the type of data plotted along the y axis * @param <N> the type of nodes used to represent data items * @param <C> the type of the {@link XYChart<X,Y>} this class uses to plot the * data. * * TODO: this is becoming (too?) closely tied to the notion that their is a * {@link XYChart} doing the rendering. Is this a good idea? -jm * TODO: pull up common history context menu items out of derived classes? -jm */ public abstract class AbstractVisualization<X, Y, N extends Node, C extends XYChart<X, Y> & TimeLineChart<X>> extends BorderPane implements TimeLineView { protected final SimpleBooleanProperty hasEvents = new SimpleBooleanProperty(true); protected final ObservableList<BarChart.Series<X, Y>> dataSets = FXCollections.<BarChart.Series<X, Y>>observableArrayList(); protected C chart; //// replacement axis label componenets private final Pane leafPane; // container for the leaf lables in the declutterd axis private final Pane branchPane;// container for the branch lables in the declutterd axis protected final Region spacer; /** task used to reload the content of this visualization */ private Task<Boolean> updateTask; protected TimeLineController controller; protected FilteredEventsModel filteredEvents; protected ReadOnlyListWrapper<N> selectedNodes = new ReadOnlyListWrapper<>(FXCollections.observableArrayList()); public ReadOnlyListProperty<N> getSelectedNodes() { return selectedNodes.getReadOnlyProperty(); } /** list of {@link Node}s to insert into the toolbar. This should be * set in an implementations constructor. */ protected List<Node> settingsNodes; /** @return the list of nodes containing settings widgets to insert into * this visualization's header */ protected List<Node> getSettingsNodes() { return Collections.unmodifiableList(settingsNodes); } /** @param value a value along this visualization's x axis * * @return true if the tick label for the given value should be bold ( has * relevant data), * false* otherwise */ protected abstract Boolean isTickBold(X value); /** apply this visualization's 'selection effect' to the given node * * @param node the node to apply the 'effect' to * @param applied true if the effect should be applied, false if the effect * should */ protected abstract void applySelectionEffect(N node, Boolean applied); /** @return a task to execute on a background thread to reload this * visualization with different data. */ protected abstract Task<Boolean> getUpdateTask(); /** @return return the {@link Effect} applied to 'selected nodes' in this * visualization, or null if selection is visualized via another * mechanism */ protected abstract Effect getSelectionEffect(); /** @param tickValue * * @return a String to use for a tick mark label given a tick value */ protected abstract String getTickMarkLabel(X tickValue); /** the spacing (in pixels) between tick marks of the horizontal * axis. This will be used to layout the decluttered replacement labels. * * @return the spacing in pixels between tick marks of the horizontal * axis */ protected abstract double getTickSpacing(); /** @return the horizontal axis used by this Visualization's chart */ protected abstract Axis<X> getXAxis(); /** @return the vertical axis used by this Visualization's chart */ protected abstract Axis<Y> getYAxis(); /** * update this visualization based on current state of zoom / * filters.Primarily this invokes the background {@link Task} returned * by {@link #getUpdateTask()} which derived classes must implement. */ synchronized public void update() { if (updateTask != null) { updateTask.cancel(true); updateTask = null; } updateTask = getUpdateTask(); updateTask.stateProperty().addListener((Observable observable) -> { switch (updateTask.getState()) { case CANCELLED: case FAILED: case READY: case RUNNING: case SCHEDULED: break; case SUCCEEDED: try { this.hasEvents.set(updateTask.get()); } catch (InterruptedException | ExecutionException ex) { Exceptions.printStackTrace(ex); } break; } }); controller.monitorTask(updateTask); } synchronized public void dispose() { if (updateTask != null) { updateTask.cancel(true); } this.filteredEvents.getRequestedZoomParamters().removeListener(invalidationListener); invalidationListener = null; } protected AbstractVisualization(Pane partPane, Pane contextPane, Region spacer) { this.leafPane = partPane; this.branchPane = contextPane; this.spacer = spacer; selectedNodes.addListener((ListChangeListener.Change<? extends N> c) -> { while (c.next()) { c.getRemoved().forEach((N n) -> { applySelectionEffect(n, false); }); c.getAddedSubList().forEach((N c1) -> { applySelectionEffect(c1, true); }); } }); } @Override synchronized public void setController(TimeLineController controller) { this.controller = controller; chart.setController(controller); setModel(controller.getEventsModel()); TimeLineController.getTimeZone().addListener((Observable observable) -> { update(); }); } @Override synchronized public void setModel(FilteredEventsModel filteredEvents) { this.filteredEvents = filteredEvents; this.filteredEvents.getRequestedZoomParamters().addListener(invalidationListener); update(); } protected InvalidationListener invalidationListener = (Observable observable) -> { update(); }; /** iterate through the list of tick-marks building a two level structure of * replacement tick marl labels. (Visually) upper level has most * detailed/highest frequency part of date/time. Second level has rest of * date/time grouped by unchanging part. * eg: * * * october-30_october-31_september-01_september-02_september-03 * * becomes * * _________30_________31___________01___________02___________03 * * _________october___________|_____________september___________ * * * NOTE: This method should only be invoked on the JFX thread */ public synchronized void layoutDateLabels() { //clear old labels branchPane.getChildren().clear(); leafPane.getChildren().clear(); //since the tickmarks aren't necessarily in value/position order, //make a clone of the list sorted by position along axis ObservableList<Axis.TickMark<X>> tickMarks = FXCollections.observableArrayList(getXAxis().getTickMarks()); tickMarks.sort( (Axis.TickMark<X> t, Axis.TickMark<X> t1) -> Double.compare(t.getPosition(), t1.getPosition())); if (tickMarks.isEmpty() == false) { //get the spacing between ticks in the underlying axis double spacing = getTickSpacing(); //initialize values from first tick TwoPartDateTime dateTime = new TwoPartDateTime(getTickMarkLabel(tickMarks.get(0).getValue())); String lastSeenBranchLabel = dateTime.branch; //cumulative width of the current branch label //x-positions (pixels) of the current branch and leaf labels double leafLabelX = 0; if (dateTime.branch.equals("")) { //if there is only one part to the date (ie only year), just add a label for each tick for (Axis.TickMark<X> t : tickMarks) { assignLeafLabel(new TwoPartDateTime(getTickMarkLabel(t.getValue())).leaf, spacing, leafLabelX, isTickBold(t.getValue())); leafLabelX += spacing; //increment x } } else { //there are two parts so ... //initialize additional state double branchLabelX = 0; double branchLabelWidth = 0; for (Axis.TickMark<X> t : tickMarks) { //for each tick //split the label into a TwoPartDateTime dateTime = new TwoPartDateTime(getTickMarkLabel(t.getValue())); //if we are still on the same branch if (lastSeenBranchLabel.equals(dateTime.branch)) { //increment branch width branchLabelWidth += spacing; } else {// we are on to a new branch, so ... assignBranchLabel(lastSeenBranchLabel, branchLabelWidth, branchLabelX); //and then update label, x-pos, and width lastSeenBranchLabel = dateTime.branch; branchLabelX += branchLabelWidth; branchLabelWidth = spacing; } //add the label for the leaf (highest frequency part) assignLeafLabel(dateTime.leaf, spacing, leafLabelX, isTickBold(t.getValue())); //increment leaf position leafLabelX += spacing; } //we have reached end so add branch label for current branch assignBranchLabel(lastSeenBranchLabel, branchLabelWidth, branchLabelX); } } //request layout since we have modified scene graph structure requestParentLayout(); } protected void setChartClickHandler() { chart.addEventHandler(MouseEvent.MOUSE_CLICKED, (MouseEvent event) -> { if (event.getButton() == MouseButton.PRIMARY && event.isStillSincePress()) { selectedNodes.clear(); } }); } /** add a {@link Text} node to the leaf container for the decluttered axis * labels * * @param labelText the string to add * @param labelWidth the width of the space available for the text * @param labelX the horizontal position in the partPane of the text * @param bold true if the text should be bold, false otherwise */ private synchronized void assignLeafLabel(String labelText, double labelWidth, double labelX, boolean bold) { Text label = new Text(" " + labelText + " "); label.setTextAlignment(TextAlignment.CENTER); label.setFont(Font.font(null, bold ? FontWeight.BOLD : FontWeight.NORMAL, 10)); //position label accounting for width label.relocate(labelX + labelWidth / 2 - label.getBoundsInLocal().getWidth() / 2, 0); label.autosize(); if (leafPane.getChildren().isEmpty()) { //just add first label leafPane.getChildren().add(label); } else { //otherwise don't actually add the label if it would intersect with previous label final Text lastLabel = (Text) leafPane.getChildren().get(leafPane.getChildren().size() - 1); if (!lastLabel.getBoundsInParent().intersects(label.getBoundsInParent())) { leafPane.getChildren().add(label); } } } /** add a {@link Label} node to the branch container for the decluttered * axis labels * * @param labelText the string to add * @param labelWidth the width of the space to use for the label * @param labelX the horizontal position in the partPane of the text */ private synchronized void assignBranchLabel(String labelText, double labelWidth, double labelX) { Label label = new Label(labelText); label.setAlignment(Pos.CENTER); label.setTextAlignment(TextAlignment.CENTER); label.setFont(Font.font(10)); //use a leading ellipse since that is the lowest frequency part, //and can be infered more easily from other surrounding labels label.setTextOverrun(OverrunStyle.LEADING_ELLIPSIS); //force size label.setMinWidth(labelWidth); label.setPrefWidth(labelWidth); label.setMaxWidth(labelWidth); label.relocate(labelX, 0); if (labelX == 0) { // first label has no border label.setStyle("-fx-border-width: 0 0 0 0 ; -fx-border-color:black;"); // NON-NLS } else { // subsequent labels have border on left to create dividers label.setStyle("-fx-border-width: 0 0 0 1; -fx-border-color:black;"); // NON-NLS } branchPane.getChildren().add(label); } /** A simple data object used to represent a partial date as up to two * parts. A low frequency part (branch) containing all but the most * specific element, and a highest frequency part (leaf) containing the most * specific element. The branch and leaf names come from thinking of the * space of all date times as a tree with higher frequency information * further from the root. If there is only one part, it will be in the * branch and the leaf will equal an empty string */ @Immutable private static final class TwoPartDateTime { /** the low frequency part of a date/time eg 2001-May-4 */ private final String branch; /** the highest frequency part of a date/time eg 14 (2pm) */ private final String leaf; TwoPartDateTime(String dateString) { //find index of separator to spit on int splitIndex = StringUtils.lastIndexOfAny(dateString, " ", "-", ":"); if (splitIndex < 0) { // there is only one part leaf = dateString; branch = ""; } else { //split at index leaf = StringUtils.substring(dateString, splitIndex + 1); branch = StringUtils.substring(dateString, 0, splitIndex); } } } }