19 package org.sleuthkit.autopsy.timeline;
21 import java.awt.HeadlessException;
22 import java.beans.PropertyChangeEvent;
23 import java.beans.PropertyChangeListener;
24 import java.io.IOException;
25 import java.time.ZoneId;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.MissingResourceException;
29 import java.util.TimeZone;
30 import java.util.concurrent.ExecutionException;
31 import java.util.concurrent.ExecutorService;
32 import java.util.concurrent.Executors;
33 import java.util.function.Consumer;
34 import java.util.function.Function;
35 import java.util.logging.Level;
36 import javafx.application.Platform;
37 import javafx.beans.Observable;
38 import javafx.beans.property.ReadOnlyBooleanProperty;
39 import javafx.beans.property.ReadOnlyBooleanWrapper;
40 import javafx.beans.property.ReadOnlyDoubleProperty;
41 import javafx.beans.property.ReadOnlyDoubleWrapper;
42 import javafx.beans.property.ReadOnlyListProperty;
43 import javafx.beans.property.ReadOnlyListWrapper;
44 import javafx.beans.property.ReadOnlyObjectProperty;
45 import javafx.beans.property.ReadOnlyObjectWrapper;
46 import javafx.beans.property.ReadOnlyStringProperty;
47 import javafx.beans.property.ReadOnlyStringWrapper;
48 import javafx.collections.FXCollections;
49 import javafx.collections.ObservableList;
50 import javafx.concurrent.Task;
51 import javafx.concurrent.Worker;
52 import javax.annotation.concurrent.GuardedBy;
53 import javax.annotation.concurrent.Immutable;
54 import javax.swing.SwingUtilities;
55 import org.joda.time.DateTime;
56 import org.joda.time.DateTimeZone;
57 import org.joda.time.Interval;
58 import org.joda.time.ReadablePeriod;
59 import org.joda.time.format.DateTimeFormat;
60 import org.joda.time.format.DateTimeFormatter;
61 import org.openide.util.NbBundle;
102 @NbBundle.Messages({
"Timeline.confirmation.dialogs.title=Update Timeline database?",
103 "TimeLinecontroller.updateNowQuestion=Do you want to update the events database now?"})
108 private static final ReadOnlyObjectWrapper<TimeZone> timeZone =
new ReadOnlyObjectWrapper<>(TimeZone.getDefault());
111 return timeZone.get().toZoneId();
115 return DateTimeFormat.forPattern(
"YYYY-MM-dd HH:mm:ss").withZone(getJodaTimeZone());
119 return DateTimeZone.forTimeZone(getTimeZone().
get());
123 return timeZone.getReadOnlyProperty();
126 private final ExecutorService executor = Executors.newSingleThreadExecutor();
128 private final ReadOnlyListWrapper<Task<?>> tasks =
new ReadOnlyListWrapper<>(FXCollections.observableArrayList());
130 private final ReadOnlyDoubleWrapper taskProgress =
new ReadOnlyDoubleWrapper(-1);
132 private final ReadOnlyStringWrapper taskMessage =
new ReadOnlyStringWrapper();
134 private final ReadOnlyStringWrapper taskTitle =
new ReadOnlyStringWrapper();
136 private final ReadOnlyStringWrapper status =
new ReadOnlyStringWrapper();
145 return status.getReadOnlyProperty();
155 private final ObservableList<
DescriptionFilter> quickHideFilters = FXCollections.observableArrayList();
158 return quickHideFilters;
168 synchronized public ReadOnlyListProperty<Task<?>>
getTasks() {
169 return tasks.getReadOnlyProperty();
173 return taskProgress.getReadOnlyProperty();
177 return taskMessage.getReadOnlyProperty();
181 return taskTitle.getReadOnlyProperty();
189 private
boolean listeningToAutopsy = false;
199 return viewMode.getReadOnlyProperty();
202 @GuardedBy(
"filteredEvents")
214 private final ReadOnlyObjectWrapper<
ZoomParams> currentParams = new ReadOnlyObjectWrapper<>();
218 private final ObservableList<Long> selectedEventIDs = FXCollections.<Long>synchronizedObservableList(FXCollections.<Long>observableArrayList());
223 synchronized public ObservableList<Long> getSelectedEventIDs() {
224 return selectedEventIDs;
228 private final ReadOnlyObjectWrapper<Interval> selectedTimeRange = new ReadOnlyObjectWrapper<>();
233 synchronized public ReadOnlyObjectProperty<Interval> getSelectedTimeRange() {
234 return selectedTimeRange.getReadOnlyProperty();
238 return eventsDBStale.getReadOnlyProperty();
242 return historyManager.getCanAdvance();
246 return historyManager.getCanRetreat();
248 private final ReadOnlyBooleanWrapper eventsDBStale =
new ReadOnlyBooleanWrapper(
true);
253 this.autoCase = autoCase;
254 this.perCaseTimelineProperties =
new PerCaseTimelineProperties(autoCase);
255 eventsDBStale.set(perCaseTimelineProperties.isDBStale());
256 eventsRepository =
new EventsRepository(autoCase, currentParams.getReadOnlyProperty());
263 historyManager.currentState().addListener((Observable observable) -> {
264 ZoomParams historyManagerParams = historyManager.getCurrentState();
266 currentParams.set(historyManagerParams);
270 InitialZoomState =
new ZoomParams(filteredEvents.getSpanningInterval(),
272 filteredEvents.filterProperty().get(),
274 historyManager.advance(InitialZoomState);
281 return filteredEvents;
285 pushFilters(filteredEvents.getDefaultFilter());
289 Interval boundingEventsInterval = filteredEvents.getBoundingEventsInterval();
290 advance(filteredEvents.zoomParametersProperty().get().withTimeRange(boundingEventsInterval));
303 SwingUtilities.invokeLater(this::closeTimelineWindow);
306 setIngestRunning(ingestRunning);
310 setEventsDBStale(
false);
312 historyManager.reset(filteredEvents.zoomParametersProperty().get());
317 setEventsDBStale(
true);
321 promptDialogManager.showProgressDialog(rebuildRepository);
329 rebuildRepoHelper(eventsRepository::rebuildRepository);
337 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
338 void rebuildTagsTable() {
339 rebuildRepoHelper(eventsRepository::rebuildTags);
342 @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
343 private
void closeTimelineWindow() {
344 if (isWindowOpen()) {
350 synchronized (filteredEvents) {
351 pushTimeRange(filteredEvents.getSpanningInterval());
356 public
void shutDownTimeLine() {
357 if (mainFrame != null) {
358 listeningToAutopsy =
false;
371 void openTimeLine() {
377 listeningToAutopsy =
true;
380 Platform.runLater(() -> {
382 if (promptDialogManager.bringCurrentDialogToFront()) {
385 if (IngestManager.getInstance().isIngestRunning()) {
387 if (promptDialogManager.confirmDuringIngest() ==
false) {
398 if (checkAndPromptForRebuild() ==
false) {
402 }
catch (HeadlessException | MissingResourceException ex) {
403 LOGGER.log(Level.SEVERE,
"Unexpected error when generating timeline, ", ex);
408 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
409 private
boolean checkAndPromptForRebuild() {
411 if (eventsRepository.countAllEvents() == 0) {
416 ArrayList<String> rebuildReasons = getRebuildReasons();
417 if (rebuildReasons.isEmpty() ==
false) {
418 if (promptDialogManager.confirmRebuild(rebuildReasons)) {
427 @NbBundle.Messages({
"TimeLineController.errorTitle=Timeline error.",
428 "TimeLineController.outOfDate.errorMessage=Error determing if the timeline is out of date. We will assume it should be updated. See the logs for more details.",
429 "TimeLineController.rebuildReasons.outOfDateError=Could not determine if the timeline data is out of date.",
430 "TimeLineController.rebuildReasons.outOfDate=The event data is out of date: Not all events will be visible.",
431 "TimeLineController.rebuildReasons.ingestWasRunning=The Timeline events database was previously populated while ingest was running: Some events may be missing, incomplete, or inaccurate.",
432 "TimeLineController.rebuildReasons.incompleteOldSchema=The Timeline events database was previously populated without incomplete information: Some features may be unavailable or non-functional unless you update the events database."})
434 ArrayList<String> rebuildReasons =
new ArrayList<>();
438 if (perCaseTimelineProperties.wasIngestRunning()) {
439 rebuildReasons.add(Bundle.TimeLineController_rebuildReasons_ingestWasRunning());
442 }
catch (IOException ex) {
443 LOGGER.log(Level.SEVERE,
"Error determing the state of the timeline db. We will assume the it is out of date.", ex);
445 Bundle.TimeLineController_outOfDate_errorMessage());
446 rebuildReasons.add(Bundle.TimeLineController_rebuildReasons_outOfDateError());
449 if (isEventsDBStale()) {
450 rebuildReasons.add(Bundle.TimeLineController_rebuildReasons_outOfDate());
453 if (eventsRepository.hasNewColumns() ==
false) {
454 rebuildReasons.add(Bundle.TimeLineController_rebuildReasons_incompleteOldSchema());
456 return rebuildReasons;
466 synchronized (filteredEvents) {
473 final Interval timeRange = filteredEvents.timeRangeProperty().get();
474 long toDurationMillis = timeRange.toDurationMillis() / 4;
475 DateTime start = timeRange.getStart().minus(toDurationMillis);
476 DateTime end = timeRange.getEnd().plus(toDurationMillis);
477 pushTimeRange(
new Interval(start, end));
481 final Interval timeRange = filteredEvents.timeRangeProperty().get();
482 long toDurationMillis = timeRange.toDurationMillis() / 4;
483 DateTime start = timeRange.getStart().plus(toDurationMillis);
484 DateTime end = timeRange.getEnd().minus(toDurationMillis);
485 pushTimeRange(
new Interval(start, end));
489 if (viewMode.get() != visualizationMode) {
490 viewMode.set(visualizationMode);
497 protected Interval call()
throws Exception {
498 return filteredEvents.getSpanningInterval(events);
502 protected void succeeded() {
506 selectedTimeRange.set(
get());
507 selectedEventIDs.setAll(events);
510 }
catch (InterruptedException | ExecutionException ex) {
511 LOGGER.log(Level.SEVERE, getTitle() +
" Unexpected error", ex);
516 monitorTask(selectEventIDsTask);
523 synchronized private
void showWindow() {
524 if (mainFrame == null) {
532 ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
533 if (currentZoom == null) {
534 advance(InitialZoomState.withTypeZoomLevel(typeZoomeLevel));
540 @SuppressWarnings(
"AssignmentToMethodParameter")
541 synchronized public
boolean pushTimeRange(Interval timeRange) {
542 timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange);
543 ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
544 if (currentZoom == null) {
545 advance(InitialZoomState.withTimeRange(timeRange));
547 }
else if (currentZoom.
hasTimeRange(timeRange) ==
false) {
555 @NbBundle.Messages({
"# {0} - the number of events",
556 "Timeline.pushDescrLOD.confdlg.msg=You are about to show details for {0} events. This might be very slow or even crash Autopsy.\n\nDo you want to continue?",
557 "Timeline.pushDescrLOD.confdlg.title=Change description level of detail?"})
559 ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
560 if (currentZoom == null) {
561 advance(InitialZoomState.withDescrLOD(newLOD));
562 }
else if (currentZoom.
hasDescrLOD(newLOD) ==
false) {
567 @SuppressWarnings(
"AssignmentToMethodParameter")
569 timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange);
570 ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
571 if (currentZoom == null) {
572 advance(InitialZoomState.withTimeAndType(timeRange, typeZoom));
575 }
else if (currentZoom.
hasTimeRange(timeRange) ==
false) {
583 ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
584 if (currentZoom == null) {
585 advance(InitialZoomState.withFilter(filter.
copyOf()));
586 }
else if (currentZoom.
hasFilter(filter) ==
false) {
592 historyManager.advance();
596 historyManager.retreat();
600 historyManager.advance(newState);
604 final Interval timeRange = filteredEvents.getSpanningInterval().overlap(interval);
608 protected Collection< Long> call()
throws Exception {
610 return filteredEvents.getEventIDs(timeRange,
new TypeFilter(type));
615 protected void succeeded() {
619 selectedTimeRange.set(timeRange);
620 selectedEventIDs.setAll(
get());
623 }
catch (InterruptedException | ExecutionException ex) {
624 LOGGER.log(Level.SEVERE, getTitle() +
" Unexpected error", ex);
629 monitorTask(selectTimeAndTypeTask);
641 Platform.runLater(() -> {
644 task.stateProperty().addListener((Observable observable) -> {
645 switch (task.getState()) {
654 if (tasks.isEmpty() ==
false) {
655 taskProgress.bind(tasks.get(0).progressProperty());
656 taskMessage.bind(tasks.get(0).messageProperty());
657 taskTitle.bind(tasks.get(0).titleProperty());
663 taskProgress.bind(task.progressProperty());
664 taskMessage.bind(task.messageProperty());
665 taskTitle.bind(task.titleProperty());
666 switch (task.getState()) {
668 executor.submit(task);
677 if (tasks.isEmpty() ==
false) {
678 taskProgress.bind(tasks.get(0).progressProperty());
679 taskMessage.bind(tasks.get(0).messageProperty());
680 taskTitle.bind(tasks.get(0).titleProperty());
692 Interval getSpanningInterval(Collection<Long> eventIDs) {
693 return filteredEvents.getSpanningInterval(eventIDs);
702 @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
703 private
boolean isWindowOpen() {
704 return mainFrame != null && mainFrame.isOpened() && mainFrame.isVisible();
715 private
void confirmOutOfDateRebuildIfWindowOpen() throws MissingResourceException, HeadlessException {
716 if (isWindowOpen()) {
717 Platform.runLater(this::checkAndPromptForRebuild);
728 return eventsDBStale.get();
736 eventsDBStale.set(stale);
738 perCaseTimelineProperties.setDbStale(stale);
739 }
catch (IOException ex) {
740 MessageNotifyUtil.
Notify.
error(
"Timeline",
"Failed to mark the timeline db as " + (stale ?
"" :
"not ") +
"stale. Some results may be out of date or missing.");
741 LOGGER.log(Level.SEVERE,
"Error marking the timeline db as stale.", ex);
747 perCaseTimelineProperties.setIngestRunning(ingestRunning);
748 }
catch (IOException ex) {
749 MessageNotifyUtil.
Notify.
error(
"Timeline",
"Failed to mark the timeline db as populated while ingest was" + (ingestRunning ?
"" :
"not ") +
"running. Some results may be out of date or missing.");
750 LOGGER.log(Level.SEVERE,
"Error marking the ingest state while the timeline db was populated.", ex);
766 }
catch (IllegalStateException notUsed) {
774 case CONTENT_CHANGED:
778 Platform.runLater(() -> setEventsDBStale(
true));
792 SwingUtilities.invokeLater(
TimeLineController.this::confirmOutOfDateRebuildIfWindowOpen);
802 switch (
Case.
Events.valueOf(evt.getPropertyName())) {
803 case BLACKBOARD_ARTIFACT_TAG_ADDED:
806 case BLACKBOARD_ARTIFACT_TAG_DELETED:
809 case CONTENT_TAG_ADDED:
812 case CONTENT_TAG_DELETED:
815 case DATA_SOURCE_ADDED:
816 Platform.runLater(() -> {
817 setEventsDBStale(
true);
818 SwingUtilities.invokeLater(
TimeLineController.this::confirmOutOfDateRebuildIfWindowOpen);
synchronized ReadOnlyDoubleProperty taskProgressProperty()
synchronized void pushZoomInTime()
TimeLineController(Case autoCase)
void removeIngestModuleEventListener(final PropertyChangeListener listener)
void setEventsDBStale(final Boolean stale)
ReadOnlyStringProperty getStatusProperty()
ZoomParams withDescrLOD(DescriptionLoD descrLOD)
static synchronized IngestManager getInstance()
boolean hasTimeRange(Interval timeRange)
FilteredEventsModel getEventsModel()
FilteredEventsModel getEventsModel()
ZoomParams withTypeZoomLevel(EventTypeZoomLevel zoomLevel)
ReadOnlyBooleanProperty eventsDBStaleProperty()
static final ReadOnlyObjectWrapper< TimeZone > timeZone
static ReadOnlyObjectProperty< TimeZone > getTimeZone()
void propertyChange(PropertyChangeEvent evt)
ArrayList< String > getRebuildReasons()
synchronized ReadOnlyBooleanProperty getCanRetreat()
boolean isIngestRunning()
void selectEventIDs(Collection< Long > events)
boolean hasFilter(RootFilter filterSet)
void selectTimeAndType(Interval interval, EventType type)
synchronized ReadOnlyBooleanProperty getCanAdvance()
void removeIngestJobEventListener(final PropertyChangeListener listener)
synchronized ReadOnlyStringProperty taskTitleProperty()
boolean hasTypeZoomLevel(EventTypeZoomLevel typeZoom)
synchronized ReadOnlyStringProperty taskMessageProperty()
boolean hasDescrLOD(DescriptionLoD newLOD)
void applyDefaultFilters()
ZoomParams withTimeRange(Interval timeRange)
void propertyChange(PropertyChangeEvent evt)
void addIngestJobEventListener(final PropertyChangeListener listener)
static ZoneId getTimeZoneID()
static Interval getIntervalAround(DateTime aroundInstant, ReadablePeriod period)
static synchronized void setTimeZone(TimeZone timeZone)
void setStatus(String string)
synchronized void monitorTask(final Task<?> task)
synchronized void pushPeriod(ReadablePeriod period)
synchronized void advance(ZoomParams newState)
ZoomParams withTimeAndType(Interval timeRange, EventTypeZoomLevel zoomLevel)
synchronized void pushDescrLOD(DescriptionLoD newLOD)
synchronized void retreat()
static synchronized void removePropertyChangeListener(PropertyChangeListener listener)
static DateTimeZone getJodaTimeZone()
synchronized void pushFilters(RootFilter filter)
synchronized void advance()
final PerCaseTimelineProperties perCaseTimelineProperties
static void error(String title, String message)
void addIngestModuleEventListener(final PropertyChangeListener listener)
static synchronized void addPropertyChangeListener(PropertyChangeListener listener)
TagsFilter getTagsFilter()
static Case getCurrentCase()
synchronized static Logger getLogger(String name)
void propertyChange(PropertyChangeEvent evt)
ZoomParams withFilter(RootFilter filter)
boolean isEventsDBStale()
synchronized void pushZoomOutTime()
static DateTimeFormatter getZonedFormatter()
static DateTime middleOf(Interval interval)
synchronized ReadOnlyListProperty< Task<?> > getTasks()
static boolean isCaseOpen()
synchronized void setViewMode(VisualizationMode visualizationMode)
synchronized void pushEventTypeZoom(EventTypeZoomLevel typeZoomeLevel)
void setIngestRunning(boolean ingestRunning)