19 package org.sleuthkit.autopsy.timeline;
 
   21 import java.io.IOException;
 
   23 import java.time.Duration;
 
   24 import java.time.Instant;
 
   25 import java.time.temporal.ChronoField;
 
   26 import java.time.temporal.ChronoUnit;
 
   27 import java.util.Arrays;
 
   28 import java.util.Collection;
 
   29 import java.util.Collections;
 
   30 import java.util.HashSet;
 
   31 import java.util.List;
 
   33 import java.util.logging.Level;
 
   34 import javafx.beans.binding.Bindings;
 
   35 import javafx.beans.property.SimpleObjectProperty;
 
   36 import javafx.fxml.FXML;
 
   37 import javafx.fxml.FXMLLoader;
 
   38 import javafx.scene.control.ButtonBar;
 
   39 import javafx.scene.control.ButtonType;
 
   40 import javafx.scene.control.ComboBox;
 
   41 import javafx.scene.control.Dialog;
 
   42 import javafx.scene.control.DialogPane;
 
   43 import javafx.scene.control.Label;
 
   44 import javafx.scene.control.ListCell;
 
   45 import javafx.scene.control.Spinner;
 
   46 import javafx.scene.control.SpinnerValueFactory;
 
   47 import javafx.scene.control.TableCell;
 
   48 import javafx.scene.control.TableColumn;
 
   49 import javafx.scene.control.TableView;
 
   50 import javafx.scene.image.ImageView;
 
   51 import javafx.scene.layout.VBox;
 
   52 import javafx.stage.Modality;
 
   53 import javafx.util.converter.IntegerStringConverter;
 
   54 import org.apache.commons.lang3.StringUtils;
 
   55 import org.apache.commons.lang3.math.NumberUtils;
 
   56 import org.apache.commons.text.WordUtils;
 
   57 import org.controlsfx.validation.ValidationMessage;
 
   58 import org.controlsfx.validation.ValidationSupport;
 
   59 import org.controlsfx.validation.Validator;
 
   60 import org.joda.time.Interval;
 
   61 import org.openide.util.NbBundle;
 
   78 @SuppressWarnings(
"PMD.SingularField") 
 
   79 final class ShowInTimelineDialog extends Dialog<ViewInTimelineRequestedEvent> {
 
   81     private static final Logger LOGGER = Logger.getLogger(ShowInTimelineDialog.class.getName());
 
   83     @NbBundle.Messages({
"ShowInTimelineDialog.showTimelineButtonType.text=Show Timeline"})
 
   84     private static final ButtonType SHOW = 
new ButtonType(Bundle.ShowInTimelineDialog_showTimelineButtonType_text(), ButtonBar.ButtonData.OK_DONE);
 
   90     private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList(
 
   92             ChronoField.MONTH_OF_YEAR,
 
   93             ChronoField.DAY_OF_MONTH,
 
   94             ChronoField.HOUR_OF_DAY,
 
   95             ChronoField.MINUTE_OF_HOUR,
 
   96             ChronoField.SECOND_OF_MINUTE);
 
   99     private TableView<TimelineEvent> eventTable;
 
  102     private TableColumn<TimelineEvent, TimelineEventType> typeColumn;
 
  105     private TableColumn<TimelineEvent, Long> dateTimeColumn;
 
  108     private Spinner<Integer> amountSpinner;
 
  111     private ComboBox<ChronoField> unitComboBox;
 
  114     private Label chooseEventLabel;
 
  116     private final VBox contentRoot = 
new VBox();
 
  118     private final ValidationSupport validationSupport = 
new ValidationSupport();
 
  128         "ShowInTimelineDialog.amountValidator.message=The entered amount must only contain digits."})
 
  129     private ShowInTimelineDialog(TimeLineController controller, Collection<Long> eventIDS) 
throws TskCoreException {
 
  132         final String name = 
"nbres:/" + StringUtils.replace(ShowInTimelineDialog.class.getPackage().getName(), 
".", 
"/") + 
"/ShowInTimelineDialog.fxml"; 
 
  134             FXMLLoader fxmlLoader = 
new FXMLLoader(
new URL(name));
 
  135             fxmlLoader.setRoot(contentRoot);
 
  136             fxmlLoader.setController(
this);
 
  139         } 
