/* * 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; import java.io.IOException; import java.net.URL; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.stream.Collectors; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleObjectProperty; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; import javafx.scene.control.ComboBox; import javafx.scene.control.Dialog; import javafx.scene.control.DialogPane; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.Spinner; import javafx.scene.control.SpinnerValueFactory; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; import javafx.stage.Modality; import javafx.util.converter.IntegerStringConverter; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.text.WordUtils; import org.controlsfx.validation.ValidationMessage; import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.Validator; import org.joda.time.Interval; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.events.ViewInTimelineRequestedEvent; import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; /** * A Dialog that, given an AbstractFile or BlackBoardArtifact, allows the user * to choose a specific event and a time range around it to show in the Timeline * List View. */ final class ShowInTimelineDialog extends Dialog<ViewInTimelineRequestedEvent> { private static final Logger LOGGER = Logger.getLogger(ShowInTimelineDialog.class.getName()); @NbBundle.Messages({"ShowInTimelineDialog.showTimelineButtonType.text=Show Timeline"}) private static final ButtonType SHOW = new ButtonType(Bundle.ShowInTimelineDialog_showTimelineButtonType_text(), ButtonBar.ButtonData.OK_DONE); /** * List of ChronoUnits the user can select from when choosing a time range * to show. */ private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList( ChronoField.YEAR, ChronoField.MONTH_OF_YEAR, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE); @FXML private TableView<SingleEvent> eventTable; @FXML private TableColumn<SingleEvent, EventType> typeColumn; @FXML private TableColumn<SingleEvent, Long> dateTimeColumn; @FXML private Spinner<Integer> amountSpinner; @FXML private ComboBox<ChronoField> unitComboBox; @FXML private Label chooseEventLabel; private final VBox contentRoot = new VBox(); private final TimeLineController controller; private final ValidationSupport validationSupport = new ValidationSupport(); /** * Common Private Constructor * * @param controller The controller for this Dialog. * @param eventIDS A List of eventIDs to present to the user to choose * from. */ @NbBundle.Messages({ "ShowInTimelineDialog.amountValidator.message=The entered amount must only contain digits." }) private ShowInTimelineDialog(TimeLineController controller, List<Long> eventIDS) { this.controller = controller; //load dialog content fxml final String name = "nbres:/" + StringUtils.replace(ShowInTimelineDialog.class.getPackage().getName(), ".", "/") + "/ShowInTimelineDialog.fxml"; // NON-NLS try { FXMLLoader fxmlLoader = new FXMLLoader(new URL(name)); fxmlLoader.setRoot(contentRoot); fxmlLoader.setController(this); fxmlLoader.load(); } catch (IOException ex) { LOGGER.log(Level.SEVERE, "Unable to load FXML, node initialization may not be complete.", ex); //NON-NLS } //assert that fxml loading happened correctly assert eventTable != null : "fx:id=\"eventTable\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'."; assert typeColumn != null : "fx:id=\"typeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'."; assert dateTimeColumn != null : "fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'."; assert amountSpinner != null : "fx:id=\"amountsSpinner\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'."; assert unitComboBox != null : "fx:id=\"unitChoiceBox\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'."; //validat that spinner has a integer in the text field. validationSupport.registerValidator(amountSpinner.getEditor(), false, Validator.createPredicateValidator(NumberUtils::isDigits, Bundle.ShowInTimelineDialog_amountValidator_message())); //configure dialog properties PromptDialogManager.setDialogIcons(this); initModality(Modality.APPLICATION_MODAL); //add scenegraph loaded from fxml to this dialog. DialogPane dialogPane = getDialogPane(); dialogPane.setContent(contentRoot); //add buttons to dialog dialogPane.getButtonTypes().setAll(SHOW, ButtonType.CANCEL); ///configure dialog controls amountSpinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 1000)); amountSpinner.getValueFactory().setConverter(new IntegerStringConverter() { /** * Convert the String to an Integer using Integer.valueOf, but if * that throws a NumberFormatException, reset the spinner to the * last valid value. * * @param string The String to convert * * @return The Integer value of string. */ @Override public Integer fromString(String string) { try { return super.fromString(string); } catch (NumberFormatException ex) { return amountSpinner.getValue(); } } }); unitComboBox.setButtonCell(new ChronoFieldListCell()); unitComboBox.setCellFactory(comboBox -> new ChronoFieldListCell()); unitComboBox.getItems().setAll(SCROLL_BY_UNITS); unitComboBox.getSelectionModel().select(ChronoField.MINUTE_OF_HOUR); typeColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().getEventType())); typeColumn.setCellFactory(param -> new TypeTableCell<>()); dateTimeColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().getStartMillis())); dateTimeColumn.setCellFactory(param -> new DateTimeTableCell<>()); //add events to table eventTable.getItems().setAll(eventIDS.stream().map(controller.getEventsModel()::getEventById).collect(Collectors.toSet())); eventTable.setPrefHeight(Math.min(200, 24 * eventTable.getItems().size() + 28)); } /** * Constructor for artifact based dialog. suppressed the choosing event * aspect as each artifact is assumed to have only one associated event. * * @param controller The controller for this Dialog * @param artifact The BlackboardArtifact to configure this dialog for. */ @NbBundle.Messages({"ShowInTimelineDialog.artifactTitle=View Result in Timeline."}) ShowInTimelineDialog(TimeLineController controller, BlackboardArtifact artifact) { //get events IDs from artifact this(controller, controller.getEventsModel().getEventIDsForArtifact(artifact)); //hide instructional label and autoselect first(and only) event. chooseEventLabel.setVisible(false); chooseEventLabel.setManaged(false); eventTable.getSelectionModel().select(0); //require validation of ammount spinner to enable show button getDialogPane().lookupButton(SHOW).disableProperty().bind(validationSupport.invalidProperty()); //set result converter that does not require selection. setResultConverter(buttonType -> (buttonType == SHOW) ? makeEventInTimeRange(eventTable.getItems().get(0)) : null ); setTitle(Bundle.ShowInTimelineDialog_artifactTitle()); } /** * Constructor for file based dialog. Allows the user to choose an event * (MAC time) derived from the given file * * @param controller The controller for this Dialog. * @param file The AbstractFile to configure this dialog for. */ @NbBundle.Messages({"# {0} - file path", "ShowInTimelineDialog.fileTitle=View {0} in timeline.", "ShowInTimelineDialog.eventSelectionValidator.message=You must select an event."}) ShowInTimelineDialog(TimeLineController controller, AbstractFile file) { this(controller, controller.getEventsModel().getEventIDsForFile(file, false)); /* * since ValidationSupport does not support list selection, we will * manually apply and remove decoration in response to selection * property changes. */ eventTable.getSelectionModel().selectedItemProperty().isNull().addListener((selectedItemNullProperty, wasNull, isNull) -> { if (isNull) { validationSupport.getValidationDecorator().applyValidationDecoration( ValidationMessage.error(eventTable, Bundle.ShowInTimelineDialog_eventSelectionValidator_message())); } else { validationSupport.getValidationDecorator().removeDecorations(eventTable); } }); //require selection and validation of ammount spinner to enable show button getDialogPane().lookupButton(SHOW).disableProperty().bind(Bindings.or( validationSupport.invalidProperty(), eventTable.getSelectionModel().selectedItemProperty().isNull() )); //set result converter that uses selection. setResultConverter(buttonType -> (buttonType == SHOW) ? makeEventInTimeRange(eventTable.getSelectionModel().getSelectedItem()) : null ); setTitle(Bundle.ShowInTimelineDialog_fileTitle(StringUtils.abbreviateMiddle(getContentPathSafe(file), " ... ", 50))); } /** * Get the unique path for the content, or if that fails, just return the * name. * * NOTE: This was copied from IamgeUtils and should be refactored to avoid * duplication. * * @param content * * @return the unique path for the content, or if that fails, just the name. */ static String getContentPathSafe(Content content) { try { return content.getUniquePath(); } catch (TskCoreException tskCoreException) { String contentName = content.getName(); LOGGER.log(Level.SEVERE, "Failed to get unique path for " + contentName, tskCoreException); //NON-NLS return contentName; } } /** * Construct this Dialog's "result" from the given event. * * @param selectedEvent The SingleEvent to include in the EventInTimeRange * * @return The EventInTimeRange that is the "result" of this dialog. */ private ViewInTimelineRequestedEvent makeEventInTimeRange(SingleEvent selectedEvent) { Duration selectedDuration = unitComboBox.getSelectionModel().getSelectedItem().getBaseUnit().getDuration().multipliedBy(amountSpinner.getValue()); Interval range = IntervalUtils.getIntervalAround(Instant.ofEpochMilli(selectedEvent.getStartMillis()), selectedDuration); return new ViewInTimelineRequestedEvent(Collections.singleton(selectedEvent.getEventID()), range); } /** * ListCell that shows a ChronoUnit */ static private class ChronoUnitListCell extends ListCell<ChronoUnit> { @Override protected void updateItem(ChronoUnit item, boolean empty) { super.updateItem(item, empty); if (empty || item == null) { setText(null); } else { setText(WordUtils.capitalizeFully(item.toString())); } } } /** * TableCell that shows a formatted date/time for a given millisecond since * the unix epoch * * @param <X> Anything */ static private class DateTimeTableCell<X> extends TableCell<X, Long> { @Override protected void updateItem(Long item, boolean empty) { super.updateItem(item, empty); if (item == null || empty) { setText(null); } else { setText(TimeLineController.getZonedFormatter().print(item)); } } } /** * TableCell that shows a EventType including the associated icon. * * @param <X> Anything */ static private class TypeTableCell<X> extends TableCell<X, EventType> { @Override protected void updateItem(EventType item, boolean empty) { super.updateItem(item, empty); if (item == null || empty) { setText(null); setGraphic(null); } else { setText(item.getDisplayName()); setGraphic(new ImageView(item.getFXImage())); } } } }