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) {
572 String text = textSupplier.apply(getEvent());
574 setTooltip(
new Tooltip(text));
583 private abstract class EventTableCell extends TableCell<CombinedEvent, CombinedEvent> {
597 protected void updateItem(CombinedEvent item,
boolean empty) {
598 super.updateItem(item, empty);
600 if (empty || item == null) {
612 private class EventRow extends TableRow<CombinedEvent> {
626 "ListChart.errorMsg=There was a problem getting the content for the selected event."})
628 protected void updateItem(CombinedEvent item,
boolean empty) {
629 CombinedEvent oldItem = getItem();
630 if (oldItem != null) {
631 visibleEvents.remove(oldItem);
633 super.updateItem(item, empty);
635 if (empty || item == null) {
638 visibleEvents.add(item);
641 setOnContextMenuRequested(contextMenuEvent -> {
645 List<MenuItem> menuItems =
new ArrayList<>();
648 for (Action action : node.
getActions(
false)) {
649 if (action == null) {
651 menuItems.add(
new SeparatorMenuItem());
653 String actionName = Objects.toString(action.getValue(Action.NAME));
655 if (Arrays.asList(
"&Properties",
"Tools").contains(actionName) ==
false) {
656 if (action instanceof Presenter.Popup) {
663 JMenuItem submenu = ((Presenter.Popup) action).getPopupPresenter();
664 menuItems.add(SwingFXMenuUtils.createFXMenu(submenu));
666 menuItems.add(SwingFXMenuUtils.createFXMenu(
new Actions.MenuItem(action,
false)));
673 new ContextMenu(menuItems.toArray(
new MenuItem[menuItems.size()]))
674 .show(
this, contextMenuEvent.getScreenX(), contextMenuEvent.getScreenY());
677 LOGGER.log(Level.SEVERE,
"There was no case open to lookup the Sleuthkit object backing a SingleEvent.", ex);
678 }
catch (TskCoreException ex) {
679 LOGGER.log(Level.SEVERE,
"Failed to lookup Sleuthkit object backing a SingleEvent.", ex);
680 Platform.runLater(() -> {
681 Notifications.create()
682 .owner(getScene().getWindow())
683 .text(Bundle.ListChart_errorMsg())
696 super(
"",
new Consumer<ActionEvent>() {
698 public void accept(ActionEvent actionEvent) {
702 setGraphic(
new ImageView(FIRST));
703 disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
710 super(
"",
new Consumer<ActionEvent>() {
712 public void accept(ActionEvent actionEvent) {
713 scrollToAndFocus(table.getItems().size() - 1);
716 setGraphic(
new ImageView(LAST));
717 IntegerBinding size = Bindings.size(table.getItems());
718 disabledProperty().bind(size.isEqualTo(0).or(
719 table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
726 super(
"",
new Consumer<ActionEvent>() {
728 public void accept(ActionEvent actionEvent) {
729 ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
731 TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
733 int focusedIndex = table.getFocusModel().getFocusedIndex();
734 CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
735 if (-1 == focusedIndex || null == focusedItem) {
736 focusedItem = visibleEvents.first();
737 focusedIndex = table.getItems().indexOf(focusedItem);
740 ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
741 ZonedDateTime nextDateTime = focusedDateTime.plus(1, selectedUnit);
742 for (ChronoField field : SCROLL_BY_UNITS) {
743 if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
744 nextDateTime = nextDateTime.with(field, field.rangeRefinedBy(nextDateTime).getMinimum());
747 long nextMillis = nextDateTime.toInstant().toEpochMilli();
749 int nextIndex = table.getItems().size() - 1;
750 for (
int i = focusedIndex; i < table.getItems().size(); i++) {
751 if (table.getItems().get(i).getStartMillis() >= nextMillis) {
756 scrollToAndFocus(nextIndex);
759 setGraphic(
new ImageView(NEXT));
760 IntegerBinding size = Bindings.size(table.getItems());
761 disabledProperty().bind(size.isEqualTo(0).or(
762 table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
770 super(
"",
new Consumer<ActionEvent>() {
772 public void accept(ActionEvent actionEvent) {
774 ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
775 TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
777 int focusedIndex = table.getFocusModel().getFocusedIndex();
778 CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
779 if (-1 == focusedIndex || null == focusedItem) {
780 focusedItem = visibleEvents.last();
781 focusedIndex = table.getItems().indexOf(focusedItem);
784 ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
785 ZonedDateTime previousDateTime = focusedDateTime.minus(1, selectedUnit);
787 for (ChronoField field : SCROLL_BY_UNITS) {
788 if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
789 previousDateTime = previousDateTime.with(field, field.rangeRefinedBy(previousDateTime).getMaximum());
792 long previousMillis = previousDateTime.toInstant().toEpochMilli();
794 int previousIndex = 0;
795 for (
int i = focusedIndex; i > 0; i--) {
796 if (table.getItems().get(i).getStartMillis() <= previousMillis) {
802 scrollToAndFocus(previousIndex);
805 setGraphic(
new ImageView(PREVIOUS));
806 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)