catch (IOException ex) {
 
  140             LOGGER.log(Level.SEVERE, 
"Unable to load FXML, node initialization may not be complete.", ex); 
 
  143         assert eventTable != null : 
"fx:id=\"eventTable\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
 
  144         assert typeColumn != null : 
"fx:id=\"typeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
 
  145         assert dateTimeColumn != null : 
"fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
 
  146         assert amountSpinner != null : 
"fx:id=\"amountsSpinner\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
 
  147         assert unitComboBox != null : 
"fx:id=\"unitChoiceBox\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
 
  150         validationSupport.registerValidator(amountSpinner.getEditor(), 
false,
 
  151                 Validator.createPredicateValidator(NumberUtils::isDigits, Bundle.ShowInTimelineDialog_amountValidator_message()));
 
  154         PromptDialogManager.setDialogIcons(
this);
 
  155         initModality(Modality.APPLICATION_MODAL);
 
  158         DialogPane dialogPane = getDialogPane();
 
  159         dialogPane.setContent(contentRoot);
 
  161         dialogPane.getButtonTypes().setAll(SHOW, ButtonType.CANCEL);
 
  164         amountSpinner.setValueFactory(
new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 1000));
 
  165         amountSpinner.getValueFactory().setConverter(
new IntegerStringConverter() {
 
  176             public Integer fromString(String 
string) {
 
  178                     return super.fromString(
string);
 
  179                 } 
catch (NumberFormatException ex) {
 
  180                     return amountSpinner.getValue();
 
  185         unitComboBox.setButtonCell(
new ChronoFieldListCell());
 
  186         unitComboBox.setCellFactory(comboBox -> 
new ChronoFieldListCell());
 
  187         unitComboBox.getItems().setAll(SCROLL_BY_UNITS);
 
  188         unitComboBox.getSelectionModel().select(ChronoField.MINUTE_OF_HOUR);
 
  190         typeColumn.setCellValueFactory(param -> 
new SimpleObjectProperty<>(param.getValue().getEventType()));
 
  191         typeColumn.setCellFactory(param -> 
new TypeTableCell<>());
 
  193         dateTimeColumn.setCellValueFactory(param -> 
new SimpleObjectProperty<>(param.getValue().getEventTimeInMs()));
 
  194         dateTimeColumn.setCellFactory(param -> 
new DateTimeTableCell<>());
 
  197         Set<TimelineEvent> events = 
new HashSet<>();
 
  198         EventsModel eventsModel = controller.getEventsModel();
 
  199         for (Long eventID : eventIDS) {
 
  201                 events.add(eventsModel.getEventById(eventID));
 
  202             } 
catch (TskCoreException ex) {
 
  203                 throw new TskCoreException(
"Error getting event by id.", ex);
 
  206         eventTable.getItems().setAll(events);
 
  207         eventTable.setPrefHeight(Math.min(200, 24 * eventTable.getItems().size() + 28));
 
  217     @NbBundle.Messages({
"ShowInTimelineDialog.artifactTitle=View Result in Timeline."})
 
  218     ShowInTimelineDialog(TimeLineController controller, BlackboardArtifact artifact) 
throws TskCoreException {
 
  220         this(controller, controller.getEventsModel().getEventIDsForArtifact(artifact));
 
  223         chooseEventLabel.setVisible(
false);
 
  224         chooseEventLabel.setManaged(
false);
 
  225         eventTable.getSelectionModel().select(0);
 
  228         getDialogPane().lookupButton(SHOW).disableProperty().bind(validationSupport.invalidProperty());
 
  231         setResultConverter(buttonType -> (buttonType == SHOW)
 
  232                 ? makeEventInTimeRange(eventTable.getItems().get(0))
 
  235         setTitle(Bundle.ShowInTimelineDialog_artifactTitle());
 
  245     @NbBundle.Messages({
"# {0} - file path",
 
  246         "ShowInTimelineDialog.fileTitle=View {0} in timeline.",
 
  247         "ShowInTimelineDialog.eventSelectionValidator.message=You must select an event."})
 
  248     ShowInTimelineDialog(TimeLineController controller, AbstractFile file) 
throws TskCoreException {
 
  249         this(controller, controller.getEventsModel().getEventIDsForFile(file, 
false));
 
  256         eventTable.getSelectionModel().selectedItemProperty().isNull().addListener((selectedItemNullProperty, wasNull, isNull) -> {
 
  258                 validationSupport.getValidationDecorator().applyValidationDecoration(
 
  259                         ValidationMessage.error(eventTable, Bundle.ShowInTimelineDialog_eventSelectionValidator_message()));
 
  261                 validationSupport.getValidationDecorator().removeDecorations(eventTable);
 
  266         getDialogPane().lookupButton(SHOW).disableProperty().bind(Bindings.or(
 
  267                 validationSupport.invalidProperty(),
 
  268                 eventTable.getSelectionModel().selectedItemProperty().isNull()
 
  272         setResultConverter(buttonType -> (buttonType == SHOW)
 
  273                 ? makeEventInTimeRange(eventTable.getSelectionModel().getSelectedItem())
 
  277         setTitle(Bundle.ShowInTimelineDialog_fileTitle(StringUtils.abbreviateMiddle(getContentPathSafe(file), 
" ... ", 50)));
 
  291     static String getContentPathSafe(Content content) {
 
  293             return content.getUniquePath();
 
  294         } 
catch (TskCoreException tskCoreException) {
 
  295             String contentName = content.getName();
 
  296             LOGGER.log(Level.SEVERE, 
"Failed to get unique path for " + contentName, tskCoreException); 
 
  308     private ViewInTimelineRequestedEvent makeEventInTimeRange(TimelineEvent selectedEvent) {
 
  309         Duration selectedDuration = unitComboBox.getSelectionModel().getSelectedItem().getBaseUnit().getDuration().multipliedBy(amountSpinner.getValue());
 
  310         Interval range = IntervalUtils.getIntervalAround(Instant.ofEpochMilli(selectedEvent.getEventTimeInMs()), selectedDuration);
 
  311         return new ViewInTimelineRequestedEvent(Collections.singleton(selectedEvent.getEventID()), range);
 
  321             super.updateItem(item, empty);
 
  323             if (empty || item == null) {
 
  326                 setText(WordUtils.capitalizeFully(item.toString()));
 
  341             super.updateItem(item, empty);
 
  343             if (item == null || empty) {
 
  356     static private class TypeTableCell<X> 
extends TableCell<X, TimelineEventType> {
 
  360             super.updateItem(item, empty);
 
  362             if (item == null || empty) {
 
void updateItem(Long item, boolean empty)
 
void updateItem(ChronoUnit item, boolean empty)
 
void updateItem(TimelineEventType item, boolean empty)
 
static String getImagePath(TimelineEventType type)
 
static DateTimeFormatter getZonedFormatter()