19 package org.sleuthkit.autopsy.timeline;
 
   21 import com.google.common.eventbus.EventBus;
 
   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.Collections;
 
   29 import java.util.List;
 
   30 import java.util.Optional;
 
   31 import java.util.TimeZone;
 
   32 import java.util.concurrent.ExecutionException;
 
   33 import java.util.concurrent.ExecutorService;
 
   34 import java.util.concurrent.Executors;
 
   35 import java.util.function.Consumer;
 
   36 import java.util.function.Function;
 
   37 import java.util.logging.Level;
 
   38 import javafx.application.Platform;
 
   39 import javafx.beans.Observable;
 
   40 import javafx.beans.property.ReadOnlyBooleanProperty;
 
   41 import javafx.beans.property.ReadOnlyBooleanWrapper;
 
   42 import javafx.beans.property.ReadOnlyDoubleProperty;
 
   43 import javafx.beans.property.ReadOnlyDoubleWrapper;
 
   44 import javafx.beans.property.ReadOnlyListProperty;
 
   45 import javafx.beans.property.ReadOnlyListWrapper;
 
   46 import javafx.beans.property.ReadOnlyObjectProperty;
 
   47 import javafx.beans.property.ReadOnlyObjectWrapper;
 
   48 import javafx.beans.property.ReadOnlyStringProperty;
 
   49 import javafx.beans.property.ReadOnlyStringWrapper;
 
   50 import javafx.collections.FXCollections;
 
   51 import javafx.collections.ObservableList;
 
   52 import javafx.collections.ObservableSet;
 
   53 import javafx.concurrent.Task;
 
   54 import javafx.concurrent.Worker;
 
   55 import static javafx.concurrent.Worker.State.FAILED;
 
   56 import static javafx.concurrent.Worker.State.SUCCEEDED;
 
   57 import javax.annotation.concurrent.GuardedBy;
 
   58 import javax.annotation.concurrent.Immutable;
 
   59 import javax.swing.SwingUtilities;
 
   60 import org.joda.time.DateTime;
 
   61 import org.joda.time.DateTimeZone;
 
   62 import org.joda.time.Interval;
 
   63 import org.joda.time.ReadablePeriod;
 
   64 import org.joda.time.format.DateTimeFormat;
 
   65 import org.joda.time.format.DateTimeFormatter;
 
   66 import org.openide.util.NbBundle;
 
  115 @NbBundle.Messages({
"Timeline.dialogs.title= Timeline",
 
  116     "TimeLinecontroller.updateNowQuestion=Do you want to update the events database now?"})
 
  121     private static final ReadOnlyObjectWrapper<TimeZone> timeZone = 
new ReadOnlyObjectWrapper<>(TimeZone.getDefault());
 
  124         return timeZone.get().toZoneId();
 
  128         return DateTimeFormat.forPattern(
"YYYY-MM-dd HH:mm:ss").withZone(getJodaTimeZone()); 
 
  132         return DateTimeZone.forTimeZone(getTimeZone().
get());
 
  136         return timeZone.getReadOnlyProperty();
 
  139     private final ExecutorService executor = Executors.newSingleThreadExecutor();
 
  141     private final ReadOnlyListWrapper<Task<?>> tasks = 
new ReadOnlyListWrapper<>(FXCollections.observableArrayList());
 
  143     private final ReadOnlyDoubleWrapper taskProgress = 
new ReadOnlyDoubleWrapper(-1);
 
  145     private final ReadOnlyStringWrapper taskMessage = 
new ReadOnlyStringWrapper();
 
  147     private final ReadOnlyStringWrapper taskTitle = 
new ReadOnlyStringWrapper();
 
  149     private final ReadOnlyStringWrapper statusMessage = 
new ReadOnlyStringWrapper();
 
  150     private EventBus eventbus = 
new EventBus(
"TimeLineController_EventBus");
 
  159         return statusMessage.getReadOnlyProperty();
 
  163         statusMessage.set(
string);
 
  169     private final ObservableList<
DescriptionFilter> quickHideFilters = FXCollections.observableArrayList();
 
  172         return quickHideFilters;
 
  182     synchronized public ReadOnlyListProperty<Task<?>> 
