19 package org.sleuthkit.autopsy.timeline;
21 import java.io.IOException;
23 import java.time.Duration;
24 import java.time.Instant;
25 import java.time.temporal.ChronoField;
26 import java.time.temporal.ChronoUnit;
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.HashSet;
31 import java.util.List;
33 import java.util.logging.Level;
34 import javafx.beans.binding.Bindings;
35 import javafx.beans.property.SimpleObjectProperty;
36 import javafx.fxml.FXML;
37 import javafx.fxml.FXMLLoader;
38 import javafx.scene.control.ButtonBar;
39 import javafx.scene.control.ButtonType;
40 import javafx.scene.control.ComboBox;
41 import javafx.scene.control.Dialog;
42 import javafx.scene.control.DialogPane;
43 import javafx.scene.control.Label;
44 import javafx.scene.control.ListCell;
45 import javafx.scene.control.Spinner;
46 import javafx.scene.control.SpinnerValueFactory;
47 import javafx.scene.control.TableCell;
48 import javafx.scene.control.TableColumn;
49 import javafx.scene.control.TableView;
50 import javafx.scene.image.ImageView;
51 import javafx.scene.layout.VBox;
52 import javafx.stage.Modality;
53 import javafx.util.converter.IntegerStringConverter;
54 import org.apache.commons.lang3.StringUtils;
55 import org.apache.commons.lang3.math.NumberUtils;
56 import org.apache.commons.lang3.text.WordUtils;
57 import org.controlsfx.validation.ValidationMessage;
58 import org.controlsfx.validation.ValidationSupport;
59 import org.controlsfx.validation.Validator;
60 import org.joda.time.Interval;
61 import org.openide.util.NbBundle;
78 @SuppressWarnings(
"PMD.SingularField")
79 final class ShowInTimelineDialog extends Dialog<ViewInTimelineRequestedEvent> {
81 private static final Logger LOGGER = Logger.getLogger(ShowInTimelineDialog.class.getName());
83 @NbBundle.Messages({
"ShowInTimelineDialog.showTimelineButtonType.text=Show Timeline"})
84 private static final ButtonType SHOW =
new ButtonType(Bundle.ShowInTimelineDialog_showTimelineButtonType_text(), ButtonBar.ButtonData.OK_DONE);
90 private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList(
92 ChronoField.MONTH_OF_YEAR,
93 ChronoField.DAY_OF_MONTH,
94 ChronoField.HOUR_OF_DAY,
95 ChronoField.MINUTE_OF_HOUR,
96 ChronoField.SECOND_OF_MINUTE);
99 private TableView<TimelineEvent> eventTable;
102 private TableColumn<TimelineEvent, TimelineEventType> typeColumn;
105 private TableColumn<TimelineEvent, Long> dateTimeColumn;
108 private Spinner<Integer> amountSpinner;
111 private ComboBox<ChronoField> unitComboBox;
114 private Label chooseEventLabel;
116 private final VBox contentRoot =
new VBox();
118 private final ValidationSupport validationSupport =
new ValidationSupport();
128 "ShowInTimelineDialog.amountValidator.message=The entered amount must only contain digits."})
129 private ShowInTimelineDialog(TimeLineController controller, Collection<Long> eventIDS)
throws TskCoreException {
132 final String name =
"nbres:/" + StringUtils.replace(ShowInTimelineDialog.class.getPackage().getName(),
".",
"/") +
"/ShowInTimelineDialog.fxml";
134 FXMLLoader fxmlLoader =
new FXMLLoader(
new URL(name));
135 fxmlLoader.setRoot(contentRoot);
136 fxmlLoader.setController(
this);
139 }
catch (IOException ex) {
140 LOGGER.log(Level.SEVERE,
"Unable to load FXML, node initialization may not be complete.", ex);
143 assert eventTable != null :
"fx:id=\"eventTable\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
144 assert typeColumn != null :
"fx:id=\"typeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
145 assert dateTimeColumn != null :
"fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
146 assert amountSpinner != null :
"fx:id=\"amountsSpinner\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
147 assert unitComboBox != null :
"fx:id=\"unitChoiceBox\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
150 validationSupport.registerValidator(amountSpinner.getEditor(),
false,
151 Validator.createPredicateValidator(NumberUtils::isDigits, Bundle.ShowInTimelineDialog_amountValidator_message()));
154 PromptDialogManager.setDialogIcons(
this);
155 initModality(Modality.APPLICATION_MODAL);
158 DialogPane dialogPane = getDialogPane();
159 dialogPane.setContent(contentRoot);
161 dialogPane.getButtonTypes().setAll(SHOW, ButtonType.CANCEL);
164 amountSpinner.setValueFactory(
new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 1000));
165 amountSpinner.getValueFactory().setConverter(
new IntegerStringConverter() {
176 public Integer fromString(String
string) {
178 return super.fromString(
string);
179 }
catch (NumberFormatException ex) {
180 return amountSpinner.getValue();
185 unitComboBox.setButtonCell(
new ChronoFieldListCell());
186 unitComboBox.setCellFactory(comboBox ->
new ChronoFieldListCell());
187 unitComboBox.getItems().setAll(SCROLL_BY_UNITS);
188 unitComboBox.getSelectionModel().select(ChronoField.MINUTE_OF_HOUR);
190 typeColumn.setCellValueFactory(param ->
new SimpleObjectProperty<>(param.getValue().getEventType()));
191 typeColumn.setCellFactory(param ->
new TypeTableCell<>());
193 dateTimeColumn.setCellValueFactory(param ->
new SimpleObjectProperty<>(param.getValue().getEventTimeInMs()));
194 dateTimeColumn.setCellFactory(param ->
new DateTimeTableCell<>());
197 Set<TimelineEvent> events =
new HashSet<>();
198 EventsModel eventsModel = controller.getEventsModel();
199 for (Long eventID : eventIDS) {
201 events.add(eventsModel.getEventById(eventID));
202 }
catch (TskCoreException ex) {
203 throw new TskCoreException(
"Error getting event by id.", ex);
206 eventTable.getItems().setAll(events);
207 eventTable.setPrefHeight(Math.min(200, 24 * eventTable.getItems().size() + 28));
217 @NbBundle.Messages({
"ShowInTimelineDialog.artifactTitle=View Result in Timeline."})
218 ShowInTimelineDialog(TimeLineController controller, BlackboardArtifact artifact)
throws TskCoreException {
220 this(controller, controller.getEventsModel().getEventIDsForArtifact(artifact));
223 chooseEventLabel.setVisible(
false);
224 chooseEventLabel.setManaged(
false);
225 eventTable.getSelectionModel().select(0);
228 getDialogPane().lookupButton(SHOW).disableProperty().bind(validationSupport.invalidProperty());
231 setResultConverter(buttonType -> (buttonType == SHOW)
232 ? makeEventInTimeRange(eventTable.getItems().get(0))
235 setTitle(Bundle.ShowInTimelineDialog_artifactTitle());
245 @NbBundle.Messages({
"# {0} - file path",
246 "ShowInTimelineDialog.fileTitle=View {0} in timeline.",
247 "ShowInTimelineDialog.eventSelectionValidator.message=You must select an event."})
248 ShowInTimelineDialog(TimeLineController controller, AbstractFile file)
throws TskCoreException {
249 this(controller, controller.getEventsModel().getEventIDsForFile(file,
false));
256 eventTable.getSelectionModel().selectedItemProperty().isNull().addListener((selectedItemNullProperty, wasNull, isNull) -> {
258 validationSupport.getValidationDecorator().applyValidationDecoration(
259 ValidationMessage.error(eventTable, Bundle.ShowInTimelineDialog_eventSelectionValidator_message()));
261 validationSupport.getValidationDecorator().removeDecorations(eventTable);
266 getDialogPane().lookupButton(SHOW).disableProperty().bind(Bindings.or(
267 validationSupport.invalidProperty(),
268 eventTable.getSelectionModel().selectedItemProperty().isNull()
272 setResultConverter(buttonType -> (buttonType == SHOW)
273 ? makeEventInTimeRange(eventTable.getSelectionModel().getSelectedItem())
277 setTitle(Bundle.ShowInTimelineDialog_fileTitle(StringUtils.abbreviateMiddle(getContentPathSafe(file),
" ... ", 50)));
291 static String getContentPathSafe(Content content) {
293 return content.getUniquePath();
294 }
catch (TskCoreException tskCoreException) {
295 String contentName = content.getName();
296 LOGGER.log(Level.SEVERE,
"Failed to get unique path for " + contentName, tskCoreException);
308 private ViewInTimelineRequestedEvent makeEventInTimeRange(TimelineEvent selectedEvent) {
309 Duration selectedDuration = unitComboBox.getSelectionModel().getSelectedItem().getBaseUnit().getDuration().multipliedBy(amountSpinner.getValue());
310 Interval range = IntervalUtils.getIntervalAround(Instant.ofEpochMilli(selectedEvent.getEventTimeInMs()), selectedDuration);
311 return new ViewInTimelineRequestedEvent(Collections.singleton(selectedEvent.getEventID()), range);
321 super.updateItem(item, empty);
323 if (empty || item == null) {
326 setText(WordUtils.capitalizeFully(item.toString()));
341 super.updateItem(item, empty);
343 if (item == null || empty) {
356 static private class TypeTableCell<X>
extends TableCell<X, TimelineEventType> {
359 protected void updateItem(TimelineEventType item,
boolean empty) {
360 super.updateItem(item, empty);
362 if (item == null || empty) {
366 setText(item.getDisplayName());
void updateItem(Long item, boolean empty)
void updateItem(ChronoUnit item, boolean empty)
void updateItem(TimelineEventType item, boolean empty)
static String getImagePath(TimelineEventType type)
static DateTimeFormatter getZonedFormatter()