/* * Autopsy Forensic Browser * * Copyright 2011-2016 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.detailview; import com.google.common.collect.ImmutableList; import java.util.List; import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.collections.ObservableList; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.chart.Axis; import javafx.scene.control.Alert; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.MenuButton; import javafx.scene.control.RadioButton; import javafx.scene.control.Slider; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.HBox; import javafx.stage.Modality; import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.action.Action; import org.joda.time.DateTime; import org.joda.time.Interval; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.ViewMode; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.ui.AbstractTimelineChart; import org.sleuthkit.autopsy.timeline.utils.MappedList; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; /** * Controller class for a DetailsChart based implementation of a timeline view. * * This class listens to changes in the assigned FilteredEventsModel and updates * the internal DetailsChart to reflect the currently requested view settings. * * Conceptually this view visualizes trees of events grouped by type and * description as a set of nested rectangles with their positions along the * x-axis tied to their times, and their vertical positions arbitrary but * constrained by the heirarchical relationships of the tree.The root of the * trees are EventStripes, which contain EventCluster, which contain more finely * grouped EventStripes, etc, etc. The leaves of the trees are EventClusters or * SingleEvents. */ public class DetailViewPane extends AbstractTimelineChart<DateTime, EventStripe, EventNodeBase<?>, DetailsChart> { private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName()); private final DateAxis detailsChartDateAxis = new DateAxis(); private final DateAxis pinnedDateAxis = new DateAxis(); @NbBundle.Messages("DetailViewPane.primaryLaneLabel.text=All Events (Filtered)") private final Axis<EventStripe> verticalAxis = new EventAxis<>(Bundle.DetailViewPane_primaryLaneLabel_text()); /** * ObservableList of events selected in this detail view. It is * automatically mapped from the list of nodes selected in this view. */ private final MappedList<TimeLineEvent, EventNodeBase<?>> selectedEvents; /** * Local copy of the zoomParams. Used to backout of a zoomParam change * without needing to requery/redraw the view. */ private ZoomParams currentZoomParams; /** * Constructor for a DetailViewPane * * @param controller the Controller to use */ public DetailViewPane(TimeLineController controller) { super(controller); this.selectedEvents = new MappedList<>(getSelectedNodes(), EventNodeBase<?>::getEvent); //initialize chart; setChart(new DetailsChart(controller, detailsChartDateAxis, pinnedDateAxis, verticalAxis, getSelectedNodes())); //bind layout fo axes and spacers detailsChartDateAxis.getTickMarks().addListener((Observable observable) -> layoutDateLabels()); detailsChartDateAxis.getTickSpacing().addListener(observable -> layoutDateLabels()); verticalAxis.setAutoRanging(false); //prevent XYChart.updateAxisRange() from accessing dataSeries on JFX thread causing ConcurrentModificationException getSelectedNodes().addListener((Observable observable) -> { //update selected nodes highlight getChart().setHighlightPredicate(getSelectedNodes()::contains); //update controllers list of selected event ids when view's selection changes. getController().selectEventIDs(getSelectedNodes().stream() .flatMap(detailNode -> detailNode.getEventIDs().stream()) .collect(Collectors.toList())); }); } /* * Get all the trees of events flattened into a single list, but only * including EventStripes and any leaf SingleEvents, since, EventClusters * contain no interesting non-time related information. */ public ObservableList<TimeLineEvent> getAllNestedEvents() { return getChart().getAllNestedEvents(); } /* * Get a list of the events that are selected in thes view. */ public ObservableList<TimeLineEvent> getSelectedEvents() { return selectedEvents; } /** * Observe the list of events that should be highlighted in this view. * * * @param highlightedEvents the ObservableList of events that should be * highlighted in this view. */ public void setHighLightedEvents(ObservableList<TimeLineEvent> highlightedEvents) { highlightedEvents.addListener((Observable observable) -> { /* * build a predicate that matches events with the same description * as any of the events in highlightedEvents or which are selected */ Predicate<EventNodeBase<?>> highlightPredicate = highlightedEvents.stream() // => events .map(TimeLineEvent::getDescription)// => event descriptions .map(new Function<String, Predicate<EventNodeBase<?>>>() { @Override public Predicate<EventNodeBase<?>> apply(String description) { return eventNode -> StringUtils.equalsIgnoreCase(eventNode.getDescription(), description); } })// => predicates that match strings agains the descriptions of the events in highlightedEvents .reduce(getSelectedNodes()::contains, Predicate::or); // => predicate that matches an of the descriptions or selected nodes getChart().setHighlightPredicate(highlightPredicate); //use this predicate to highlight nodes }); } @Override final protected DateAxis getXAxis() { return detailsChartDateAxis; } /** * Get a new Action that will unhide events with the given description. * * @param description the description to unhide * @param descriptionLoD the description level of detail to match * * @return a new Action that will unhide events with the given description. */ public Action newUnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) { return new UnhideDescriptionAction(description, descriptionLoD, getChart()); } /** * Get a new Action that will hide events with the given description. * * @param description the description to hide * @param descriptionLoD the description level of detail to match * * @return a new Action that will hide events with the given description. */ public Action newHideDescriptionAction(String description, DescriptionLoD descriptionLoD) { return new HideDescriptionAction(description, descriptionLoD, getChart()); } @ThreadConfined(type = ThreadConfined.ThreadType.JFX) @Override protected void clearData() { getChart().reset(); } @Override protected Boolean isTickBold(DateTime value) { return false; } @Override final protected Axis<EventStripe> getYAxis() { return verticalAxis; } @Override protected double getTickSpacing() { return detailsChartDateAxis.getTickSpacing().get(); } @Override protected String getTickMarkLabel(DateTime value) { return detailsChartDateAxis.getTickMarkLabel(value); } @Override protected Task<Boolean> getNewUpdateTask() { return new DetailsUpdateTask(); } @Override protected void applySelectionEffect(EventNodeBase<?> c1, Boolean selected) { c1.applySelectionEffect(selected); } @Override protected double getAxisMargin() { return 0; } @Override final protected ViewMode getViewMode() { return ViewMode.DETAIL; } @Override protected ImmutableList<Node> getSettingsControls() { return ImmutableList.copyOf(new DetailViewSettingsPane(getChart().getLayoutSettings()).getChildrenUnmodifiable()); } @Override protected boolean hasCustomTimeNavigationControls() { return false; } @Override protected ImmutableList<Node> getTimeNavigationControls() { return ImmutableList.of(); } /** * A Pane that contains widgets to adjust settings specific to a * DetailViewPane */ static private class DetailViewSettingsPane extends HBox { @FXML private RadioButton hiddenRadio; @FXML private RadioButton showRadio; @FXML private ToggleGroup descrVisibility; @FXML private RadioButton countsRadio; @FXML private CheckBox bandByTypeBox; @FXML private CheckBox oneEventPerRowBox; @FXML private CheckBox truncateAllBox; @FXML private Slider truncateWidthSlider; @FXML private Label truncateSliderLabel; @FXML private MenuButton advancedLayoutOptionsButtonLabel; @FXML private ToggleButton pinnedEventsToggle; private final DetailsChartLayoutSettings layoutSettings; DetailViewSettingsPane(DetailsChartLayoutSettings layoutSettings) { this.layoutSettings = layoutSettings; FXMLConstructor.construct(DetailViewSettingsPane.this, "DetailViewSettingsPane.fxml"); //NON-NLS } @NbBundle.Messages({ "DetailViewPane.truncateSliderLabel.text=max description width (px):", "DetailViewPane.advancedLayoutOptionsButtonLabel.text=Advanced Layout Options", "DetailViewPane.bandByTypeBox.text=Band by Type", "DetailViewPane.oneEventPerRowBox.text=One Per Row", "DetailViewPane.truncateAllBox.text=Truncate Descriptions", "DetailViewPane.showRadio.text=Show Full Description", "DetailViewPane.countsRadio.text=Show Counts Only", "DetailViewPane.hiddenRadio.text=Hide Description"}) @FXML void initialize() { assert bandByTypeBox != null : "fx:id=\"bandByTypeBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; //NON-NLS assert oneEventPerRowBox != null : "fx:id=\"oneEventPerRowBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; //NON-NLS assert truncateAllBox != null : "fx:id=\"truncateAllBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; //NON-NLS assert truncateWidthSlider != null : "fx:id=\"truncateAllSlider\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; //NON-NLS assert pinnedEventsToggle != null : "fx:id=\"pinnedEventsToggle\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; //NON-NLS //bind widgets to settings object properties bandByTypeBox.selectedProperty().bindBidirectional(layoutSettings.bandByTypeProperty()); oneEventPerRowBox.selectedProperty().bindBidirectional(layoutSettings.oneEventPerRowProperty()); truncateAllBox.selectedProperty().bindBidirectional(layoutSettings.truncateAllProperty()); truncateSliderLabel.disableProperty().bind(truncateAllBox.selectedProperty().not()); pinnedEventsToggle.selectedProperty().bindBidirectional(layoutSettings.pinnedLaneShowing()); final InvalidationListener sliderListener = observable -> { if (truncateWidthSlider.isValueChanging() == false) { layoutSettings.truncateWidthProperty().set(truncateWidthSlider.getValue()); } }; truncateWidthSlider.valueProperty().addListener(sliderListener); truncateWidthSlider.valueChangingProperty().addListener(sliderListener); descrVisibility.selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> { if (newToggle == countsRadio) { layoutSettings.descrVisibilityProperty().set(DescriptionVisibility.COUNT_ONLY); } else if (newToggle == showRadio) { layoutSettings.descrVisibilityProperty().set(DescriptionVisibility.SHOWN); } else if (newToggle == hiddenRadio) { layoutSettings.descrVisibilityProperty().set(DescriptionVisibility.HIDDEN); } }); //Assign localized labels truncateSliderLabel.setText(Bundle.DetailViewPane_truncateSliderLabel_text()); advancedLayoutOptionsButtonLabel.setText(Bundle.DetailViewPane_advancedLayoutOptionsButtonLabel_text()); bandByTypeBox.setText(Bundle.DetailViewPane_bandByTypeBox_text()); oneEventPerRowBox.setText(Bundle.DetailViewPane_oneEventPerRowBox_text()); truncateAllBox.setText(Bundle.DetailViewPane_truncateAllBox_text()); showRadio.setText(Bundle.DetailViewPane_showRadio_text()); countsRadio.setText(Bundle.DetailViewPane_countsRadio_text()); hiddenRadio.setText(Bundle.DetailViewPane_hiddenRadio_text()); } } @NbBundle.Messages({ "DetailViewPane.loggedTask.queryDb=Retrieving event data", "DetailViewPane.loggedTask.name=Updating Details View", "DetailViewPane.loggedTask.updateUI=Populating view", "DetailViewPane.loggedTask.continueButton=Continue", "DetailViewPane.loggedTask.backButton=Back (Cancel)", "# {0} - number of events", "DetailViewPane.loggedTask.prompt=You are about to show details for {0} events. This might be very slow and could exhaust available memory.\n\nDo you want to continue?"}) private class DetailsUpdateTask extends ViewRefreshTask<Interval> { DetailsUpdateTask() { super(Bundle.DetailViewPane_loggedTask_name(), true); } @Override protected Boolean call() throws Exception { super.call(); if (isCancelled()) { return null; } FilteredEventsModel eventsModel = getEventsModel(); ZoomParams newZoomParams = eventsModel.getZoomParamaters(); //if the zoomParams haven't actually changed, just bail if (Objects.equals(currentZoomParams, newZoomParams)) { return true; } updateMessage(Bundle.DetailViewPane_loggedTask_queryDb()); //get the event stripes to be displayed List<EventStripe> eventStripes = eventsModel.getEventStripes(); final int size = eventStripes.size(); //if there are too many stipes show a confirmation dialog if (size > 2000) { Task<ButtonType> task = new Task<ButtonType>() { @Override protected ButtonType call() throws Exception { ButtonType ContinueButtonType = new ButtonType(Bundle.DetailViewPane_loggedTask_continueButton(), ButtonBar.ButtonData.OK_DONE); ButtonType back = new ButtonType(Bundle.DetailViewPane_loggedTask_backButton(), ButtonBar.ButtonData.CANCEL_CLOSE); Alert alert = new Alert(Alert.AlertType.WARNING, Bundle.DetailViewPane_loggedTask_prompt(size), ContinueButtonType, back); alert.setHeaderText(""); alert.initModality(Modality.APPLICATION_MODAL); alert.initOwner(getScene().getWindow()); ButtonType userResponse = alert.showAndWait().orElse(back); if (userResponse == back) { DetailsUpdateTask.this.cancel(); } return userResponse; } }; //show dialog on JFX thread and block this thread until the dialog is dismissed. Platform.runLater(task); task.get(); } if (isCancelled()) { return null; } //we are going to accept the new zoomParams currentZoomParams = newZoomParams; //clear the chart and set the horixontal axis resetView(eventsModel.getTimeRange()); updateMessage(Bundle.DetailViewPane_loggedTask_updateUI()); //add all the stripes for (int i = 0; i < size; i++) { if (isCancelled()) { return null; } updateProgress(i, size); final EventStripe stripe = eventStripes.get(i); Platform.runLater(() -> getChart().addStripe(stripe)); } return eventStripes.isEmpty() == false; } @Override protected void cancelled() { super.cancelled(); getController().retreat(); } @Override protected void setDateValues(Interval timeRange) { detailsChartDateAxis.setRange(timeRange, true); pinnedDateAxis.setRange(timeRange, true); } @Override protected void succeeded() { super.succeeded(); layoutDateLabels(); } } }