/*
* Autopsy Forensic Browser
*
* Copyright 2014-2016 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.timeline;
import com.google.common.eventbus.EventBus;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.TimeZone;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import static javafx.concurrent.Worker.State.FAILED;
import static javafx.concurrent.Worker.State.SUCCEEDED;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.Immutable;
import javax.swing.SwingUtilities;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Interval;
import org.joda.time.ReadablePeriod;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE;
import static org.sleuthkit.autopsy.casemodule.Case.Events.DATA_SOURCE_ADDED;
import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.coreutils.History;
import org.sleuthkit.autopsy.coreutils.LoggedTask;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.events.AutopsyEvent;
import org.sleuthkit.autopsy.ingest.IngestManager;
import static org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent.CANCELLED;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.db.EventsRepository;
import org.sleuthkit.autopsy.timeline.events.ViewInTimelineRequestedEvent;
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
import org.sleuthkit.autopsy.timeline.filters.RootFilter;
import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
import org.sleuthkit.autopsy.timeline.utils.IntervalUtils;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
import org.sleuthkit.autopsy.timeline.zooming.TimeUnits;
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
/**
* Controller in the MVC design along with FilteredEventsModel TimeLineView.
* Forwards interpreted user gestures form views to model. Provides model to
* view. Is entry point for timeline module.
*
* Concurrency Policy:<ul>
* <li>Since filteredEvents is internally synchronized, only compound access to
* it needs external synchronization</li>
* * <li>Since eventsRepository is internally synchronized, only compound
* access to it needs external synchronization <li>
* <li>Other state including listeningToAutopsy, mainFrame, viewMode, and the
* listeners should only be accessed with this object's intrinsic lock held, or
* on the EDT as indicated.
* </li>
* </ul>
*/
@NbBundle.Messages({"Timeline.dialogs.title= Timeline",
"TimeLinecontroller.updateNowQuestion=Do you want to update the events database now?"})
public class TimeLineController {
private static final Logger LOGGER = Logger.getLogger(TimeLineController.class.getName());
private static final ReadOnlyObjectWrapper<TimeZone> timeZone = new ReadOnlyObjectWrapper<>(TimeZone.getDefault());
public static ZoneId getTimeZoneID() {
return timeZone.get().toZoneId();
}
public static DateTimeFormatter getZonedFormatter() {
return DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss").withZone(getJodaTimeZone()); //NON-NLS
}
public static DateTimeZone getJodaTimeZone() {
return DateTimeZone.forTimeZone(getTimeZone().get());
}
public static ReadOnlyObjectProperty<TimeZone> getTimeZone() {
return timeZone.getReadOnlyProperty();
}
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final ReadOnlyListWrapper<Task<?>> tasks = new ReadOnlyListWrapper<>(FXCollections.observableArrayList());
private final ReadOnlyDoubleWrapper taskProgress = new ReadOnlyDoubleWrapper(-1);
private final ReadOnlyStringWrapper taskMessage = new ReadOnlyStringWrapper();
private final ReadOnlyStringWrapper taskTitle = new ReadOnlyStringWrapper();
private final ReadOnlyStringWrapper statusMessage = new ReadOnlyStringWrapper();
private EventBus eventbus = new EventBus("TimeLineController_EventBus");
/**
* Status is a string that will be displayed in the status bar as a kind of
* user hint/information when it is not empty
*
* @return The status property
*/
public ReadOnlyStringProperty statusMessageProperty() {
return statusMessage.getReadOnlyProperty();
}
public void setStatusMessage(String string) {
statusMessage.set(string);
}
private final Case autoCase;
private final PerCaseTimelineProperties perCaseTimelineProperties;
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final ObservableList<DescriptionFilter> quickHideFilters = FXCollections.observableArrayList();
public ObservableList<DescriptionFilter> getQuickHideFilters() {
return quickHideFilters;
}
/**
* @return The autopsy Case assigned to the controller
*/
public Case getAutopsyCase() {
return autoCase;
}
synchronized public ReadOnlyListProperty<Task<?>> getTasks() {
return tasks.getReadOnlyProperty();
}
synchronized public ReadOnlyDoubleProperty taskProgressProperty() {
return taskProgress.getReadOnlyProperty();
}
synchronized public ReadOnlyStringProperty taskMessageProperty() {
return taskMessage.getReadOnlyProperty();
}
synchronized public ReadOnlyStringProperty taskTitleProperty() {
return taskTitle.getReadOnlyProperty();
}
@ThreadConfined(type = ThreadConfined.ThreadType.AWT)
private TimeLineTopComponent topComponent;
//are the listeners currently attached
@ThreadConfined(type = ThreadConfined.ThreadType.AWT)
private boolean listeningToAutopsy = false;
private final PropertyChangeListener caseListener = new AutopsyCaseListener();
private final PropertyChangeListener ingestJobListener = new AutopsyIngestJobListener();
private final PropertyChangeListener ingestModuleListener = new AutopsyIngestModuleListener();
@GuardedBy("this")
private final ReadOnlyObjectWrapper<ViewMode> viewMode = new ReadOnlyObjectWrapper<>(ViewMode.COUNTS);
@GuardedBy("filteredEvents")
private final FilteredEventsModel filteredEvents;
private final EventsRepository eventsRepository;
@GuardedBy("this")
private final ZoomParams InitialZoomState;
@GuardedBy("this")
private final History<ZoomParams> historyManager = new History<>();
@GuardedBy("this")
private final ReadOnlyObjectWrapper<ZoomParams> currentParams = new ReadOnlyObjectWrapper<>();
//selected events (ie shown in the result viewer)
@GuardedBy("this")
private final ObservableList<Long> selectedEventIDs = FXCollections.<Long>observableArrayList();
@GuardedBy("this")
private final ReadOnlyObjectWrapper<Interval> selectedTimeRange = new ReadOnlyObjectWrapper<>();
private final ReadOnlyBooleanWrapper eventsDBStale = new ReadOnlyBooleanWrapper(true);
private final PromptDialogManager promptDialogManager = new PromptDialogManager(this);
/**
* Get an ObservableList of selected event IDs
*
* @return A list of the selected event IDs
*/
synchronized public ObservableList<Long> getSelectedEventIDs() {
return selectedEventIDs;
}
/**
* Get a read only observable view of the selected time range.
*
* @return A read only view of the selected time range.
*/
synchronized public ReadOnlyObjectProperty<Interval> selectedTimeRangeProperty() {
return selectedTimeRange.getReadOnlyProperty();
}
/**
* Get the selected time range.
*
* @return The selected time range.
*/
synchronized public Interval getSelectedTimeRange() {
return selectedTimeRange.get();
}
public ReadOnlyBooleanProperty eventsDBStaleProperty() {
return eventsDBStale.getReadOnlyProperty();
}
/**
* Is the events db out of date (stale)?
*
* @return True if the events db is out of date , false otherwise
*/
public boolean isEventsDBStale() {
return eventsDBStale.get();
}
/**
* Set the events database stale or not
*
* @param stale The new state of the events db: stale/not-stale
*/
@NbBundle.Messages({
"TimeLineController.setEventsDBStale.errMsgStale=Failed to mark the timeline db as stale. Some results may be out of date or missing.",
"TimeLineController.setEventsDBStale.errMsgNotStale=Failed to mark the timeline db as not stale. Some results may be out of date or missing."})
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void setEventsDBStale(final Boolean stale) {
eventsDBStale.set(stale);
try {
//persist to disk
perCaseTimelineProperties.setDbStale(stale);
} catch (IOException ex) {
MessageNotifyUtil.Notify.error(Bundle.Timeline_dialogs_title(),
stale ? Bundle.TimeLineController_setEventsDBStale_errMsgStale()
: Bundle.TimeLineController_setEventsDBStale_errMsgNotStale());
LOGGER.log(Level.SEVERE, "Error marking the timeline db as stale.", ex); //NON-NLS
}
}
synchronized public ReadOnlyBooleanProperty canAdvanceProperty() {
return historyManager.getCanAdvance();
}
synchronized public ReadOnlyBooleanProperty canRetreatProperty() {
return historyManager.getCanRetreat();
}
synchronized public ReadOnlyObjectProperty<ViewMode> viewModeProperty() {
return viewMode.getReadOnlyProperty();
}
/**
* Set a new ViewMode as the active one.
*
* @param viewMode The new ViewMode to set.
*/
synchronized public void setViewMode(ViewMode viewMode) {
if (this.viewMode.get() != viewMode) {
this.viewMode.set(viewMode);
}
}
/**
* Get the currently active ViewMode.
*
* @return The currently active ViewMode.
*/
synchronized public ViewMode getViewMode() {
return viewMode.get();
}
public TimeLineController(Case autoCase) throws IOException {
this.autoCase = autoCase;
this.perCaseTimelineProperties = new PerCaseTimelineProperties(autoCase);
eventsDBStale.set(perCaseTimelineProperties.isDBStale());
eventsRepository = new EventsRepository(autoCase, currentParams.getReadOnlyProperty());
/*
* as the history manager's current state changes, modify the tags
* filter to be in sync, and expose that as propery from
* TimeLineController. Do we need to do this with datasource or hash hit
* filters?
*/
historyManager.currentState().addListener((Observable observable) -> {
ZoomParams historyManagerParams = historyManager.getCurrentState();
eventsRepository.syncTagsFilter(historyManagerParams.getFilter().getTagsFilter());
currentParams.set(historyManagerParams);
});
filteredEvents = eventsRepository.getEventsModel();
InitialZoomState = new ZoomParams(filteredEvents.getSpanningInterval(),
EventTypeZoomLevel.BASE_TYPE,
filteredEvents.filterProperty().get(),
DescriptionLoD.SHORT);
historyManager.advance(InitialZoomState);
//clear the selected events when the view mode changes
viewMode.addListener(observable -> selectEventIDs(Collections.emptySet()));
}
/**
* @return a shared events model
*/
public FilteredEventsModel getEventsModel() {
return filteredEvents;
}
public void applyDefaultFilters() {
pushFilters(filteredEvents.getDefaultFilter());
}
public void zoomOutToActivity() {
Interval boundingEventsInterval = filteredEvents.getBoundingEventsInterval();
advance(filteredEvents.zoomParametersProperty().get().withTimeRange(boundingEventsInterval));
}
private final ObservableSet<TimeLineEvent> pinnedEvents = FXCollections.observableSet();
private final ObservableSet<TimeLineEvent> pinnedEventsUnmodifiable = FXCollections.unmodifiableObservableSet(pinnedEvents);
public void pinEvent(TimeLineEvent event) {
pinnedEvents.add(event);
}
public void unPinEvent(TimeLineEvent event) {
pinnedEvents.removeIf(event::equals);
}
public ObservableSet<TimeLineEvent> getPinnedEvents() {
return pinnedEventsUnmodifiable;
}
/**
* Rebuild the repo using the given repoBuilder (expected to be a member
* reference to EventsRepository.rebuildRepository() or
* EventsRepository.rebuildTags()) and display the UI when it is done. If
* either file or artifact is not null the user will be prompted to choose a
* derived event and time range to show in the Timeline List View.
*
* @param repoBuilder A Function from Consumer<Worker.State> to
* CancellationProgressTask<?>. Ie a function that
* given a worker state listener, produces a task with
* that listener attached. Expected to be a method
* reference to either
* EventsRepository.rebuildRepository() or
* EventsRepository.rebuildTags()
* @param markDBNotStale After the repo is rebuilt should it be marked not
* stale
* @param file The AbstractFile from which to choose an event to
* show in the List View.
* @param artifact The BlackboardArtifact to show in the List View.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@NbBundle.Messages({
"TimeLineController.setIngestRunning.errMsgRunning=Failed to mark the timeline db as populated while ingest was running. Some results may be out of date or missing.",
"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."})
private void rebuildRepoHelper(Function<Consumer<Worker.State>, CancellationProgressTask<?>> repoBuilder, Boolean markDBNotStale, AbstractFile file, BlackboardArtifact artifact) {
boolean ingestRunning = IngestManager.getInstance().isIngestRunning();
//if there is an existing prompt or progressdialog, just show that
if (promptDialogManager.bringCurrentDialogToFront()) {
return;
}
//confirm timeline during ingest
if (ingestRunning && promptDialogManager.confirmDuringIngest() == false) {
return; //if they cancel, do nothing.
}
//get a task that rebuilds the repo with the below state listener attached
final CancellationProgressTask<?> rebuildRepositoryTask;
rebuildRepositoryTask = repoBuilder.apply(new Consumer<Worker.State>() {
@Override
public void accept(Worker.State newSate) {
//this will be on JFX thread
switch (newSate) {
case SUCCEEDED:
/*
* Record if ingest was running the last time the db was
* rebuilt, and hence it might stale.
*/
try {
perCaseTimelineProperties.setIngestRunning(ingestRunning);
} catch (IOException ex) {
MessageNotifyUtil.Notify.error(Bundle.Timeline_dialogs_title(),
ingestRunning ? Bundle.TimeLineController_setIngestRunning_errMsgRunning()
: Bundle.TimeLinecontroller_setIngestRunning_errMsgNotRunning());
LOGGER.log(Level.SEVERE, "Error marking the ingest state while the timeline db was populated.", ex); //NON-NLS
}
if (markDBNotStale) {
setEventsDBStale(false);
filteredEvents.postDBUpdated();
}
if (file == null && artifact == null) {
SwingUtilities.invokeLater(TimeLineController.this::showWindow);
TimeLineController.this.showFullRange();
} else {
//prompt user to pick specific event and time range
ShowInTimelineDialog showInTimelineDilaog =
(file == null)
? new ShowInTimelineDialog(TimeLineController.this, artifact)
: new ShowInTimelineDialog(TimeLineController.this, file);
Optional<ViewInTimelineRequestedEvent> dialogResult = showInTimelineDilaog.showAndWait();
dialogResult.ifPresent(viewInTimelineRequestedEvent -> {
SwingUtilities.invokeLater(TimeLineController.this::showWindow);
showInListView(viewInTimelineRequestedEvent); //show requested event in list view
});
}
break;
case FAILED:
case CANCELLED:
setEventsDBStale(true);
break;
}
}
});
/*
* Since both of the expected repoBuilders start the back ground task,
* all we have to do is show progress dialog for the task
*/
promptDialogManager.showDBPopulationProgressDialog(rebuildRepositoryTask);
}
/**
* Rebuild the entire repo in the background, and show the timeline when
* done.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
public void rebuildRepo() {
rebuildRepo(null, null);
}
/**
* Rebuild the entire repo in the background, and show the timeline when
* done.
*
* @param file The AbstractFile from which to choose an event to show in
* the List View.
* @param artifact The BlackboardArtifact to show in the List View.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void rebuildRepo(AbstractFile file, BlackboardArtifact artifact) {
rebuildRepoHelper(eventsRepository::rebuildRepository, true, file, artifact);
}
/**
* Drop the tags table and rebuild it in the background, and show the
* timeline when done.
*
* @param file The AbstractFile from which to choose an event to show in
* the List View.
* @param artifact The BlackboardArtifact to show in the List View.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void rebuildTagsTable(AbstractFile file, BlackboardArtifact artifact) {
rebuildRepoHelper(eventsRepository::rebuildTags, false, file, artifact);
}
/**
* Show the entire range of the timeline.
*/
private boolean showFullRange() {
synchronized (filteredEvents) {
return pushTimeRange(filteredEvents.getSpanningInterval());
}
}
/**
* Show the events and the amount of time indicated in the given
* ViewInTimelineRequestedEvent in the List View.
*
* @param requestEvent Contains the ID of the requested events and the
* timerange to show.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void showInListView(ViewInTimelineRequestedEvent requestEvent) {
synchronized (filteredEvents) {
setViewMode(ViewMode.LIST);
selectEventIDs(requestEvent.getEventIDs());
if (pushTimeRange(requestEvent.getInterval()) == false) {
eventbus.post(requestEvent);
}
}
}
/**
* "Shut down" Timeline. Remove all the case and ingest listers. Close the
* timeline window.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.AWT)
public void shutDownTimeLine() {
listeningToAutopsy = false;
IngestManager.getInstance().removeIngestModuleEventListener(ingestModuleListener);
IngestManager.getInstance().removeIngestJobEventListener(ingestJobListener);
Case.removePropertyChangeListener(caseListener);
if (topComponent != null) {
topComponent.close();
topComponent = null;
}
OpenTimelineAction.invalidateController();
}
/**
* Add the case and ingest listeners, prompt for rebuilding the database if
* necessary, and show the timeline window.
*
* @param file The AbstractFile from which to choose an event to show in
* the List View.
* @param artifact The BlackboardArtifact to show in the List View.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.AWT)
void showTimeLine(AbstractFile file, BlackboardArtifact artifact) {
// listen for case changes (specifically images being added, and case changes).
if (Case.isCaseOpen() && !listeningToAutopsy) {
IngestManager.getInstance().addIngestModuleEventListener(ingestModuleListener);
IngestManager.getInstance().addIngestJobEventListener(ingestJobListener);
Case.addPropertyChangeListener(caseListener);
listeningToAutopsy = true;
}
Platform.runLater(() -> promptForRebuild(file, artifact));
}
/**
* Prompt the user to confirm rebuilding the db. Checks if a database
* rebuild is necessary and includes the reasons in the prompt. If the user
* confirms, rebuilds the database. Shows the timeline window when the
* rebuild is done, or immediately if the rebuild is not confirmed.
*
* @param file The AbstractFile from which to choose an event to show in
* the List View.
* @param artifact The BlackboardArtifact to show in the List View.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void promptForRebuild(AbstractFile file, BlackboardArtifact artifact) {
//if there is an existing prompt or progressdialog, just show that
if (promptDialogManager.bringCurrentDialogToFront()) {
return;
}
//if the repo is empty just (re)build it with out asking, the user can always cancel part way through
if (eventsRepository.countAllEvents() == 0) {
rebuildRepo(file, artifact);
return;
}
//if necessary prompt user with reasons to rebuild
List<String> rebuildReasons = getRebuildReasons();
if (false == rebuildReasons.isEmpty()) {
if (promptDialogManager.confirmRebuild(rebuildReasons)) {
rebuildRepo(file, artifact);
return;
}
}
/*
* if the repo was not rebuilt, at a minimum rebuild the tags which may
* have been updated without our knowing it, since we can't/aren't
* checking them. This should at least be quick.
*
* //TODO: can we check the tags to see if we need to do this?
*/
rebuildTagsTable(file, artifact);
}
/**
* Get a list of reasons why the user might won't to rebuild the database.
* The potential reasons are not necessarily orthogonal to each other.
*
* @return A list of reasons why the user might won't to rebuild the
* database.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.ANY)
@NbBundle.Messages({"TimeLineController.errorTitle=Timeline error.",
"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.",
"TimeLineController.rebuildReasons.outOfDateError=Could not determine if the timeline data is out of date.",
"TimeLineController.rebuildReasons.outOfDate=The event data is out of date: Not all events will be visible.",
"TimeLineController.rebuildReasons.ingestWasRunning=The Timeline events database was previously populated while ingest was running: Some events may be missing, incomplete, or inaccurate.",
"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."})
private List<String> getRebuildReasons() {
ArrayList<String> rebuildReasons = new ArrayList<>();
try {
//if ingest was running during last rebuild, prompt to rebuild
if (perCaseTimelineProperties.wasIngestRunning()) {
rebuildReasons.add(Bundle.TimeLineController_rebuildReasons_ingestWasRunning());
}
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, "Error determing the state of the timeline db. We will assume the it is out of date.", ex); //NON-NLS
MessageNotifyUtil.Notify.error(Bundle.TimeLineController_errorTitle(),
Bundle.TimeLineController_outOfDate_errorMessage());
rebuildReasons.add(Bundle.TimeLineController_rebuildReasons_outOfDateError());
}
//if the events db is stale, prompt to rebuild
if (isEventsDBStale()) {
rebuildReasons.add(Bundle.TimeLineController_rebuildReasons_outOfDate());
}
// if the TL DB schema has been upgraded since last time TL ran, prompt for rebuild
if (eventsRepository.hasNewColumns() == false) {
rebuildReasons.add(Bundle.TimeLineController_rebuildReasons_incompleteOldSchema());
}
return rebuildReasons;
}
/**
* Request a time range the same length as the given period and centered
* around the middle of the currently viewed time range.
*
* @param period The period of time to show around the current center of the
* view.
*/
synchronized public void pushPeriod(ReadablePeriod period) {
synchronized (filteredEvents) {
pushTimeRange(IntervalUtils.getIntervalAroundMiddle(filteredEvents.getTimeRange(), period));
}
}
synchronized public void pushZoomOutTime() {
final Interval timeRange = filteredEvents.timeRangeProperty().get();
long toDurationMillis = timeRange.toDurationMillis() / 4;
DateTime start = timeRange.getStart().minus(toDurationMillis);
DateTime end = timeRange.getEnd().plus(toDurationMillis);
pushTimeRange(new Interval(start, end));
}
synchronized public void pushZoomInTime() {
final Interval timeRange = filteredEvents.timeRangeProperty().get();
long toDurationMillis = timeRange.toDurationMillis() / 4;
DateTime start = timeRange.getStart().plus(toDurationMillis);
DateTime end = timeRange.getEnd().minus(toDurationMillis);
pushTimeRange(new Interval(start, end));
}
/**
* Show the timeline TimeLineTopComponent. This method will construct a new
* instance of TimeLineTopComponent if necessary.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.AWT)
synchronized private void showWindow() {
if (topComponent == null) {
topComponent = new TimeLineTopComponent(this);
}
topComponent.open();
topComponent.toFront();
/*
* Make this top component active so its ExplorerManager's lookup gets
* proxied in Utilities.actionsGlobalContext()
*/
topComponent.requestActive();
}
synchronized public void pushEventTypeZoom(EventTypeZoomLevel typeZoomeLevel) {
ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
if (currentZoom == null) {
advance(InitialZoomState.withTypeZoomLevel(typeZoomeLevel));
} else if (currentZoom.hasTypeZoomLevel(typeZoomeLevel) == false) {
advance(currentZoom.withTypeZoomLevel(typeZoomeLevel));
}
}
/**
* Set the new interval to view, and record it in the history. The interval
* will be clamped to the span of events in the current case.
*
* @param timeRange The Interval to view.
*
* @return True if the interval was changed. False if the interval was the
* same as the existing one and no change happened.
*/
synchronized public boolean pushTimeRange(Interval timeRange) {
//clamp timerange to case
Interval clampedTimeRange;
if (timeRange == null) {
clampedTimeRange = this.filteredEvents.getSpanningInterval();
} else {
Interval spanningInterval = this.filteredEvents.getSpanningInterval();
if (spanningInterval.overlaps(timeRange)) {
clampedTimeRange = spanningInterval.overlap(timeRange);
} else {
clampedTimeRange = spanningInterval;
}
}
ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
if (currentZoom == null) {
advance(InitialZoomState.withTimeRange(clampedTimeRange));
return true;
} else if (currentZoom.hasTimeRange(clampedTimeRange) == false) {
advance(currentZoom.withTimeRange(clampedTimeRange));
return true;
} else {
return false;
}
}
/**
* Change the view by setting a new time range that is the length of
* timeUnit and centered at the current center.
*
* @param timeUnit The unit of time to view
*
* @return true if the view actually changed.
*/
synchronized public boolean pushTimeUnit(TimeUnits timeUnit) {
if (timeUnit == TimeUnits.FOREVER) {
return showFullRange();
} else {
return pushTimeRange(IntervalUtils.getIntervalAroundMiddle(filteredEvents.getTimeRange(), timeUnit.getPeriod()));
}
}
synchronized public void pushDescrLOD(DescriptionLoD newLOD) {
ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
if (currentZoom == null) {
advance(InitialZoomState.withDescrLOD(newLOD));
} else if (currentZoom.hasDescrLOD(newLOD) == false) {
advance(currentZoom.withDescrLOD(newLOD));
}
}
@SuppressWarnings("AssignmentToMethodParameter") //clamp timerange to case
synchronized public void pushTimeAndType(Interval timeRange, EventTypeZoomLevel typeZoom) {
timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange);
ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
if (currentZoom == null) {
advance(InitialZoomState.withTimeAndType(timeRange, typeZoom));
} else if (currentZoom.hasTimeRange(timeRange) == false && currentZoom.hasTypeZoomLevel(typeZoom) == false) {
advance(currentZoom.withTimeAndType(timeRange, typeZoom));
} else if (currentZoom.hasTimeRange(timeRange) == false) {
advance(currentZoom.withTimeRange(timeRange));
} else if (currentZoom.hasTypeZoomLevel(typeZoom) == false) {
advance(currentZoom.withTypeZoomLevel(typeZoom));
}
}
synchronized public void pushFilters(RootFilter filter) {
ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get();
if (currentZoom == null) {
advance(InitialZoomState.withFilter(filter.copyOf()));
} else if (currentZoom.hasFilter(filter) == false) {
advance(currentZoom.withFilter(filter.copyOf()));
}
}
synchronized public void advance() {
historyManager.advance();
}
synchronized public void retreat() {
historyManager.retreat();
}
synchronized private void advance(ZoomParams newState) {
historyManager.advance(newState);
}
/**
* Select the given event IDs and set their spanning interval as the
* selected time range.
*
* @param eventIDs The eventIDs to select
*/
synchronized public void selectEventIDs(Collection<Long> eventIDs) {
selectedTimeRange.set(filteredEvents.getSpanningInterval(eventIDs));
selectedEventIDs.setAll(eventIDs);
}
public void selectTimeAndType(Interval interval, EventType type) {
final Interval timeRange = filteredEvents.getSpanningInterval().overlap(interval);
final LoggedTask<Collection<Long>> selectTimeAndTypeTask = new LoggedTask<Collection<Long>>("Select Time and Type", true) { //NON-NLS
@Override
protected Collection< Long> call() throws Exception {
synchronized (TimeLineController.this) {
return filteredEvents.getEventIDs(timeRange, new TypeFilter(type));
}
}
@Override
protected void succeeded() {
super.succeeded();
try {
synchronized (TimeLineController.this) {
selectedTimeRange.set(timeRange);
selectedEventIDs.setAll(get());
}
} catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.SEVERE, getTitle() + " Unexpected error", ex); //NON-NLS
}
}
};
monitorTask(selectTimeAndTypeTask);
}
/**
* submit a task for execution and add it to the list of tasks whose
* progress is monitored and displayed in the progress bar
*
* @param task
*/
synchronized public void monitorTask(final Task<?> task) {
//TODO: refactor this to use JavaFX Service? -jm
if (task != null) {
Platform.runLater(() -> {
//is this actually threadsafe, could we get a finished task stuck in the list?
task.stateProperty().addListener((Observable observable) -> {
switch (task.getState()) {
case READY:
case RUNNING:
case SCHEDULED:
break;
case SUCCEEDED:
case CANCELLED:
case FAILED:
tasks.remove(task);
if (tasks.isEmpty() == false) {
taskProgress.bind(tasks.get(0).progressProperty());
taskMessage.bind(tasks.get(0).messageProperty());
taskTitle.bind(tasks.get(0).titleProperty());
}
break;
}
});
tasks.add(task);
taskProgress.bind(task.progressProperty());
taskMessage.bind(task.messageProperty());
taskTitle.bind(task.titleProperty());
switch (task.getState()) {
case READY:
executor.submit(task);
break;
case SCHEDULED:
case RUNNING:
case SUCCEEDED:
case CANCELLED:
case FAILED:
tasks.remove(task);
if (tasks.isEmpty() == false) {
taskProgress.bind(tasks.get(0).progressProperty());
taskMessage.bind(tasks.get(0).messageProperty());
taskTitle.bind(tasks.get(0).titleProperty());
}
break;
}
});
}
}
/**
* Register the given object to receive events.
*
* @param o The object to register. Must implement public methods annotated
* with Subscribe.
*/
synchronized public void registerForEvents(Object o) {
eventbus.register(o);
}
/**
* Un-register the given object, so it no longer receives events.
*
* @param o The object to un-register.
*/
synchronized public void unRegisterForEvents(Object o) {
eventbus.unregister(0);
}
static synchronized public void setTimeZone(TimeZone timeZone) {
TimeLineController.timeZone.set(timeZone);
}
/**
* Listener for IngestManager.IngestModuleEvents.
*/
@Immutable
private class AutopsyIngestModuleListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
/**
* Checking for a current case is a stop gap measure until a
* different way of handling the closing of cases is worked out.
* Currently, remote events may be received for a case that is
* already closed.
*/
try {
Case.getCurrentCase();
} catch (IllegalStateException notUsed) {
// Case is closed, do nothing.
return;
}
switch (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName())) {
case CONTENT_CHANGED:
case DATA_ADDED:
//since black board artifacts or new derived content have been added, the DB is stale.
Platform.runLater(() -> setEventsDBStale(true));
break;
case FILE_DONE:
/*
* Do nothing, since we have captured all new results in
* CONTENT_CHANGED and DATA_ADDED or the IngestJob listener,
*/
break;
}
}
}
/**
* Listener for IngestManager.IngestJobEvents
*/
@Immutable
private class AutopsyIngestJobListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
switch (IngestManager.IngestJobEvent.valueOf(evt.getPropertyName())) {
case DATA_SOURCE_ANALYSIS_COMPLETED:
//mark db stale, and prompt to rebuild
Platform.runLater(() -> setEventsDBStale(true));
filteredEvents.postAutopsyEventLocally((AutopsyEvent) evt);
break;
case DATA_SOURCE_ANALYSIS_STARTED:
case CANCELLED:
case COMPLETED:
case STARTED:
break;
}
}
}
/**
* Listener for Case.Events
*/
@Immutable
private class AutopsyCaseListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
switch (Case.Events.valueOf(evt.getPropertyName())) {
case BLACKBOARD_ARTIFACT_TAG_ADDED:
executor.submit(() -> filteredEvents.handleArtifactTagAdded((BlackBoardArtifactTagAddedEvent) evt));
break;
case BLACKBOARD_ARTIFACT_TAG_DELETED:
executor.submit(() -> filteredEvents.handleArtifactTagDeleted((BlackBoardArtifactTagDeletedEvent) evt));
break;
case CONTENT_TAG_ADDED:
executor.submit(() -> filteredEvents.handleContentTagAdded((ContentTagAddedEvent) evt));
break;
case CONTENT_TAG_DELETED:
executor.submit(() -> filteredEvents.handleContentTagDeleted((ContentTagDeletedEvent) evt));
break;
case DATA_SOURCE_ADDED:
//mark db stale, and prompt to rebuild
Platform.runLater(() -> setEventsDBStale(true));
filteredEvents.postAutopsyEventLocally((AutopsyEvent) evt);
break;
case CURRENT_CASE:
//close timeline on case changes.
SwingUtilities.invokeLater(TimeLineController.this::shutDownTimeLine);
break;
}
}
}
}