getTasks() {
 
  183         return tasks.getReadOnlyProperty();
 
  187         return taskProgress.getReadOnlyProperty();
 
  191         return taskMessage.getReadOnlyProperty();
 
  195         return taskTitle.getReadOnlyProperty();
 
  203     private 
boolean listeningToAutopsy = false;
 
  210     private final ReadOnlyObjectWrapper<
ViewMode> viewMode = new ReadOnlyObjectWrapper<>(
ViewMode.COUNTS);
 
  212     @GuardedBy("filteredEvents")
 
  224     private final ReadOnlyObjectWrapper<
ZoomParams> currentParams = new ReadOnlyObjectWrapper<>();
 
  228     private final ObservableList<Long> selectedEventIDs = FXCollections.<Long>observableArrayList();
 
  231     private final ReadOnlyObjectWrapper<Interval> selectedTimeRange = new ReadOnlyObjectWrapper<>();
 
  233     private final ReadOnlyBooleanWrapper eventsDBStale = new ReadOnlyBooleanWrapper(true);
 
  242     synchronized public ObservableList<Long> getSelectedEventIDs() {
 
  243         return selectedEventIDs;
 
  252         return selectedTimeRange.getReadOnlyProperty();
 
  261         return selectedTimeRange.get();
 
  265         return eventsDBStale.getReadOnlyProperty();
 
  274         return eventsDBStale.get();
 
  283         "TimeLineController.setEventsDBStale.errMsgStale=Failed to mark the timeline db as stale. Some results may be out of date or missing.",
 
  284         "TimeLineController.setEventsDBStale.errMsgNotStale=Failed to mark the timeline db as not stale. Some results may be out of date or missing."})
 
  285     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  287         eventsDBStale.set(stale);
 
  290             perCaseTimelineProperties.setDbStale(stale);
 
  291         } 
