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 @SuppressWarnings(
"PMD.SingularField")
76 final class ShowInTimelineDialog extends Dialog<ViewInTimelineRequestedEvent> {
78 private static final Logger LOGGER = Logger.getLogger(ShowInTimelineDialog.class.getName());
80 @NbBundle.Messages({
"ShowInTimelineDialog.showTimelineButtonType.text=Show Timeline"})
81 private static final ButtonType SHOW =
new ButtonType(Bundle.ShowInTimelineDialog_showTimelineButtonType_text(), ButtonBar.ButtonData.OK_DONE);
87 private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList(
89 ChronoField.MONTH_OF_YEAR,
90 ChronoField.DAY_OF_MONTH,
91 ChronoField.HOUR_OF_DAY,
92 ChronoField.MINUTE_OF_HOUR,
93 ChronoField.SECOND_OF_MINUTE);
96 private TableView<SingleEvent> eventTable;
99 private TableColumn<SingleEvent, EventType> typeColumn;
102 private TableColumn<SingleEvent, Long> dateTimeColumn;
105 private Spinner<Integer> amountSpinner;
108 private ComboBox<ChronoField> unitComboBox;
111 private Label chooseEventLabel;
113 private final VBox contentRoot =
new VBox();
115 private final TimeLineController controller;
117 private final ValidationSupport validationSupport =
new ValidationSupport();
127 "ShowInTimelineDialog.amountValidator.message=The entered amount must only contain digits."
129 private ShowInTimelineDialog(TimeLineController controller, List<Long> eventIDS) {
130 this.controller = controller;
133 final String name =
"nbres:/" + StringUtils.replace(ShowInTimelineDialog.class.getPackage().getName(),
".",
"/") +
"/ShowInTimelineDialog.fxml";
135 FXMLLoader fxmlLoader =
new FXMLLoader(
new URL(name));
136 fxmlLoader.setRoot(contentRoot);
137 fxmlLoader.setController(
this);
140 }
catch (IOException ex) {
141 LOGGER.log(Level.SEVERE,
"Unable to load FXML, node initialization may not be complete.", ex);
144 assert eventTable != null :
"fx:id=\"eventTable\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
145 assert typeColumn != null :
"fx:id=\"typeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
146 assert dateTimeColumn != null :
"fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
147 assert amountSpinner != null :
"fx:id=\"amountsSpinner\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
148 assert unitComboBox != null :
"fx:id=\"unitChoiceBox\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
151 validationSupport.registerValidator(amountSpinner.getEditor(),
false,
152 Validator.createPredicateValidator(NumberUtils::isDigits, Bundle.ShowInTimelineDialog_amountValidator_message()));
155 PromptDialogManager.setDialogIcons(
this);
156 initModality(Modality.APPLICATION_MODAL);
159 DialogPane dialogPane = getDialogPane();
160 dialogPane.setContent(contentRoot);
162 dialogPane.getButtonTypes().setAll(SHOW, ButtonType.CANCEL);
165 amountSpinner.setValueFactory(
new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 1000));
166 amountSpinner.getValueFactory().setConverter(
new IntegerStringConverter() {
177 public Integer fromString(String
string) {
179 return super.fromString(
string);
180 }
catch (NumberFormatException ex) {
181 return amountSpinner.getValue();
186 unitComboBox.setButtonCell(
new ChronoFieldListCell());
187 unitComboBox.setCellFactory(comboBox ->
new ChronoFieldListCell());
188 unitComboBox.getItems().setAll(SCROLL_BY_UNITS);
189 unitComboBox.getSelectionModel().select(ChronoField.MINUTE_OF_HOUR);
191 typeColumn.setCellValueFactory(param ->
new SimpleObjectProperty<>(param.getValue().getEventType()));
192 typeColumn.setCellFactory(param ->
new TypeTableCell<>());
194 dateTimeColumn.setCellValueFactory(param ->
new SimpleObjectProperty<>(param.getValue().getStartMillis()));
195 dateTimeColumn.setCellFactory(param ->
new DateTimeTableCell<>());
198 eventTable.getItems().setAll(eventIDS.stream().map(controller.getEventsModel()::getEventById).collect(Collectors.toSet()));
199 eventTable.setPrefHeight(Math.min(200, 24 * eventTable.getItems().size() + 28));
209 @NbBundle.Messages({
"ShowInTimelineDialog.artifactTitle=View Result in Timeline."})
210 ShowInTimelineDialog(TimeLineController controller, BlackboardArtifact artifact) {
212 this(controller, controller.getEventsModel().getEventIDsForArtifact(artifact));
215 chooseEventLabel.setVisible(
false);
216 chooseEventLabel.setManaged(
false);
217 eventTable.getSelectionModel().select(0);
220 getDialogPane().lookupButton(SHOW).disableProperty().bind(validationSupport.invalidProperty());
223 setResultConverter(buttonType -> (buttonType == SHOW)
224 ? makeEventInTimeRange(eventTable.getItems().get(0))
227 setTitle(Bundle.ShowInTimelineDialog_artifactTitle());
237 @NbBundle.Messages({
"# {0} - file path",
238 "ShowInTimelineDialog.fileTitle=View {0} in timeline.",
239 "ShowInTimelineDialog.eventSelectionValidator.message=You must select an event."})
240 ShowInTimelineDialog(TimeLineController controller, AbstractFile file) {
241 this(controller, controller.getEventsModel().getEventIDsForFile(file,
false));
248 eventTable.getSelectionModel().selectedItemProperty().isNull().addListener((selectedItemNullProperty, wasNull, isNull) -> {
250 validationSupport.getValidationDecorator().applyValidationDecoration(
251 ValidationMessage.error(eventTable, Bundle.ShowInTimelineDialog_eventSelectionValidator_message()));
253 validationSupport.getValidationDecorator().removeDecorations(eventTable);
258 getDialogPane().lookupButton(SHOW).disableProperty().bind(Bindings.or(
259 validationSupport.invalidProperty(),
260 eventTable.getSelectionModel().selectedItemProperty().isNull()
264 setResultConverter(buttonType -> (buttonType == SHOW)
265 ? makeEventInTimeRange(eventTable.getSelectionModel().getSelectedItem())
269 setTitle(Bundle.ShowInTimelineDialog_fileTitle(StringUtils.abbreviateMiddle(getContentPathSafe(file),
" ... ", 50)));
283 static String getContentPathSafe(Content content) {
285 return content.getUniquePath();
286 }
catch (TskCoreException tskCoreException) {
287 String contentName = content.getName();
288 LOGGER.log(Level.SEVERE,
"Failed to get unique path for " + contentName, tskCoreException);
300 private ViewInTimelineRequestedEvent makeEventInTimeRange(SingleEvent selectedEvent) {
301 Duration selectedDuration = unitComboBox.getSelectionModel().getSelectedItem().getBaseUnit().getDuration().multipliedBy(amountSpinner.getValue());
302 Interval range = IntervalUtils.getIntervalAround(Instant.ofEpochMilli(selectedEvent.getStartMillis()), selectedDuration);
303 return new ViewInTimelineRequestedEvent(Collections.singleton(selectedEvent.getEventID()), range);
313 super.updateItem(item, empty);
315 if (empty || item == null) {
318 setText(WordUtils.capitalizeFully(item.toString()));
333 super.updateItem(item, empty);
335 if (item == null || empty) {
352 super.updateItem(item, empty);
354 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)