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;
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<>();
460 .map(tag -> tag.getName().getDisplayName())
461 .forEach(tagNames::add);
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())
477 .map(tag -> tag.getName().getDisplayName())
478 .forEach(tagNames::add);
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));
529 Tooltip tooltip =
new Tooltip(Bundle.ListTimeline_hashHitTooltip_text(String.join(
"\n", hashSetNames)));
530 tooltip.setGraphic(
new ImageView(HASH_HIT));
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);
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)
BlackboardArtifact getBlackboardArtifact(long artifactID)
AbstractFile getAbstractFileById(long id)
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)
Set< String > getHashSetNames()
void updateItem(CombinedEvent item, boolean empty)
SingleEvent getEventById(Long eventID)
static DateTimeFormatter getZonedFormatter()
static void construct(Node node, String fxmlFileName)