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.Collections;
 
   29 import java.util.List;
 
   30 import java.util.logging.Level;
 
   31 import java.util.stream.Collectors;
 
   32 import javafx.beans.binding.Bindings;
 
   33 import javafx.beans.property.SimpleObjectProperty;
 
   34 import javafx.fxml.FXML;
 
   35 import javafx.fxml.FXMLLoader;
 
   36 import javafx.scene.control.ButtonBar;
 
   37 import javafx.scene.control.ButtonType;
 
   38 import javafx.scene.control.ComboBox;
 
   39 import javafx.scene.control.Dialog;
 
   40 import javafx.scene.control.DialogPane;
 
   41 import javafx.scene.control.Label;
 
   42 import javafx.scene.control.ListCell;
 
   43 import javafx.scene.control.Spinner;
 
   44 import javafx.scene.control.SpinnerValueFactory;
 
   45 import javafx.scene.control.TableCell;
 
   46 import javafx.scene.control.TableColumn;
 
   47 import javafx.scene.control.TableView;
 
   48 import javafx.scene.image.ImageView;
 
   49 import javafx.scene.layout.VBox;
 
   50 import javafx.stage.Modality;
 
   51 import javafx.util.converter.IntegerStringConverter;
 
   52 import org.apache.commons.lang3.StringUtils;
 
   53 import org.apache.commons.lang3.math.NumberUtils;
 
   54 import org.apache.commons.lang3.text.WordUtils;
 
   55 import org.controlsfx.validation.ValidationMessage;
 
   56 import org.controlsfx.validation.ValidationSupport;
 
   57 import org.controlsfx.validation.Validator;
 
   58 import org.joda.time.Interval;
 
   59 import org.openide.util.NbBundle;
 
   75 @SuppressWarnings(
"PMD.SingularField") 
 
   76 final class ShowInTimelineDialog extends Dialog<ViewInTimelineRequestedEvent> {
 
   78     private static final Logger LOGGER = Logger.getLogger(ShowInTimelineDialog.class.getName());
 
   80     @NbBundle.Messages({
"ShowInTimelineDialog.showTimelineButtonType.text=Show Timeline"})
 
   81     private static final ButtonType SHOW = 
new ButtonType(Bundle.ShowInTimelineDialog_showTimelineButtonType_text(), ButtonBar.ButtonData.OK_DONE);
 
   87     private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList(
 
   89             ChronoField.MONTH_OF_YEAR,
 
   90             ChronoField.DAY_OF_MONTH,
 
   91             ChronoField.HOUR_OF_DAY,
 
   92             ChronoField.MINUTE_OF_HOUR,
 
   93             ChronoField.SECOND_OF_MINUTE);
 
   96     private TableView<SingleEvent> eventTable;
 
   99     private TableColumn<SingleEvent, EventType> typeColumn;
 
  102     private TableColumn<SingleEvent, Long> dateTimeColumn;
 
  105     private Spinner<Integer> amountSpinner;
 
  108     private ComboBox<ChronoField> unitComboBox;
 
  111     private Label chooseEventLabel;
 
  113     private final VBox contentRoot = 
new VBox();
 
  115     private final TimeLineController controller;
 
  117     private final ValidationSupport validationSupport = 
new ValidationSupport();
 
  127         "ShowInTimelineDialog.amountValidator.message=The entered amount must only contain digits." 
  129     private ShowInTimelineDialog(TimeLineController controller, List<Long> eventIDS) {
 
  130         this.controller = controller;
 
  133         final String name = 
"nbres:/" + StringUtils.replace(ShowInTimelineDialog.class.getPackage().getName(), 
".", 
"/") + 
"/ShowInTimelineDialog.fxml"; 
 
  135             FXMLLoader fxmlLoader = 
new FXMLLoader(
new URL(name));
 
  136             fxmlLoader.setRoot(contentRoot);
 
  137             fxmlLoader.setController(
this);
 
  140         } 
catch (IOException ex) {
 
  141             LOGGER.log(Level.SEVERE, 
"Unable to load FXML, node initialization may not be complete.", ex); 
 
  144         assert eventTable != null : 
"fx:id=\"eventTable\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
 
  145         assert typeColumn != null : 
"fx:id=\"typeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
 
  146         assert dateTimeColumn != null : 
"fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
 
  147         assert amountSpinner != null : 
"fx:id=\"amountsSpinner\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
 
  148         assert unitComboBox != null : 
"fx:id=\"unitChoiceBox\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
 
  151         validationSupport.registerValidator(amountSpinner.getEditor(), 
false,
 
  152                 Validator.createPredicateValidator(NumberUtils::isDigits, Bundle.ShowInTimelineDialog_amountValidator_message()));
 
  155         PromptDialogManager.setDialogIcons(
this);
 
  156         initModality(Modality.APPLICATION_MODAL);
 
  159         DialogPane dialogPane = getDialogPane();
 
  160         dialogPane.setContent(contentRoot);
 
  162         dialogPane.getButtonTypes().setAll(SHOW, ButtonType.CANCEL);
 
  165         amountSpinner.setValueFactory(
new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 1000));
 
  166         amountSpinner.getValueFactory().setConverter(
new IntegerStringConverter() {
 
  177             public Integer fromString(String 
string) {
 
  179                     return super.fromString(
string);
 
  180                 } 
catch (NumberFormatException ex) {
 
  181                     return amountSpinner.getValue();
 
  186         unitComboBox.setButtonCell(
new ChronoFieldListCell());
 
  187         unitComboBox.setCellFactory(comboBox -> 
new ChronoFieldListCell());
 
  188         unitComboBox.getItems().setAll(SCROLL_BY_UNITS);
 
  189         unitComboBox.getSelectionModel().select(ChronoField.MINUTE_OF_HOUR);
 
  191         typeColumn.setCellValueFactory(param -> 
new SimpleObjectProperty<>(param.getValue().getEventType()));
 
  192         typeColumn.setCellFactory(param -> 
new TypeTableCell<>());
 
  194         dateTimeColumn.setCellValueFactory(param -> 
new SimpleObjectProperty<>(param.getValue().getStartMillis()));
 
  195         dateTimeColumn.setCellFactory(param -> 
new DateTimeTableCell<>());
 
  198         eventTable.getItems().setAll(eventIDS.stream().map(controller.getEventsModel()::getEventById).collect(Collectors.toSet()));
 
  199         eventTable.setPrefHeight(Math.min(200, 24 * eventTable.getItems().size() + 28));
 
  209     @NbBundle.Messages({
"ShowInTimelineDialog.artifactTitle=View Result in Timeline."})
 
  210     ShowInTimelineDialog(TimeLineController controller, BlackboardArtifact artifact) {
 
  212         this(controller, controller.getEventsModel().getEventIDsForArtifact(artifact));
 
  215         chooseEventLabel.setVisible(
false);
 
  216         chooseEventLabel.setManaged(
false);
 
  217         eventTable.getSelectionModel().select(0);
 
  220         getDialogPane().lookupButton(SHOW).disableProperty().bind(validationSupport.invalidProperty());
 
  223         setResultConverter(buttonType -> (buttonType == SHOW)
 
  224                 ? makeEventInTimeRange(eventTable.getItems().get(0))
 
  227         setTitle(Bundle.ShowInTimelineDialog_artifactTitle());
 
  237     @NbBundle.Messages({
"# {0} - file path",
 
  238         "ShowInTimelineDialog.fileTitle=View {0} in timeline.",
 
  239         "ShowInTimelineDialog.eventSelectionValidator.message=You must select an event."})
 
  240     ShowInTimelineDialog(TimeLineController controller, AbstractFile file) {
 
  241         this(controller, controller.getEventsModel().getEventIDsForFile(file, 
false));
 
  248         eventTable.getSelectionModel().selectedItemProperty().isNull().addListener((selectedItemNullProperty, wasNull, isNull) -> {
 
  250                 validationSupport.getValidationDecorator().applyValidationDecoration(
 
  251                         ValidationMessage.error(eventTable, Bundle.ShowInTimelineDialog_eventSelectionValidator_message()));
 
  253                 validationSupport.getValidationDecorator().removeDecorations(eventTable);
 
  258         getDialogPane().lookupButton(SHOW).disableProperty().bind(Bindings.or(
 
  259                 validationSupport.invalidProperty(),
 
  260                 eventTable.getSelectionModel().selectedItemProperty().isNull()
 
  264         setResultConverter(buttonType -> (buttonType == SHOW)
 
  265                 ? makeEventInTimeRange(eventTable.getSelectionModel().getSelectedItem())
 
  269         setTitle(Bundle.ShowInTimelineDialog_fileTitle(StringUtils.abbreviateMiddle(getContentPathSafe(file), 
" ... ", 50)));
 
  283     static String getContentPathSafe(Content content) {
 
  285             return content.getUniquePath();
 
  286         } 
catch (TskCoreException tskCoreException) {
 
  287             String contentName = content.getName();
 
  288             LOGGER.log(Level.SEVERE, 
"Failed to get unique path for " + contentName, tskCoreException); 
 
  300     private ViewInTimelineRequestedEvent makeEventInTimeRange(SingleEvent selectedEvent) {
 
  301         Duration selectedDuration = unitComboBox.getSelectionModel().getSelectedItem().getBaseUnit().getDuration().multipliedBy(amountSpinner.getValue());
 
  302         Interval range = IntervalUtils.getIntervalAround(Instant.ofEpochMilli(selectedEvent.getStartMillis()), selectedDuration);
 
  303         return new ViewInTimelineRequestedEvent(Collections.singleton(selectedEvent.getEventID()), range);
 
  313             super.updateItem(item, empty);
 
  315             if (empty || item == null) {
 
  318                 setText(WordUtils.capitalizeFully(item.toString()));
 
  333             super.updateItem(item, empty);
 
  335             if (item == null || empty) {
 
  352             super.updateItem(item, empty);
 
  354             if (item == null || empty) {
 
void updateItem(Long item, boolean empty)
void updateItem(ChronoUnit item, boolean empty)
static DateTimeFormatter getZonedFormatter()
void updateItem(EventType item, boolean empty)