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.Function;
 
   41 import java.util.logging.Level;
 
   42 import java.util.stream.Collectors;
 
   43 import javafx.application.Platform;
 
   44 import javafx.beans.binding.Bindings;
 
   45 import javafx.beans.binding.IntegerBinding;
 
   46 import javafx.beans.binding.StringBinding;
 
   47 import javafx.beans.property.SimpleObjectProperty;
 
   48 import javafx.beans.value.ObservableValue;
 
   49 import javafx.collections.ListChangeListener;
 
   50 import javafx.fxml.FXML;
 
   51 import javafx.geometry.Pos;
 
   52 import javafx.scene.Node;
 
   53 import javafx.scene.control.Button;
 
   54 import javafx.scene.control.ComboBox;
 
   55 import javafx.scene.control.ContextMenu;
 
   56 import javafx.scene.control.Label;
 
   57 import javafx.scene.control.MenuItem;
 
   58 import javafx.scene.control.OverrunStyle;
 
   59 import javafx.scene.control.SelectionMode;
 
   60 import javafx.scene.control.SeparatorMenuItem;
 
   61 import javafx.scene.control.TableCell;
 
   62 import javafx.scene.control.TableColumn;
 
   63 import javafx.scene.control.TableRow;
 
   64 import javafx.scene.control.TableView;
 
   65 import javafx.scene.control.Tooltip;
 
   66 import javafx.scene.image.Image;
 
   67 import javafx.scene.image.ImageView;
 
   68 import javafx.scene.layout.BorderPane;
 
   69 import javafx.scene.layout.HBox;
 
   70 import javafx.scene.layout.VBox;
 
   71 import javafx.util.Callback;
 
   72 import javax.swing.Action;
 
   73 import javax.swing.JMenuItem;
 
   74 import org.controlsfx.control.Notifications;
 
   75 import org.controlsfx.control.action.ActionUtils;
 
   76 import org.openide.awt.Actions;
 
   77 import org.openide.util.NbBundle;
 
   78 import org.openide.util.actions.Presenter;
 
  100 class ListTimeline 
