19 package org.sleuthkit.autopsy.timeline.ui.listvew;
 
   21 import com.google.common.collect.Iterables;
 
   22 import com.google.common.math.DoubleMath;
 
   23 import java.math.RoundingMode;
 
   24 import java.time.Instant;
 
   25 import java.time.ZoneId;
 
   26 import java.time.ZonedDateTime;
 
   27 import java.time.temporal.ChronoField;
 
   28 import java.time.temporal.TemporalUnit;
 
   29 import java.util.ArrayList;
 
   30 import java.util.Arrays;
 
   31 import java.util.Collection;
 
   32 import java.util.Collections;
 
   33 import java.util.Comparator;
 
   34 import java.util.List;
 
   35 import java.util.Objects;
 
   37 import java.util.SortedSet;
 
   38 import java.util.TreeSet;
 
   39 import java.util.concurrent.ConcurrentSkipListSet;
 
   40 import java.util.function.Consumer;
 
   41 import java.util.function.Function;
 
   42 import java.util.logging.Level;
 
   43 import java.util.stream.Collectors;
 
   44 import javafx.application.Platform;
 
   45 import javafx.beans.binding.Bindings;
 
   46 import javafx.beans.binding.IntegerBinding;
 
   47 import javafx.beans.binding.StringBinding;
 
   48 import javafx.beans.property.SimpleObjectProperty;
 
   49 import javafx.beans.value.ObservableValue;
 
   50 import javafx.collections.ListChangeListener;
 
   51 import javafx.event.ActionEvent;
 
   52 import javafx.fxml.FXML;
 
   53 import javafx.geometry.Pos;
 
   54 import javafx.scene.Node;
 
   55 import javafx.scene.control.Button;
 
   56 import javafx.scene.control.ComboBox;
 
   57 import javafx.scene.control.ContextMenu;
 
   58 import javafx.scene.control.Label;
 
   59 import javafx.scene.control.MenuItem;
 
   60 import javafx.scene.control.OverrunStyle;
 
   61 import javafx.scene.control.SelectionMode;
 
   62 import javafx.scene.control.SeparatorMenuItem;
 
   63 import javafx.scene.control.TableCell;
 
   64 import javafx.scene.control.TableColumn;
 
   65 import javafx.scene.control.TableRow;
 
   66 import javafx.scene.control.TableView;
 
   67 import javafx.scene.control.Tooltip;
 
   68 import javafx.scene.image.Image;
 
   69 import javafx.scene.image.ImageView;
 
   70 import javafx.scene.layout.BorderPane;
 
   71 import javafx.scene.layout.HBox;
 
   72 import javafx.scene.layout.VBox;
 
   73 import javafx.util.Callback;
 
   74 import javax.swing.Action;
 
   75 import javax.swing.JMenuItem;
 
   76 import org.controlsfx.control.Notifications;
 
   77 import org.controlsfx.control.action.ActionUtils;
 
   78 import org.openide.awt.Actions;
 
   79 import org.openide.util.NbBundle;
 
   80 import org.openide.util.actions.Presenter;
 
  102 class ListTimeline 
