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)