extends BorderPane {
 
  104     private static final Image HASH_HIT = 
new Image(
"/org/sleuthkit/autopsy/images/hashset_hits.png");  
 
  105     private static final Image TAG = 
new Image(
"/org/sleuthkit/autopsy/images/green-tag-icon-16.png");  
 
  106     private static final Image FIRST = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_first.png");  
 
  107     private static final Image PREVIOUS = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_previous.png");  
 
  108     private static final Image NEXT = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_next.png");  
 
  109     private static final Image LAST = 
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_last.png");  
 
  114     private static final Callback<TableColumn.CellDataFeatures<
CombinedEvent, 
CombinedEvent>, ObservableValue<CombinedEvent>> CELL_VALUE_FACTORY = param -> 
new SimpleObjectProperty<>(param.getValue());
 
  116     private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList(
 
  118             ChronoField.MONTH_OF_YEAR,
 
  119             ChronoField.DAY_OF_MONTH,
 
  120             ChronoField.HOUR_OF_DAY,
 
  121             ChronoField.MINUTE_OF_HOUR,
 
  122             ChronoField.SECOND_OF_MINUTE);
 
  124     private static final int DEFAULT_ROW_HEIGHT = 24;
 
  127     private HBox navControls;
 
  130     private ComboBox<ChronoField> scrollInrementComboBox;
 
  133     private Button firstButton;
 
  136     private Button previousButton;
 
  139     private Button nextButton;
 
  142     private Button lastButton;
 
  145     private Label eventCountLabel;
 
  147     private TableView<CombinedEvent> table;
 
  149     private TableColumn<CombinedEvent, CombinedEvent> idColumn;
 
  151     private TableColumn<CombinedEvent, CombinedEvent> dateTimeColumn;
 
  153     private TableColumn<CombinedEvent, CombinedEvent> descriptionColumn;
 
  155     private TableColumn<CombinedEvent, CombinedEvent> typeColumn;
 
  157     private TableColumn<CombinedEvent, CombinedEvent> knownColumn;
 
  159     private TableColumn<CombinedEvent, CombinedEvent> taggedColumn;
 
  161     private TableColumn<CombinedEvent, CombinedEvent> hashHitColumn;
 
  167     private final SortedSet<CombinedEvent> visibleEvents;
 
  170     private final SleuthkitCase sleuthkitCase;
 
  178     private final ListChangeListener<CombinedEvent> selectedEventListener = 
new ListChangeListener<CombinedEvent>() {
 
  180         public void onChanged(ListChangeListener.Change<? extends CombinedEvent> c) {
 
  181             controller.
selectEventIDs(table.getSelectionModel().getSelectedItems().stream()
 
  182                     .filter(Objects::nonNull)
 
  184                     .collect(Collectors.toSet()));
 
  194         this.controller = controller;
 
  198         this.visibleEvents = 
new ConcurrentSkipListSet<>(Comparator.comparing(table.getItems()::indexOf));
 
  203         "# {0} - the number of events",
 
  204         "ListTimeline.eventCountLabel.text={0} events"})
 
  206         assert eventCountLabel != null : 
"fx:id=\"eventCountLabel\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  207         assert table != null : 
"fx:id=\"table\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  208         assert idColumn != null : 
"fx:id=\"idColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  209         assert dateTimeColumn != null : 
"fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  210         assert descriptionColumn != null : 
"fx:id=\"descriptionColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  211         assert typeColumn != null : 
"fx:id=\"typeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  212         assert knownColumn != null : 
"fx:id=\"knownColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; 
 
  217         scrollInrementComboBox.getItems().setAll(SCROLL_BY_UNITS);
 
  218         scrollInrementComboBox.getSelectionModel().select(ChronoField.YEAR);
 
  219         ActionUtils.configureButton(
new ScrollToFirst(), firstButton);
 
  220         ActionUtils.configureButton(
new ScrollToPrevious(), previousButton);
 
  221         ActionUtils.configureButton(
new ScrollToNext(), nextButton);
 
  222         ActionUtils.configureButton(
new ScrollToLast(), lastButton);
 
  225         table.setRowFactory(tableView -> 
new EventRow());
 
  228         table.getColumns().remove(idColumn);
 
  231         dateTimeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  232         dateTimeColumn.setCellFactory(col -> 
new TextEventTableCell(singleEvent ->
 
  235         descriptionColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  236         descriptionColumn.setCellFactory(col -> 
new TextEventTableCell(singleEvent ->
 
  239         typeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  240         typeColumn.setCellFactory(col -> 
new EventTypeCell());
 
  242         knownColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  243         knownColumn.setCellFactory(col -> 
new TextEventTableCell(singleEvent ->
 
  244                 singleEvent.getKnown().getName()));
 
  246         taggedColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  247         taggedColumn.setCellFactory(col -> 
new TaggedCell());
 
  249         hashHitColumn.setCellValueFactory(CELL_VALUE_FACTORY);
 
  250         hashHitColumn.setCellFactory(col -> 
new HashHitCell());
 
  253         eventCountLabel.textProperty().bind(
new StringBinding() {
 
  255                 bind(table.getItems());
 
  259             protected String computeValue() {
 
  260                 return Bundle.ListTimeline_eventCountLabel_text(table.getItems().size());
 
  265         table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
 
  266         table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
 
  276     void setCombinedEvents(Collection<CombinedEvent> events) {
 
  277         table.getItems().setAll(events);
 
  285     void selectEvents(Collection<Long> selectedEventIDs) {
 
  286         if (selectedEventIDs.isEmpty()) {
 
  288             table.getSelectionModel().clearSelection();
 
  300             table.getSelectionModel().getSelectedItems().removeListener(selectedEventListener);
 
  302             table.getSelectionModel().clearSelection();
 
  304             table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
 
  307             int[] selectedIndices = table.getItems().stream()
 
  308                     .filter(combinedEvent -> Collections.disjoint(combinedEvent.getEventIDs(), selectedEventIDs) == 
false)
 
  309                     .mapToInt(table.getItems()::indexOf)
 
  313             if (selectedIndices.length > 0) {
 
  314                 Integer firstSelectedIndex = selectedIndices[0];
 
  315                 table.getSelectionModel().selectIndices(firstSelectedIndex, selectedIndices);
 
  316                 scrollTo(firstSelectedIndex);
 
  317                 table.requestFocus(); 
 
  329     List<Node> getTimeNavigationControls() {
 
  330         return Collections.singletonList(navControls);
 
  340     private void scrollToAndFocus(Integer index) {
 
  341         table.requestFocus();
 
  343         table.getFocusModel().focus(index);
 
  351     private void scrollTo(Integer index) {
 
  352         if (visibleEvents.contains(table.getItems().get(index)) == 
false) {
 
  353             table.scrollTo(DoubleMath.roundToInt(index - ((table.getHeight() / DEFAULT_ROW_HEIGHT)) / 2, RoundingMode.HALF_EVEN));
 
  363             "ListView.EventTypeCell.modifiedTooltip=File Modified ( M )",
 
  364             "ListView.EventTypeCell.accessedTooltip=File Accessed ( A )",
 
  365             "ListView.EventTypeCell.createdTooltip=File Created ( B, for Born )",
 
  366             "ListView.EventTypeCell.changedTooltip=File Changed ( C )" 
  369         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  370             super.updateItem(item, empty);
 
  372             if (empty || item == null) {
 
  378                     String typeString = 
""; 
 
  379                     VBox toolTipVbox = 
new VBox(5);
 
  381                     for (FileSystemTypes type : Arrays.asList(FileSystemTypes.FILE_MODIFIED, FileSystemTypes.FILE_ACCESSED, FileSystemTypes.FILE_CHANGED, FileSystemTypes.FILE_CREATED)) {
 
  386                                     toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_modifiedTooltip(), 
new ImageView(type.getFXImage())));
 
  390                                     toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_accessedTooltip(), 
new ImageView(type.getFXImage())));
 
  394                                     toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_createdTooltip(), 
new ImageView(type.getFXImage())));
 
  398                                     toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_changedTooltip(), 
new ImageView(type.getFXImage())));
 
  401                                     throw new UnsupportedOperationException(
"Unknown FileSystemType: " + type.name()); 
 
  409                     Tooltip tooltip = 
new Tooltip();
 
  410                     tooltip.setGraphic(toolTipVbox);
 
  416                     setGraphic(
new ImageView(eventType.
getFXImage()));
 
  432             setAlignment(Pos.CENTER);
 
  436             "ListTimeline.taggedTooltip.error=There was a problem getting the tag names for the selected event.",
 
  438             "ListTimeline.taggedTooltip.text=Tags:\n{0}"})
 
  440         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  441             super.updateItem(item, empty);
 
  443             if (empty || item == null || (getEvent().isTagged() == 
false)) {
 
  451                 setGraphic(
new ImageView(TAG));
 
  453                 SortedSet<String> tagNames = 
new TreeSet<>();
 
  456                     AbstractFile abstractFileById = sleuthkitCase.getAbstractFileById(getEvent().getFileID());
 
  458                             .map(tag -> tag.getName().getDisplayName())
 
  459                             .forEach(tagNames::add);
 
  461                 } 
catch (TskCoreException ex) {
 
  462                     LOGGER.log(Level.SEVERE, 
"Failed to lookup tags for obj id " + getEvent().getFileID(), ex); 
 
  463                     Platform.runLater(() -> {
 
  464                         Notifications.create()
 
  465                                 .owner(getScene().getWindow())
 
  466                                 .text(Bundle.ListTimeline_taggedTooltip_error())
 
  473                         BlackboardArtifact artifact = sleuthkitCase.getBlackboardArtifact(artifactID);
 
  475                                 .map(tag -> tag.getName().getDisplayName())
 
  476                                 .forEach(tagNames::add);
 
  477                     } 
catch (TskCoreException ex) {
 
  478                         LOGGER.log(Level.SEVERE, 
"Failed to lookup tags for artifact id " + artifactID, ex); 
 
  479                         Platform.runLater(() -> {
 
  480                             Notifications.create()
 
  481                                     .owner(getScene().getWindow())
 
  482                                     .text(Bundle.ListTimeline_taggedTooltip_error())
 
  487                 Tooltip tooltip = 
new Tooltip(Bundle.ListTimeline_taggedTooltip_text(String.join(
"\n", tagNames))); 
 
  488                 tooltip.setGraphic(
new ImageView(TAG));
 
  504             setAlignment(Pos.CENTER);
 
  508             "ListTimeline.hashHitTooltip.error=There was a problem getting the hash set names for the selected event.",
 
  509             "# {0} - hash set names",
 
  510             "ListTimeline.hashHitTooltip.text=Hash Sets:\n{0}"})
 
  512         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  513             super.updateItem(item, empty);
 
  515             if (empty || item == null || (getEvent().isHashHit() == 
false)) {
 
  524                 setGraphic(
new ImageView(HASH_HIT));
 
  526                     Set<String> hashSetNames = 
new TreeSet<>(sleuthkitCase.getAbstractFileById(getEvent().getFileID()).getHashSetNames());
 
  527                     Tooltip tooltip = 
new Tooltip(Bundle.ListTimeline_hashHitTooltip_text(String.join(
"\n", hashSetNames))); 
 
  528                     tooltip.setGraphic(
new ImageView(HASH_HIT));
 
  530                 } 
catch (TskCoreException ex) {
 
  531                     LOGGER.log(Level.SEVERE, 
"Failed to lookup hash set names for obj id " + getEvent().getFileID(), ex); 
 
  532                     Platform.runLater(() -> {
 
  533                         Notifications.create()
 
  534                                 .owner(getScene().getWindow())
 
  535                                 .text(Bundle.ListTimeline_hashHitTooltip_error())
 
  558             setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
 
  559             setEllipsisString(
" ... "); 
 
  563         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  564             super.updateItem(item, empty);
 
  565             if (empty || item == null) {
 
  568                 setText(textSupplier.apply(getEvent()));
 
  577     private abstract class EventTableCell extends TableCell<CombinedEvent, CombinedEvent> {
 
  591         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  592             super.updateItem(item, empty);
 
  594             if (empty || item == null) {
 
  606     private class EventRow extends TableRow<CombinedEvent> {
 
  620             "ListChart.errorMsg=There was a problem getting the content for the selected event."})
 
  622         protected void updateItem(CombinedEvent item, 
boolean empty) {
 
  623             CombinedEvent oldItem = getItem();
 
  624             if (oldItem != null) {
 
  625                 visibleEvents.remove(oldItem);
 
  627             super.updateItem(item, empty);
 
  629             if (empty || item == null) {
 
  632                 visibleEvents.add(item);
 
  635                 setOnContextMenuRequested(contextMenuEvent -> {
 
  639                         List<MenuItem> menuItems = 
new ArrayList<>();
 
  642                         for (Action action : node.
getActions(
false)) {
 
  643                             if (action == null) {
 
  645                                 menuItems.add(
new SeparatorMenuItem());
 
  647                                 String actionName = Objects.toString(action.getValue(Action.NAME));
 
  649                                 if (Arrays.asList(
"&Properties", 
"Tools").contains(actionName) == 
false) { 
 
  650                                     if (action instanceof Presenter.Popup) {
 
  657                                         JMenuItem submenu = ((Presenter.Popup) action).getPopupPresenter();
 
  667                         new ContextMenu(menuItems.toArray(
new MenuItem[menuItems.size()]))
 
  668                                 .show(
this, contextMenuEvent.getScreenX(), contextMenuEvent.getScreenY());
 
  669                     } 
catch (IllegalStateException ex) {
 
  671                         LOGGER.log(Level.SEVERE, 
"There was no case open to lookup the Sleuthkit object backing a SingleEvent.", ex); 
 
  672                     } 
catch (TskCoreException ex) {
 
  673                         LOGGER.log(Level.SEVERE, 
"Failed to lookup Sleuthkit object backing a SingleEvent.", ex); 
 
  674                         Platform.runLater(() -> {
 
  675                             Notifications.create()
 
  676                                     .owner(getScene().getWindow())
 
  677                                     .text(Bundle.ListChart_errorMsg())
 
  690             super(
"", actionEvent -> scrollToAndFocus(0));
 
  691             setGraphic(
new ImageView(FIRST));
 
  692             disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
 
  699             super(
"", actionEvent -> scrollToAndFocus(table.getItems().size() - 1));
 
  700             setGraphic(
new ImageView(LAST));
 
  701             IntegerBinding size = Bindings.size(table.getItems());
 
  702             disabledProperty().bind(size.isEqualTo(0).or(
 
  703                     table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
 
  710             super(
"", actionEvent -> {
 
  712                 ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
 
  714                 TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
 
  716                 int focusedIndex = table.getFocusModel().getFocusedIndex();
 
  717                 CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
 
  718                 if (-1 == focusedIndex || null == focusedItem) {
 
  719                     focusedItem = visibleEvents.first();
 
  720                     focusedIndex = table.getItems().indexOf(focusedItem);
 
  723                 ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
 
  724                 ZonedDateTime nextDateTime = focusedDateTime.plus(1, selectedUnit);
 
  725                 for (ChronoField field : SCROLL_BY_UNITS) {
 
  726                     if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
 
  727                         nextDateTime = nextDateTime.with(field, field.rangeRefinedBy(nextDateTime).getMinimum());
 
  730                 long nextMillis = nextDateTime.toInstant().toEpochMilli();
 
  732                 int nextIndex = table.getItems().size() - 1;
 
  733                 for (
int i = focusedIndex; i < table.getItems().size(); i++) {
 
  734                     if (table.getItems().get(i).getStartMillis() >= nextMillis) {
 
  739                 scrollToAndFocus(nextIndex);
 
  741             setGraphic(
new ImageView(NEXT));
 
  742             IntegerBinding size = Bindings.size(table.getItems());
 
  743             disabledProperty().bind(size.isEqualTo(0).or(
 
  744                     table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
 
  752             super(
"", actionEvent -> {
 
  754                 ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
 
  755                 TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
 
  757                 int focusedIndex = table.getFocusModel().getFocusedIndex();
 
  758                 CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
 
  759                 if (-1 == focusedIndex || null == focusedItem) {
 
  760                     focusedItem = visibleEvents.last();
 
  761                     focusedIndex = table.getItems().indexOf(focusedItem);
 
  764                 ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
 
  765                 ZonedDateTime previousDateTime = focusedDateTime.minus(1, selectedUnit);
 
  767                 for (ChronoField field : SCROLL_BY_UNITS) {
 
  768                     if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
 
  769                         previousDateTime = previousDateTime.with(field, field.rangeRefinedBy(previousDateTime).getMaximum());
 
  772                 long previousMillis = previousDateTime.toInstant().toEpochMilli();
 
  774                 int previousIndex = 0;
 
  775                 for (
int i = focusedIndex; i > 0; i--) {
 
  776                     if (table.getItems().get(i).getStartMillis() <= previousMillis) {
 
  782                 scrollToAndFocus(previousIndex);
 
  784             setGraphic(
new ImageView(PREVIOUS));
 
  785             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)