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;
103 class ListTimeline
extends BorderPane {
107 private static final Image HASH_HIT =
new Image(
"/org/sleuthkit/autopsy/images/hashset_hits.png");
108 private static final Image TAG =
new Image(
"/org/sleuthkit/autopsy/images/green-tag-icon-16.png");
109 private static final Image FIRST =
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_first.png");
110 private static final Image PREVIOUS =
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_previous.png");
111 private static final Image NEXT =
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_next.png");
112 private static final Image LAST =
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_last.png");
117 private static final Callback<TableColumn.CellDataFeatures<
CombinedEvent,
CombinedEvent>, ObservableValue<CombinedEvent>> CELL_VALUE_FACTORY = param ->
new SimpleObjectProperty<>(param.getValue());
119 private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList(
121 ChronoField.MONTH_OF_YEAR,
122 ChronoField.DAY_OF_MONTH,
123 ChronoField.HOUR_OF_DAY,
124 ChronoField.MINUTE_OF_HOUR,
125 ChronoField.SECOND_OF_MINUTE);
127 private static final int DEFAULT_ROW_HEIGHT = 24;
130 private HBox navControls;
133 private ComboBox<ChronoField> scrollInrementComboBox;
136 private Button firstButton;
139 private Button previousButton;
142 private Button nextButton;
145 private Button lastButton;
148 private Label eventCountLabel;
150 private TableView<CombinedEvent> table;
152 private TableColumn<CombinedEvent, CombinedEvent> idColumn;
154 private TableColumn<CombinedEvent, CombinedEvent> dateTimeColumn;
156 private TableColumn<CombinedEvent, CombinedEvent> descriptionColumn;
158 private TableColumn<CombinedEvent, CombinedEvent> typeColumn;
160 private TableColumn<CombinedEvent, CombinedEvent> knownColumn;
162 private TableColumn<CombinedEvent, CombinedEvent> taggedColumn;
164 private TableColumn<CombinedEvent, CombinedEvent> hashHitColumn;
170 private final SortedSet<CombinedEvent> visibleEvents;
173 private final SleuthkitCase sleuthkitCase;
181 private final ListChangeListener<CombinedEvent> selectedEventListener =
new ListChangeListener<CombinedEvent>() {
183 public void onChanged(ListChangeListener.Change<? extends CombinedEvent> c) {
184 controller.
selectEventIDs(table.getSelectionModel().getSelectedItems().stream()
185 .filter(Objects::nonNull)
187 .collect(Collectors.toSet()));
197 this.controller = controller;
201 this.visibleEvents =
new ConcurrentSkipListSet<>(Comparator.comparing(table.getItems()::indexOf));
206 "# {0} - the number of events",
207 "ListTimeline.eventCountLabel.text={0} events"})
209 assert eventCountLabel != null :
"fx:id=\"eventCountLabel\" was not injected: check your FXML file 'ListViewPane.fxml'.";
210 assert table != null :
"fx:id=\"table\" was not injected: check your FXML file 'ListViewPane.fxml'.";
211 assert idColumn != null :
"fx:id=\"idColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
212 assert dateTimeColumn != null :
"fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
213 assert descriptionColumn != null :
"fx:id=\"descriptionColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
214 assert typeColumn != null :
"fx:id=\"typeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
215 assert knownColumn != null :
"fx:id=\"knownColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
220 scrollInrementComboBox.getItems().setAll(SCROLL_BY_UNITS);
221 scrollInrementComboBox.getSelectionModel().select(ChronoField.YEAR);
222 ActionUtils.configureButton(
new ScrollToFirst(), firstButton);
223 ActionUtils.configureButton(
new ScrollToPrevious(), previousButton);
224 ActionUtils.configureButton(
new ScrollToNext(), nextButton);
225 ActionUtils.configureButton(
new ScrollToLast(), lastButton);
228 table.setRowFactory(tableView ->
new EventRow());
231 table.getColumns().remove(idColumn);
234 dateTimeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
235 dateTimeColumn.setCellFactory(col ->
new TextEventTableCell(singleEvent ->
238 descriptionColumn.setCellValueFactory(CELL_VALUE_FACTORY);
239 descriptionColumn.setCellFactory(col ->
new TextEventTableCell(singleEvent ->
242 typeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
243 typeColumn.setCellFactory(col ->
new EventTypeCell());
245 knownColumn.setCellValueFactory(CELL_VALUE_FACTORY);
246 knownColumn.setCellFactory(col ->
new TextEventTableCell(singleEvent ->
247 singleEvent.getKnown().getName()));
249 taggedColumn.setCellValueFactory(CELL_VALUE_FACTORY);
250 taggedColumn.setCellFactory(col ->
new TaggedCell());
252 hashHitColumn.setCellValueFactory(CELL_VALUE_FACTORY);
253 hashHitColumn.setCellFactory(col ->
new HashHitCell());
256 eventCountLabel.textProperty().bind(
new StringBinding() {
258 bind(table.getItems());
262 protected String computeValue() {
263 return Bundle.ListTimeline_eventCountLabel_text(table.getItems().size());
268 table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
269 table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
279 void setCombinedEvents(Collection<CombinedEvent> events) {
280 table.getItems().setAll(events);
288 void selectEvents(Collection<Long> selectedEventIDs) {
289 if (selectedEventIDs.isEmpty()) {
291 table.getSelectionModel().clearSelection();
303 table.getSelectionModel().getSelectedItems().removeListener(selectedEventListener);
305 table.getSelectionModel().clearSelection();
307 table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
310 int[] selectedIndices = table.getItems().stream()
311 .filter(combinedEvent -> Collections.disjoint(combinedEvent.getEventIDs(), selectedEventIDs) ==
false)
312 .mapToInt(table.getItems()::indexOf)
316 if (selectedIndices.length > 0) {
317 Integer firstSelectedIndex = selectedIndices[0];
318 table.getSelectionModel().selectIndices(firstSelectedIndex, selectedIndices);
319 scrollTo(firstSelectedIndex);
320 table.requestFocus();
332 List<Node> getTimeNavigationControls() {
333 return Collections.singletonList(navControls);
343 private void scrollToAndFocus(Integer index) {
344 table.requestFocus();
346 table.getFocusModel().focus(index);
354 private void scrollTo(Integer index) {
355 if (visibleEvents.contains(table.getItems().get(index)) ==
false) {
356 table.scrollTo(DoubleMath.roundToInt(index - ((table.getHeight() / DEFAULT_ROW_HEIGHT)) / 2, RoundingMode.HALF_EVEN));
366 "ListView.EventTypeCell.modifiedTooltip=File Modified ( M )",
367 "ListView.EventTypeCell.accessedTooltip=File Accessed ( A )",
368 "ListView.EventTypeCell.createdTooltip=File Created ( B, for Born )",
369 "ListView.EventTypeCell.changedTooltip=File Changed ( C )"
372 protected void updateItem(CombinedEvent item,
boolean empty) {
373 super.updateItem(item, empty);
375 if (empty || item == null) {
381 String typeString =
"";
382 VBox toolTipVbox =
new VBox(5);
384 for (FileSystemTypes type : Arrays.asList(FileSystemTypes.FILE_MODIFIED, FileSystemTypes.FILE_ACCESSED, FileSystemTypes.FILE_CHANGED, FileSystemTypes.FILE_CREATED)) {
389 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_modifiedTooltip(),
new ImageView(type.getFXImage())));
393 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_accessedTooltip(),
new ImageView(type.getFXImage())));
397 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_createdTooltip(),
new ImageView(type.getFXImage())));
401 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_changedTooltip(),
new ImageView(type.getFXImage())));
404 throw new UnsupportedOperationException(
"Unknown FileSystemType: " + type.name());
412 Tooltip tooltip =
new Tooltip();
413 tooltip.setGraphic(toolTipVbox);
419 setGraphic(
new ImageView(eventType.
getFXImage()));
435 setAlignment(Pos.CENTER);
439 "ListTimeline.taggedTooltip.error=There was a problem getting the tag names for the selected event.",
441 "ListTimeline.taggedTooltip.text=Tags:\n{0}"})
443 protected void updateItem(CombinedEvent item,
boolean empty) {
444 super.updateItem(item, empty);
446 if (empty || item == null || (getEvent().isTagged() ==
false)) {
454 setGraphic(
new ImageView(TAG));
456 SortedSet<String> tagNames =
new TreeSet<>();
459 AbstractFile abstractFileById = sleuthkitCase.getAbstractFileById(getEvent().getFileID());
461 .map(tag -> tag.getName().getDisplayName())
462 .forEach(tagNames::add);
464 }
catch (TskCoreException ex) {
465 LOGGER.log(Level.SEVERE,
"Failed to lookup tags for obj id " + getEvent().getFileID(), ex);
466 Platform.runLater(() -> {
467 Notifications.create()
468 .owner(getScene().getWindow())
469 .text(Bundle.ListTimeline_taggedTooltip_error())
476 BlackboardArtifact artifact = sleuthkitCase.getBlackboardArtifact(artifactID);
478 .map(tag -> tag.getName().getDisplayName())
479 .forEach(tagNames::add);
480 }
catch (TskCoreException ex) {
481 LOGGER.log(Level.SEVERE,
"Failed to lookup tags for artifact id " + artifactID, ex);
482 Platform.runLater(() -> {
483 Notifications.create()
484 .owner(getScene().getWindow())
485 .text(Bundle.ListTimeline_taggedTooltip_error())
490 Tooltip tooltip =
new Tooltip(Bundle.ListTimeline_taggedTooltip_text(String.join(
"\n", tagNames)));
491 tooltip.setGraphic(
new ImageView(TAG));
507 setAlignment(Pos.CENTER);
511 "ListTimeline.hashHitTooltip.error=There was a problem getting the hash set names for the selected event.",
512 "# {0} - hash set names",
513 "ListTimeline.hashHitTooltip.text=Hash Sets:\n{0}"})
515 protected void updateItem(CombinedEvent item,
boolean empty) {
516 super.updateItem(item, empty);
518 if (empty || item == null || (getEvent().isHashHit() ==
false)) {
527 setGraphic(
new ImageView(HASH_HIT));
529 Set<String> hashSetNames =
new TreeSet<>(sleuthkitCase.getAbstractFileById(getEvent().getFileID()).getHashSetNames());
530 Tooltip tooltip =
new Tooltip(Bundle.ListTimeline_hashHitTooltip_text(String.join(
"\n", hashSetNames)));
531 tooltip.setGraphic(
new ImageView(HASH_HIT));
533 }
catch (TskCoreException ex) {
534 LOGGER.log(Level.SEVERE,
"Failed to lookup hash set names for obj id " + getEvent().getFileID(), ex);
535 Platform.runLater(() -> {
536 Notifications.create()
537 .owner(getScene().getWindow())
538 .text(Bundle.ListTimeline_hashHitTooltip_error())
561 setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
562 setEllipsisString(
" ... ");
566 protected void updateItem(CombinedEvent item,
boolean empty) {
567 super.updateItem(item, empty);
568 if (empty || item == null) {
571 setText(textSupplier.apply(getEvent()));
580 private abstract class EventTableCell extends TableCell<CombinedEvent, CombinedEvent> {
594 protected void updateItem(CombinedEvent item,
boolean empty) {
595 super.updateItem(item, empty);
597 if (empty || item == null) {
609 private class EventRow extends TableRow<CombinedEvent> {
623 "ListChart.errorMsg=There was a problem getting the content for the selected event."})
625 protected void updateItem(CombinedEvent item,
boolean empty) {
626 CombinedEvent oldItem = getItem();
627 if (oldItem != null) {
628 visibleEvents.remove(oldItem);
630 super.updateItem(item, empty);
632 if (empty || item == null) {
635 visibleEvents.add(item);
638 setOnContextMenuRequested(contextMenuEvent -> {
642 List<MenuItem> menuItems =
new ArrayList<>();
645 for (Action action : node.
getActions(
false)) {
646 if (action == null) {
648 menuItems.add(
new SeparatorMenuItem());
650 String actionName = Objects.toString(action.getValue(Action.NAME));
652 if (Arrays.asList(
"&Properties",
"Tools").contains(actionName) ==
false) {
653 if (action instanceof Presenter.Popup) {
660 JMenuItem submenu = ((Presenter.Popup) action).getPopupPresenter();
670 new ContextMenu(menuItems.toArray(
new MenuItem[menuItems.size()]))
671 .show(
this, contextMenuEvent.getScreenX(), contextMenuEvent.getScreenY());
674 LOGGER.log(Level.SEVERE,
"There was no case open to lookup the Sleuthkit object backing a SingleEvent.", ex);
675 }
catch (TskCoreException ex) {
676 LOGGER.log(Level.SEVERE,
"Failed to lookup Sleuthkit object backing a SingleEvent.", ex);
677 Platform.runLater(() -> {
678 Notifications.create()
679 .owner(getScene().getWindow())
680 .text(Bundle.ListChart_errorMsg())
693 super(
"",
new Consumer<ActionEvent>() {
695 public void accept(ActionEvent actionEvent) {
699 setGraphic(
new ImageView(FIRST));
700 disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
707 super(
"",
new Consumer<ActionEvent>() {
709 public void accept(ActionEvent actionEvent) {
710 scrollToAndFocus(table.getItems().size() - 1);
713 setGraphic(
new ImageView(LAST));
714 IntegerBinding size = Bindings.size(table.getItems());
715 disabledProperty().bind(size.isEqualTo(0).or(
716 table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
723 super(
"",
new Consumer<ActionEvent>() {
725 public void accept(ActionEvent actionEvent) {
726 ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
728 TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
730 int focusedIndex = table.getFocusModel().getFocusedIndex();
731 CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
732 if (-1 == focusedIndex || null == focusedItem) {
733 focusedItem = visibleEvents.first();
734 focusedIndex = table.getItems().indexOf(focusedItem);
737 ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
738 ZonedDateTime nextDateTime = focusedDateTime.plus(1, selectedUnit);
739 for (ChronoField field : SCROLL_BY_UNITS) {
740 if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
741 nextDateTime = nextDateTime.with(field, field.rangeRefinedBy(nextDateTime).getMinimum());
744 long nextMillis = nextDateTime.toInstant().toEpochMilli();
746 int nextIndex = table.getItems().size() - 1;
747 for (
int i = focusedIndex; i < table.getItems().size(); i++) {
748 if (table.getItems().get(i).getStartMillis() >= nextMillis) {
753 scrollToAndFocus(nextIndex);
756 setGraphic(
new ImageView(NEXT));
757 IntegerBinding size = Bindings.size(table.getItems());
758 disabledProperty().bind(size.isEqualTo(0).or(
759 table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
767 super(
"",
new Consumer<ActionEvent>() {
769 public void accept(ActionEvent actionEvent) {
771 ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
772 TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
774 int focusedIndex = table.getFocusModel().getFocusedIndex();
775 CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
776 if (-1 == focusedIndex || null == focusedItem) {
777 focusedItem = visibleEvents.last();
778 focusedIndex = table.getItems().indexOf(focusedItem);
781 ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
782 ZonedDateTime previousDateTime = focusedDateTime.minus(1, selectedUnit);
784 for (ChronoField field : SCROLL_BY_UNITS) {
785 if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
786 previousDateTime = previousDateTime.with(field, field.rangeRefinedBy(previousDateTime).getMaximum());
789 long previousMillis = previousDateTime.toInstant().toEpochMilli();
791 int previousIndex = 0;
792 for (
int i = focusedIndex; i > 0; i--) {
793 if (table.getItems().get(i).getStartMillis() <= previousMillis) {
799 scrollToAndFocus(previousIndex);
802 setGraphic(
new ImageView(PREVIOUS));
803 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)