catch (IOException ex) {
 
  293                     stale ? Bundle.TimeLineController_setEventsDBStale_errMsgStale()
 
  294                             : Bundle.TimeLineController_setEventsDBStale_errMsgNotStale());
 
  295             LOGGER.log(Level.SEVERE, 
"Error marking the timeline db as stale.", ex); 
 
  300         return historyManager.getCanAdvance();
 
  304         return historyManager.getCanRetreat();
 
  308         return viewMode.getReadOnlyProperty();
 
  317         if (this.viewMode.get() != viewMode) {
 
  318             this.viewMode.set(viewMode);
 
  328         return viewMode.get();
 
  332         this.autoCase = autoCase;
 
  333         this.perCaseTimelineProperties = 
new PerCaseTimelineProperties(autoCase);
 
  334         eventsDBStale.set(perCaseTimelineProperties.isDBStale());
 
  335         eventsRepository = 
new EventsRepository(autoCase, currentParams.getReadOnlyProperty());
 
  343         historyManager.currentState().addListener((Observable observable) -> {
 
  344             ZoomParams historyManagerParams = historyManager.getCurrentState();
 
  346             currentParams.set(historyManagerParams);
 
  350         InitialZoomState = 
new ZoomParams(filteredEvents.getSpanningInterval(),
 
  352                 filteredEvents.filterProperty().get(),
 
  354         historyManager.advance(InitialZoomState);
 
  357         viewMode.addListener(observable -> selectEventIDs(Collections.emptySet()));
 
  364         return filteredEvents;
 
  368         pushFilters(filteredEvents.getDefaultFilter());
 
  372         Interval boundingEventsInterval = filteredEvents.getBoundingEventsInterval();
 
  373         advance(filteredEvents.zoomParametersProperty().get().withTimeRange(boundingEventsInterval));
 
  376     private final ObservableSet<TimeLineEvent> pinnedEvents = FXCollections.observableSet();
 
  377     private final ObservableSet<TimeLineEvent> pinnedEventsUnmodifiable = FXCollections.unmodifiableObservableSet(pinnedEvents);
 
  380         pinnedEvents.add(event);
 
  384         pinnedEvents.removeIf(event::equals);
 
  388         return pinnedEventsUnmodifiable;
 
  413         "TimeLineController.setIngestRunning.errMsgRunning=Failed to mark the timeline db as populated while ingest was running. Some results may be out of date or missing.",
 
  414         "TimeLinecontroller.setIngestRunning.errMsgNotRunning=Failed to mark the timeline db as populated while ingest was not running. Some results may be out of date or missing."})
 
  419         if (promptDialogManager.bringCurrentDialogToFront()) {
 
  424         if (ingestRunning && promptDialogManager.confirmDuringIngest() == 
false) {
 
  430         rebuildRepositoryTask = repoBuilder.apply(
new Consumer<Worker.State>() {
 
  432             public void accept(Worker.State newSate) {
 
  441                             perCaseTimelineProperties.setIngestRunning(ingestRunning);
 
  442                         } 
catch (IOException ex) {
 
  444                                     ingestRunning ? Bundle.TimeLineController_setIngestRunning_errMsgRunning()
 
  445                                             : Bundle.TimeLinecontroller_setIngestRunning_errMsgNotRunning());
 
  446                             LOGGER.log(Level.SEVERE, 
"Error marking the ingest state while the timeline db was populated.", ex); 
 
  448                         if (markDBNotStale) {
 
  449                             setEventsDBStale(
false);
 
  450                             filteredEvents.postDBUpdated();
 
  452                         if (file == null && artifact == null) {
 
  457                             ShowInTimelineDialog showInTimelineDilaog =
 
  461                             Optional<ViewInTimelineRequestedEvent> dialogResult = showInTimelineDilaog.showAndWait();
 
  462                             dialogResult.ifPresent(viewInTimelineRequestedEvent -> {
 
  464                                 showInListView(viewInTimelineRequestedEvent); 
 
  470                         setEventsDBStale(
true);
 
  480         promptDialogManager.showDBPopulationProgressDialog(rebuildRepositoryTask);
 
  488     public 
void rebuildRepo() {
 
  489         rebuildRepo(null, null);
 
  501     private 
void rebuildRepo(AbstractFile file, BlackboardArtifact artifact) {
 
  502         rebuildRepoHelper(eventsRepository::rebuildRepository, 
true, file, artifact);
 
  514     private 
void rebuildTagsTable(AbstractFile file, BlackboardArtifact artifact) {
 
  515         rebuildRepoHelper(eventsRepository::rebuildTags, 
false, file, artifact);
 
  522         synchronized (filteredEvents) {
 
  523             return pushTimeRange(filteredEvents.getSpanningInterval());
 
  536         synchronized (filteredEvents) {
 
  538             selectEventIDs(requestEvent.getEventIDs());
 
  539             if (pushTimeRange(requestEvent.getInterval()) == 
false) {
 
  540                 eventbus.post(requestEvent);
 
  550     public 
void shutDownTimeLine() {
 
  551         listeningToAutopsy = 
false;
 
  555         if (topComponent != null) {
 
  556             topComponent.close();
 
  571     void showTimeLine(AbstractFile file, BlackboardArtifact artifact) {
 
  577             listeningToAutopsy = 
true;
 
  579         Platform.runLater(() -> promptForRebuild(file, artifact));
 
  592     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
  593     private 
void promptForRebuild(AbstractFile file, BlackboardArtifact artifact) {
 
  595         if (promptDialogManager.bringCurrentDialogToFront()) {
 
  600         if (eventsRepository.countAllEvents() == 0) {
 
  601             rebuildRepo(file, artifact);
 
  606         List<String> rebuildReasons = getRebuildReasons();
 
  607         if (
false == rebuildReasons.isEmpty()) {
 
  608             if (promptDialogManager.confirmRebuild(rebuildReasons)) {
 
  609                 rebuildRepo(file, artifact);
 
  621         rebuildTagsTable(file, artifact);
 
  632     @NbBundle.Messages({
"TimeLineController.errorTitle=Timeline error.",
 
  633         "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.",
 
  634         "TimeLineController.rebuildReasons.outOfDateError=Could not determine if the timeline data is out of date.",
 
  635         "TimeLineController.rebuildReasons.outOfDate=The event data is out of date:  Not all events will be visible.",
 
  636         "TimeLineController.rebuildReasons.ingestWasRunning=The Timeline events database was previously populated while ingest was running:  Some events may be missing, incomplete, or inaccurate.",
 
  637         "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."})
 
  639         ArrayList<String> rebuildReasons = 
new ArrayList<>();
 
  643             if (perCaseTimelineProperties.wasIngestRunning()) {
 
  644                 rebuildReasons.add(Bundle.TimeLineController_rebuildReasons_ingestWasRunning());
 
  647         } 
catch (IOException ex) {
 
  648             LOGGER.log(Level.SEVERE, 
"Error determing the state of the timeline db. We will assume the it is out of date.", ex); 
 
  650                     Bundle.TimeLineController_outOfDate_errorMessage());
 
  651             rebuildReasons.add(Bundle.TimeLineController_rebuildReasons_outOfDateError());
 
  654         if (isEventsDBStale()) {
 
  655             rebuildReasons.add(Bundle.TimeLineController_rebuildReasons_outOfDate());
 
  658         if (eventsRepository.hasNewColumns() == 
false) {
 
  659             rebuildReasons.add(Bundle.TimeLineController_rebuildReasons_incompleteOldSchema());
 
  661         return rebuildReasons;
 
  672         synchronized (filteredEvents) {
 
  678         final Interval timeRange = filteredEvents.timeRangeProperty().get();
 
  679         long toDurationMillis = timeRange.toDurationMillis() / 4;
 
  680         DateTime start = timeRange.getStart().minus(toDurationMillis);
 
  681         DateTime end = timeRange.getEnd().plus(toDurationMillis);
 
  682         pushTimeRange(
new Interval(start, end));
 
  686         final Interval timeRange = filteredEvents.timeRangeProperty().get();
 
  687         long toDurationMillis = timeRange.toDurationMillis() / 4;
 
  688         DateTime start = timeRange.getStart().plus(toDurationMillis);
 
  689         DateTime end = timeRange.getEnd().minus(toDurationMillis);
 
  690         pushTimeRange(
new Interval(start, end));
 
  698     synchronized private 
void showWindow() {
 
  699         if (topComponent == null) {
 
  702         if (topComponent.isOpened() == 
false) {
 
  705         topComponent.toFront();
 
  710         topComponent.requestActive();
 
  714         ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
 
  715         if (currentZoom == null) {
 
  716             advance(InitialZoomState.withTypeZoomLevel(typeZoomeLevel));
 
  733         Interval clampedTimeRange;
 
  734         if (timeRange == null) {
 
  735             clampedTimeRange = this.filteredEvents.getSpanningInterval();
 
  737             Interval spanningInterval = this.filteredEvents.getSpanningInterval();
 
  738             if (spanningInterval.overlaps(timeRange)) {
 
  739                 clampedTimeRange = spanningInterval.overlap(timeRange);
 
  741                 clampedTimeRange = spanningInterval;
 
  745         ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
 
  746         if (currentZoom == null) {
 
  747             advance(InitialZoomState.withTimeRange(clampedTimeRange));
 
  749         } 
else if (currentZoom.
hasTimeRange(clampedTimeRange) == 
false) {
 
  767             return showFullRange();
 
  774         ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
 
  775         if (currentZoom == null) {
 
  776             advance(InitialZoomState.withDescrLOD(newLOD));
 
  777         } 
else if (currentZoom.
hasDescrLOD(newLOD) == 
false) {
 
  782     @SuppressWarnings(
"AssignmentToMethodParameter") 
 
  784         timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange);
 
  785         ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
 
  786         if (currentZoom == null) {
 
  787             advance(InitialZoomState.withTimeAndType(timeRange, typeZoom));
 
  790         } 
else if (currentZoom.
hasTimeRange(timeRange) == 
false) {
 
  798         ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
 
  799         if (currentZoom == null) {
 
  800             advance(InitialZoomState.withFilter(filter.
copyOf()));
 
  801         } 
else if (currentZoom.
hasFilter(filter) == 
false) {
 
  807         historyManager.advance();
 
  811         historyManager.retreat();
 
  815         historyManager.advance(newState);
 
  825         selectedTimeRange.set(filteredEvents.getSpanningInterval(eventIDs));
 
  826         selectedEventIDs.setAll(eventIDs);
 
  830         final Interval timeRange = filteredEvents.getSpanningInterval().overlap(interval);
 
  834             protected Collection< Long> call() 
throws Exception {
 
  836                     return filteredEvents.getEventIDs(timeRange, 
new TypeFilter(type));
 
  841             protected void succeeded() {
 
  845                         selectedTimeRange.set(timeRange);
 
  846                         selectedEventIDs.setAll(
get());
 
  849                 } 
catch (InterruptedException | ExecutionException ex) {
 
  850                     LOGGER.log(Level.SEVERE, getTitle() + 
" Unexpected error", ex); 
 
  855         monitorTask(selectTimeAndTypeTask);
 
  867             Platform.runLater(() -> {
 
  870                 task.stateProperty().addListener((Observable observable) -> {
 
  871                     switch (task.getState()) {
 
  880                             if (tasks.isEmpty() == 
false) {
 
  881                                 taskProgress.bind(tasks.get(0).progressProperty());
 
  882                                 taskMessage.bind(tasks.get(0).messageProperty());
 
  883                                 taskTitle.bind(tasks.get(0).titleProperty());
 
  889                 taskProgress.bind(task.progressProperty());
 
  890                 taskMessage.bind(task.messageProperty());
 
  891                 taskTitle.bind(task.titleProperty());
 
  892                 switch (task.getState()) {
 
  894                         executor.submit(task);
 
  903                         if (tasks.isEmpty() == 
false) {
 
  904                             taskProgress.bind(tasks.get(0).progressProperty());
 
  905                             taskMessage.bind(tasks.get(0).messageProperty());
 
  906                             taskTitle.bind(tasks.get(0).titleProperty());
 
  921         eventbus.register(o);
 
  930         eventbus.unregister(o);
 
  960                 case CONTENT_CHANGED:
 
  963                     Platform.runLater(() -> setEventsDBStale(
true));
 
  984                 case DATA_SOURCE_ANALYSIS_COMPLETED:
 
  986                     Platform.runLater(() -> setEventsDBStale(
true));
 
  987                     filteredEvents.postAutopsyEventLocally((
AutopsyEvent) evt);
 
  989                 case DATA_SOURCE_ANALYSIS_STARTED:
 
 1006             switch (
Case.
Events.valueOf(evt.getPropertyName())) {
 
 1007                 case BLACKBOARD_ARTIFACT_TAG_ADDED:
 
 1010                 case BLACKBOARD_ARTIFACT_TAG_DELETED:
 
 1013                 case CONTENT_TAG_ADDED:
 
 1016                 case CONTENT_TAG_DELETED:
 
 1019                 case DATA_SOURCE_ADDED:
 
 1021                     Platform.runLater(() -> setEventsDBStale(
true));
 
 1022                     filteredEvents.postAutopsyEventLocally((
AutopsyEvent) evt);
 
synchronized ReadOnlyDoubleProperty taskProgressProperty()
synchronized void pushZoomInTime()
TimeLineController(Case autoCase)
void removeIngestModuleEventListener(final PropertyChangeListener listener)
void setEventsDBStale(final Boolean stale)
ObservableSet< TimeLineEvent > getPinnedEvents()
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
synchronized ViewMode getViewMode()
static ReadOnlyObjectProperty< TimeZone > getTimeZone()
void propertyChange(PropertyChangeEvent evt)
static void removePropertyChangeListener(PropertyChangeListener listener)
void rebuildRepoHelper(Function< Consumer< Worker.State >, CancellationProgressTask<?>> repoBuilder, Boolean markDBNotStale, AbstractFile file, BlackboardArtifact artifact)
synchronized void setViewMode(ViewMode viewMode)
boolean isIngestRunning()
synchronized void selectEventIDs(Collection< Long > eventIDs)
synchronized ReadOnlyObjectProperty< Interval > selectedTimeRangeProperty()
boolean hasFilter(RootFilter filterSet)
void pinEvent(TimeLineEvent event)
void selectTimeAndType(Interval interval, EventType type)
synchronized boolean pushTimeUnit(TimeUnits timeUnit)
synchronized boolean pushTimeRange(Interval timeRange)
void removeIngestJobEventListener(final PropertyChangeListener listener)
synchronized ReadOnlyStringProperty taskTitleProperty()
boolean hasTypeZoomLevel(EventTypeZoomLevel typeZoom)
synchronized Interval getSelectedTimeRange()
synchronized ReadOnlyStringProperty taskMessageProperty()
boolean hasDescrLOD(DescriptionLoD newLOD)
void applyDefaultFilters()
static Interval getIntervalAroundMiddle(Interval interval, ReadablePeriod period)
List< String > getRebuildReasons()
ZoomParams withTimeRange(Interval timeRange)
void propertyChange(PropertyChangeEvent evt)
synchronized void unRegisterForEvents(Object o)
void addIngestJobEventListener(final PropertyChangeListener listener)
static ZoneId getTimeZoneID()
static synchronized void setTimeZone(TimeZone timeZone)
synchronized void monitorTask(final Task<?> task)
synchronized void pushPeriod(ReadablePeriod period)
synchronized void registerForEvents(Object o)
static void addPropertyChangeListener(PropertyChangeListener listener)
synchronized void advance(ZoomParams newState)
ReadOnlyStringProperty statusMessageProperty()
ZoomParams withTimeAndType(Interval timeRange, EventTypeZoomLevel zoomLevel)
synchronized void pushDescrLOD(DescriptionLoD newLOD)
synchronized void retreat()
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)
TagsFilter getTagsFilter()
synchronized ReadOnlyObjectProperty< ViewMode > viewModeProperty()
synchronized static Logger getLogger(String name)
void propertyChange(PropertyChangeEvent evt)
static Case getCurrentCaseThrows()
synchronized ReadOnlyBooleanProperty canAdvanceProperty()
ZoomParams withFilter(RootFilter filter)
boolean isEventsDBStale()
synchronized void pushZoomOutTime()
static DateTimeFormatter getZonedFormatter()
synchronized ReadOnlyListProperty< Task<?> > getTasks()
void unPinEvent(TimeLineEvent event)
synchronized ReadOnlyBooleanProperty canRetreatProperty()
static boolean isCaseOpen()
synchronized void pushEventTypeZoom(EventTypeZoomLevel typeZoomeLevel)
void setStatusMessage(String string)