extends BorderPane {
 
  106     private static final Image HASH_HIT = 
new Image(
"/org/sleuthkit/autopsy/images/hashset_hits.png");  
 
  107     private static final Image TAG = 
new Image(
"/org/sleuthkit/autopsy/images/green-tag-icon-16.png");  
 
  108     private static final Image FIRST = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_first.png");  
 
  109     private static final Image PREVIOUS = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_previous.png");  
 
  110     private static final Image NEXT = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_next.png");  
 
  111     private static final Image LAST = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_last.png");  
 
  116     private static final Callback<TableColumn.CellDataFeatures<
CombinedEvent, 
CombinedEvent>, ObservableValue<CombinedEvent>> CELL_VALUE_FACTORY = param -> 
new SimpleObjectProperty<>(param.getValue());
 
  118     private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList(
 
  120             ChronoField.MONTH_OF_YEAR,
 
  121             ChronoField.DAY_OF_MONTH,
 
  122             ChronoField.HOUR_OF_DAY,
 
  123             ChronoField.MINUTE_OF_HOUR,
 
  124             ChronoField.SECOND_OF_MINUTE);
 
  126     private static final int DEFAULT_ROW_HEIGHT = 24;
 
  129     private HBox navControls;
 
  132     private ComboBox<ChronoField> scrollInrementComboBox;
 
  135     private Button firstButton;
 
  138     private Button previousButton;
 
  141     private Button nextButton;
 
  144     private Button lastButton;
 
  147     private Label eventCountLabel;
 
  149     private TableView<CombinedEvent> table;
 
  151     private TableColumn<CombinedEvent, CombinedEvent> idColumn;
 
  153     private TableColumn<CombinedEvent, CombinedEvent> dateTimeColumn;
 
  155     private TableColumn<CombinedEvent, CombinedEvent> descriptionColumn;
 
  157     private TableColumn<CombinedEvent, CombinedEvent> typeColumn;
 
  159     private TableColumn<CombinedEvent, CombinedEvent> knownColumn;
 
  161     private TableColumn<CombinedEvent, CombinedEvent> taggedColumn;
 
  163     private TableColumn<CombinedEvent, CombinedEvent> hashHitColumn;
 
  169     private final SortedSet<CombinedEvent> visibleEvents;
 
  172     private final SleuthkitCase sleuthkitCase;
 
  180     private final ListChangeListener<CombinedEvent> selectedEventListener = 
new ListChangeListener<CombinedEvent>() {
 
  182         public void onChanged(ListChangeListener.Change<? extends CombinedEvent> c) {
 
  183             controller.
selectEventIDs(table.getSelectionModel().getSelectedItems().stream()
 
  184                     .filter(Objects::nonNull)
 
  186                     .collect(Collectors.toSet()));
 
  196         this.controller = controller;
 
  200         this.visibleEvents = 
new ConcurrentSkipListSet<>(Comparator.comparing(table.getItems()::indexOf));
 
  205         "# {0} - the number of events",
 
  206         "ListTimeline.eventCountLabel.text={0} events"})
 
  208         assert eventCountLabel != null : 
"fx:id=\"eventCountLabel\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  209         assert table != null : 
"fx:id=\"table\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  210         assert idColumn != null : 
"fx:id=\"idColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  211         assert dateTimeColumn != null : 
"fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  212         assert descriptionColumn != null : 
"fx:id=\"descriptionColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  213         assert typeColumn != null : 
"fx:id=\"typeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  214         assert knownColumn != null : 
"fx:id=\"knownColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  219         scrollInrementComboBox.getItems().setAll(SCROLL_BY_UNITS);
 
  220         scrollInrementComboBox.getSelectionModel().select(ChronoField.YEAR);
 
  221         ActionUtils.configureButton(
new ScrollToFirst(), firstButton);
 
  222         ActionUtils.configureButton(
new ScrollToPrevious(), previousButton);
 
  223         ActionUtils.configureButton(
new ScrollToNext(), nextButton);
 
  224         ActionUtils.configureButton(
new ScrollToLast(), lastButton);
 
  227         table.setRowFactory(tableView -> 
new EventRow());
 
  230         table.getColumns().remove(idColumn);
 
  233         dateTimeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  234         dateTimeColumn.setCellFactory(col -> 
new TextEventTableCell(singleEvent ->
 
  237         descriptionColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  238         descriptionColumn.setCellFactory(col -> 
new TextEventTableCell(singleEvent ->
 
  241         typeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  242         typeColumn.setCellFactory(col -> 
new EventTypeCell());
 
  244         knownColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  245         knownColumn.setCellFactory(col -> 
new TextEventTableCell(singleEvent ->
 
  246                 singleEvent.getKnown().getName()));
 
  248         taggedColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  249         taggedColumn.setCellFactory(col -> 
new TaggedCell());
 
  251         hashHitColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  252         hashHitColumn.setCellFactory(col -> 
new HashHitCell());
 
  255         eventCountLabel.textProperty().bind(
new StringBinding() {
 
  257                 bind(table.getItems());
 
  261             protected String computeValue() {
 
  262                 return Bundle.ListTimeline_eventCountLabel_text(table.getItems().size());
 
  267         table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
 
  268         table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
 
  278     void setCombinedEvents(Collection<CombinedEvent> events) {
 
  279         table.getItems().setAll(events);
 
  287     void selectEvents(Collection<Long> selectedEventIDs) {
 
  288         if (selectedEventIDs.isEmpty()) {
 
  290             table.getSelectionModel().clearSelection();
 
  302             table.getSelectionModel().getSelectedItems().removeListener(selectedEventListener);
 
  304             table.getSelectionModel().clearSelection();
 
  306             table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
 
  309             int[] selectedIndices = table.getItems().stream()
 
  310                     .filter(combinedEvent -> Collections.disjoint(combinedEvent.getEventIDs(), selectedEventIDs) == 
false)
 
  311                     .mapToInt(table.getItems()::indexOf)
 
  315             if (selectedIndices.length > 0) {
 
  316                 Integer firstSelectedIndex = selectedIndices[0];
 
  317                 table.getSelectionModel().selectIndices(firstSelectedIndex, selectedIndices);
 
  318                 scrollTo(firstSelectedIndex);
 
  319                 table.requestFocus(); 
 
  331     List<Node> getTimeNavigationControls() {
 
  332         return Collections.singletonList(navControls);
 
  342     private void scrollToAndFocus(Integer index) {
 
  343         table.requestFocus();
 
  345         table.getFocusModel().focus(index);
 
  353     private void scrollTo(Integer index) {
 
  354         if (visibleEvents.contains(table.getItems().get(index)) == 
false) {
 
  355             table.scrollTo(DoubleMath.roundToInt(index - ((table.getHeight() / DEFAULT_ROW_HEIGHT)) / 2, RoundingMode.HALF_EVEN));
 
  365             "ListView.EventTypeCell.modifiedTooltip=File Modified ( M )",
 
  366             "ListView.EventTypeCell.accessedTooltip=File Accessed ( A )",
 
  367             "ListView.EventTypeCell.createdTooltip=File Created ( B, for Born )",
 
  368             "ListView.EventTypeCell.changedTooltip=File Changed ( C )" 
  371         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  372             super.updateItem(item, empty);
 
  374             if (empty || item == null) {
 
  380                     String typeString = 
""; 
 
  381                     VBox toolTipVbox = 
new VBox(5);
 
  383                     for (FileSystemTypes type : Arrays.asList(FileSystemTypes.FILE_MODIFIED, FileSystemTypes.FILE_ACCESSED, FileSystemTypes.FILE_CHANGED, FileSystemTypes.FILE_CREATED)) {
 
  388                                     toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_modifiedTooltip(), 
new ImageView(type.getFXImage())));
 
  392                                     toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_accessedTooltip(), 
new ImageView(type.getFXImage())));
 
  396                                     toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_createdTooltip(), 
new ImageView(type.getFXImage())));
 
  400                                     toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_changedTooltip(), 
new ImageView(type.getFXImage())));
 
  403                                     throw new UnsupportedOperationException(
"Unknown FileSystemType: " + type.name()); 
 
  411                     Tooltip tooltip = 
new Tooltip();
 
  412                     tooltip.setGraphic(toolTipVbox);
 
  418                     setGraphic(
new ImageView(eventType.
getFXImage()));
 
  434             setAlignment(Pos.CENTER);
 
  438             "ListTimeline.taggedTooltip.error=There was a problem getting the tag names for the selected event.",
 
  440             "ListTimeline.taggedTooltip.text=Tags:\n{0}"})
 
  442         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  443             super.updateItem(item, empty);
 
  445             if (empty || item == null || (getEvent().isTagged() == 
false)) {
 
  453                 setGraphic(
new ImageView(TAG));
 
  455                 SortedSet<String> tagNames = 
new TreeSet<>();
 
  458                     AbstractFile abstractFileById = sleuthkitCase.getAbstractFileById(getEvent().getFileID());
 
  460                             .map(tag -> tag.getName().getDisplayName())
 
  461                             .forEach(tagNames::add);
 
  463                 } 
catch (TskCoreException ex) {
 
  464                     LOGGER.log(Level.SEVERE, 
"Failed to lookup tags for obj id " + getEvent().getFileID(), ex); 
 
  465                     Platform.runLater(() -> {
 
  466                         Notifications.create()
 
  467                                 .owner(getScene().getWindow())
 
  468                                 .text(Bundle.ListTimeline_taggedTooltip_error())
 
  475                         BlackboardArtifact artifact = sleuthkitCase.getBlackboardArtifact(artifactID);
 
  477                                 .map(tag -> tag.getName().getDisplayName())
 
  478                                 .forEach(tagNames::add);
 
  479                     } 
catch (TskCoreException ex) {
 
  480                         LOGGER.log(Level.SEVERE, 
"Failed to lookup tags for artifact id " + artifactID, ex); 
 
  481                         Platform.runLater(() -> {
 
  482                             Notifications.create()
 
  483                                     .owner(getScene().getWindow())
 
  484                                     .text(Bundle.ListTimeline_taggedTooltip_error())
 
  489                 Tooltip tooltip = 
new Tooltip(Bundle.ListTimeline_taggedTooltip_text(String.join(
"\n", tagNames))); 
 
  490                 tooltip.setGraphic(
new ImageView(TAG));
 
  506             setAlignment(Pos.CENTER);
 
  510             "ListTimeline.hashHitTooltip.error=There was a problem getting the hash set names for the selected event.",
 
  511             "# {0} - hash set names",
 
  512             "ListTimeline.hashHitTooltip.text=Hash Sets:\n{0}"})
 
  514         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  515             super.updateItem(item, empty);
 
  517             if (empty || item == null || (getEvent().isHashHit() == 
false)) {
 
  526                 setGraphic(
new ImageView(HASH_HIT));
 
  528                     Set<String> hashSetNames = 
new TreeSet<>(sleuthkitCase.getAbstractFileById(getEvent().getFileID()).getHashSetNames());
 
  529                     Tooltip tooltip = 
new Tooltip(Bundle.ListTimeline_hashHitTooltip_text(String.join(
"\n", hashSetNames))); 
 
  530                     tooltip.setGraphic(
new ImageView(HASH_HIT));
 
  532                 } 
catch (TskCoreException ex) {
 
  533                     LOGGER.log(Level.SEVERE, 
"Failed to lookup hash set names for obj id " + getEvent().getFileID(), ex); 
 
  534                     Platform.runLater(() -> {
 
  535                         Notifications.create()
 
  536                                 .owner(getScene().getWindow())
 
  537                                 .text(Bundle.ListTimeline_hashHitTooltip_error())
 
  560             setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
 
  561             setEllipsisString(
" ... "); 
 
  565         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  566             super.updateItem(item, empty);
 
  567             if (empty || item == null) {
 
  570                 setText(textSupplier.apply(getEvent()));
 
  579     private abstract class EventTableCell extends TableCell<CombinedEvent, CombinedEvent> {
 
  593         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  594             super.updateItem(item, empty);
 
  596             if (empty || item == null) {
 
  608     private class EventRow extends TableRow<CombinedEvent> {
 
  622             "ListChart.errorMsg=There was a problem getting the content for the selected event."})
 
  624         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  625             CombinedEvent oldItem = getItem();
 
  626             if (oldItem != null) {
 
  627                 visibleEvents.remove(oldItem);
 
  629             super.updateItem(item, empty);
 
  631             if (empty || item == null) {
 
  634                 visibleEvents.add(item);
 
  637                 setOnContextMenuRequested(contextMenuEvent -> {
 
  641                         List<MenuItem> menuItems = 
new ArrayList<>();
 
  644                         for (Action action : node.
getActions(
false)) {
 
  645                             if (action == null) {
 
  647                                 menuItems.add(
new SeparatorMenuItem());
 
  649                                 String actionName = Objects.toString(action.getValue(Action.NAME));
 
  651                                 if (Arrays.asList(
"&Properties", 
"Tools").contains(actionName) == 
false) { 
 
  652                                     if (action instanceof Presenter.Popup) {
 
  659                                         JMenuItem submenu = ((Presenter.Popup) action).getPopupPresenter();
 
  669                         new ContextMenu(menuItems.toArray(
new MenuItem[menuItems.size()]))
 
  670                                 .show(
this, contextMenuEvent.getScreenX(), contextMenuEvent.getScreenY());
 
  671                     } 
catch (IllegalStateException ex) {
 
  673                         LOGGER.log(Level.SEVERE, 
"There was no case open to lookup the Sleuthkit object backing a SingleEvent.", ex); 
 
  674                     } 
catch (TskCoreException ex) {
 
  675                         LOGGER.log(Level.SEVERE, 
"Failed to lookup Sleuthkit object backing a SingleEvent.", ex); 
 
  676                         Platform.runLater(() -> {
 
  677                             Notifications.create()
 
  678                                     .owner(getScene().getWindow())
 
  679                                     .text(Bundle.ListChart_errorMsg())
 
  692             super(
"", 
new Consumer<ActionEvent>() { 
 
  694                 public void accept(ActionEvent actionEvent) {
 
  698             setGraphic(
new ImageView(FIRST));
 
  699             disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
 
  706             super(
"", 
new Consumer<ActionEvent>() {  
 
  708                 public void accept(ActionEvent actionEvent) {
 
  709                     scrollToAndFocus(table.getItems().size() - 1);
 
  712             setGraphic(
new ImageView(LAST));
 
  713             IntegerBinding size = Bindings.size(table.getItems());
 
  714             disabledProperty().bind(size.isEqualTo(0).or(
 
  715                     table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
 
  722             super(
"", 
new Consumer<ActionEvent>() { 
 
  724                 public void accept(ActionEvent actionEvent) {
 
  725                     ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
 
  727                     TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
 
  729                     int focusedIndex = table.getFocusModel().getFocusedIndex();
 
  730                     CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
 
  731                     if (-1 == focusedIndex || null == focusedItem) {
 
  732                         focusedItem = visibleEvents.first();
 
  733                         focusedIndex = table.getItems().indexOf(focusedItem);
 
  736                     ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
 
  737                     ZonedDateTime nextDateTime = focusedDateTime.plus(1, selectedUnit);
 
  738                     for (ChronoField field : SCROLL_BY_UNITS) {
 
  739                         if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
 
  740                             nextDateTime = nextDateTime.with(field, field.rangeRefinedBy(nextDateTime).getMinimum());
 
  743                     long nextMillis = nextDateTime.toInstant().toEpochMilli();
 
  745                     int nextIndex = table.getItems().size() - 1;
 
  746                     for (
int i = focusedIndex; i < table.getItems().size(); i++) {
 
  747                         if (table.getItems().get(i).getStartMillis() >= nextMillis) {
 
  752                     scrollToAndFocus(nextIndex);
 
  755             setGraphic(
new ImageView(NEXT));
 
  756             IntegerBinding size = Bindings.size(table.getItems());
 
  757             disabledProperty().bind(size.isEqualTo(0).or(
 
  758                     table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
 
  766             super(
"", 
new Consumer<ActionEvent>() { 
 
  768                 public void accept(ActionEvent actionEvent) {
 
  770                     ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
 
  771                     TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
 
  773                     int focusedIndex = table.getFocusModel().getFocusedIndex();
 
  774                     CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
 
  775                     if (-1 == focusedIndex || null == focusedItem) {
 
  776                         focusedItem = visibleEvents.last();
 
  777                         focusedIndex = table.getItems().indexOf(focusedItem);
 
  780                     ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
 
  781                     ZonedDateTime previousDateTime = focusedDateTime.minus(1, selectedUnit);
 
  783                     for (ChronoField field : SCROLL_BY_UNITS) {
 
  784                         if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
 
  785                             previousDateTime = previousDateTime.with(field, field.rangeRefinedBy(previousDateTime).getMaximum());
 
  788                     long previousMillis = previousDateTime.toInstant().toEpochMilli();
 
  790                     int previousIndex = 0;
 
  791                     for (
int i = focusedIndex; i > 0; i--) {
 
  792                         if (table.getItems().get(i).getStartMillis() <= previousMillis) {
 
  798                     scrollToAndFocus(previousIndex);
 
  801             setGraphic(
new ImageView(PREVIOUS));
 
  802             disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
 
Optional< Long > getArtifactID()
FilteredEventsModel getEventsModel()
static EventNode createEventNode(final Long eventID, FilteredEventsModel eventsModel)
Action[] getActions(boolean context)
synchronized void selectEventIDs(Collection< Long > eventIDs)
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
TagsManager getTagsManager()
void updateItem(CombinedEvent item, boolean empty)
Long getRepresentativeEventID()
static ZoneId getTimeZoneID()
final Function< SingleEvent, String > textSupplier
SleuthkitCase getSleuthkitCase()
Set< EventType > getEventTypes()
synchronized ObservableList< Long > getSelectedEventIDs()
synchronized static Logger getLogger(String name)
void updateItem(CombinedEvent item, boolean empty)
SingleEvent getEventById(Long eventID)
static DateTimeFormatter getZonedFormatter()
static void construct(Node node, String fxmlFileName)