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.Collections;
29 import java.util.List;
30 import java.util.logging.Level;
31 import java.util.stream.Collectors;
32 import javafx.beans.binding.Bindings;
33 import javafx.beans.property.SimpleObjectProperty;
34 import javafx.fxml.FXML;
35 import javafx.fxml.FXMLLoader;
36 import javafx.scene.control.ButtonBar;
37 import javafx.scene.control.ButtonType;
38 import javafx.scene.control.ComboBox;
39 import javafx.scene.control.Dialog;
40 import javafx.scene.control.DialogPane;
41 import javafx.scene.control.Label;
42 import javafx.scene.control.ListCell;
43 import javafx.scene.control.Spinner;
44 import javafx.scene.control.SpinnerValueFactory;
45 import javafx.scene.control.TableCell;
46 import javafx.scene.control.TableColumn;
47 import javafx.scene.control.TableView;
48 import javafx.scene.image.ImageView;
49 import javafx.scene.layout.VBox;
50 import javafx.stage.Modality;
51 import javafx.util.converter.IntegerStringConverter;
52 import org.apache.commons.lang3.StringUtils;
53 import org.apache.commons.lang3.math.NumberUtils;
54 import org.apache.commons.lang3.text.WordUtils;
55 import org.controlsfx.validation.ValidationMessage;
56 import org.controlsfx.validation.ValidationSupport;
57 import org.controlsfx.validation.Validator;
58 import org.joda.time.Interval;
59 import org.openide.util.NbBundle;
75 final class ShowInTimelineDialog
extends Dialog<ViewInTimelineRequestedEvent> {
77 private static final Logger LOGGER = Logger.getLogger(ShowInTimelineDialog.class.getName());
79 @NbBundle.Messages({
"ShowInTimelineDialog.showTimelineButtonType.text=Show Timeline"})
80 private static final ButtonType SHOW =
new ButtonType(Bundle.ShowInTimelineDialog_showTimelineButtonType_text(), ButtonBar.ButtonData.OK_DONE);
86 private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList(
88 ChronoField.MONTH_OF_YEAR,
89 ChronoField.DAY_OF_MONTH,
90 ChronoField.HOUR_OF_DAY,
91 ChronoField.MINUTE_OF_HOUR,
92 ChronoField.SECOND_OF_MINUTE);
95 private TableView<SingleEvent> eventTable;
98 private TableColumn<SingleEvent, EventType> typeColumn;
101 private TableColumn<SingleEvent, Long> dateTimeColumn;
104 private Spinner<Integer> amountSpinner;
107 private ComboBox<ChronoField> unitComboBox;
110 private Label chooseEventLabel;
112 private final VBox contentRoot =
new VBox();
114 private final TimeLineController controller;
116 private final ValidationSupport validationSupport =
new ValidationSupport();
126 "ShowInTimelineDialog.amountValidator.message=The entered amount must only contain digits."
128 private ShowInTimelineDialog(TimeLineController controller, List<Long> eventIDS) {
129 this.controller = controller;
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().getStartMillis()));
194 dateTimeColumn.setCellFactory(param ->
new DateTimeTableCell<>());
197 eventTable.getItems().setAll(eventIDS.stream().map(controller.getEventsModel()::getEventById).collect(Collectors.toSet()));
198 eventTable.setPrefHeight(Math.min(200, 24 * eventTable.getItems().size() + 28));
208 @NbBundle.Messages({
"ShowInTimelineDialog.artifactTitle=View Result in Timeline."})
209 ShowInTimelineDialog(TimeLineController controller, BlackboardArtifact artifact) {
211 this(controller, controller.getEventsModel().getEventIDsForArtifact(artifact));
214 chooseEventLabel.setVisible(
false);
215 chooseEventLabel.setManaged(
false);
216 eventTable.getSelectionModel().select(0);
219 getDialogPane().lookupButton(SHOW).disableProperty().bind(validationSupport.invalidProperty());
222 setResultConverter(buttonType -> (buttonType == SHOW)
223 ? makeEventInTimeRange(eventTable.getItems().get(0))
226 setTitle(Bundle.ShowInTimelineDialog_artifactTitle());
236 @NbBundle.Messages({
"# {0} - file path",
237 "ShowInTimelineDialog.fileTitle=View {0} in timeline.",
238 "ShowInTimelineDialog.eventSelectionValidator.message=You must select an event."})
239 ShowInTimelineDialog(TimeLineController controller, AbstractFile file) {
240 this(controller, controller.getEventsModel().getEventIDsForFile(file,
false));
247 eventTable.getSelectionModel().selectedItemProperty().isNull().addListener((selectedItemNullProperty, wasNull, isNull) -> {
249 validationSupport.getValidationDecorator().applyValidationDecoration(
250 ValidationMessage.error(eventTable, Bundle.ShowInTimelineDialog_eventSelectionValidator_message()));
252 validationSupport.getValidationDecorator().removeDecorations(eventTable);
257 getDialogPane().lookupButton(SHOW).disableProperty().bind(Bindings.or(
258 validationSupport.invalidProperty(),
259 eventTable.getSelectionModel().selectedItemProperty().isNull()
263 setResultConverter(buttonType -> (buttonType == SHOW)
264 ? makeEventInTimeRange(eventTable.getSelectionModel().getSelectedItem())
268 setTitle(Bundle.ShowInTimelineDialog_fileTitle(StringUtils.abbreviateMiddle(getContentPathSafe(file),
" ... ", 50)));
282 static String getContentPathSafe(Content content) {
284 return content.getUniquePath();
285 }
catch (TskCoreException tskCoreException) {
286 String contentName = content.getName();
287 LOGGER.log(Level.SEVERE,
"Failed to get unique path for " + contentName, tskCoreException);
299 private ViewInTimelineRequestedEvent makeEventInTimeRange(SingleEvent selectedEvent) {
300 Duration selectedDuration = unitComboBox.getSelectionModel().getSelectedItem().getBaseUnit().getDuration().multipliedBy(amountSpinner.getValue());
301 Interval range = IntervalUtils.getIntervalAround(Instant.ofEpochMilli(selectedEvent.getStartMillis()), selectedDuration);
302 return new ViewInTimelineRequestedEvent(Collections.singleton(selectedEvent.getEventID()), range);
312 super.updateItem(item, empty);
314 if (empty || item == null) {
317 setText(WordUtils.capitalizeFully(item.toString()));
332 super.updateItem(item, empty);
334 if (item == null || empty) {
351 super.updateItem(item, empty);
353 if (item == null || empty) {
void updateItem(Long item, boolean empty)
void updateItem(ChronoUnit item, boolean empty)
static DateTimeFormatter getZonedFormatter()
void updateItem(EventType item, boolean empty)