mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-17 18:17:43 +00:00
Merge pull request #4436 from millmanorama/1151-don't_store_redundant_description_levels
1151 don't store redundant description levels
This commit is contained in:
commit
ac6741aa50
@ -22,7 +22,6 @@ import com.google.common.cache.CacheBuilder;
|
|||||||
import com.google.common.cache.LoadingCache;
|
import com.google.common.cache.LoadingCache;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.eventbus.EventBus;
|
import com.google.common.eventbus.EventBus;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@ -40,7 +39,6 @@ import javafx.collections.ObservableList;
|
|||||||
import javafx.collections.ObservableMap;
|
import javafx.collections.ObservableMap;
|
||||||
import javafx.collections.ObservableSet;
|
import javafx.collections.ObservableSet;
|
||||||
import static org.apache.commons.collections4.CollectionUtils.emptyIfNull;
|
import static org.apache.commons.collections4.CollectionUtils.emptyIfNull;
|
||||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
|
||||||
import org.joda.time.DateTimeZone;
|
import org.joda.time.DateTimeZone;
|
||||||
import org.joda.time.Interval;
|
import org.joda.time.Interval;
|
||||||
import org.openide.util.NbBundle;
|
import org.openide.util.NbBundle;
|
||||||
@ -61,7 +59,6 @@ import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.FilterState;
|
|||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.TagsFilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.TagsFilterState;
|
||||||
import org.sleuthkit.autopsy.timeline.utils.CacheLoaderImpl;
|
import org.sleuthkit.autopsy.timeline.utils.CacheLoaderImpl;
|
||||||
import org.sleuthkit.autopsy.timeline.utils.CheckedFunction;
|
|
||||||
import org.sleuthkit.autopsy.timeline.utils.FilterUtils;
|
import org.sleuthkit.autopsy.timeline.utils.FilterUtils;
|
||||||
import org.sleuthkit.autopsy.timeline.zooming.ZoomState;
|
import org.sleuthkit.autopsy.timeline.zooming.ZoomState;
|
||||||
import org.sleuthkit.datamodel.AbstractFile;
|
import org.sleuthkit.datamodel.AbstractFile;
|
||||||
@ -404,16 +401,16 @@ public final class FilteredEventsModel {
|
|||||||
return eventManager.getTagCountsByTagName(eventIDsWithTags);
|
return eventManager.getTagCountsByTagName(eventIDsWithTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Long> getEventIDs(Interval timeRange, TimelineFilter filter) throws TskCoreException {
|
public List<Long> getEventIDs(Interval timeRange, FilterState<? extends TimelineFilter> filter) throws TskCoreException {
|
||||||
|
|
||||||
final Interval overlap;
|
final Interval overlap;
|
||||||
RootFilterState intersection;
|
RootFilter intersection;
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
overlap = getSpanningInterval().overlap(timeRange);
|
overlap = getSpanningInterval().overlap(timeRange);
|
||||||
intersection = getFilterState().intersect(filter);
|
intersection = getFilterState().intersect(filter).getActiveFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
return eventManager.getEventIDs(overlap, intersection.getActiveFilter());
|
return eventManager.getEventIDs(overlap, intersection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -698,33 +695,4 @@ public final class FilteredEventsModel {
|
|||||||
private CacheInvalidatedEvent() {
|
private CacheInvalidatedEvent() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* take the result of a group_concat SQLite operation and split it into a
|
|
||||||
* set of X using the mapper to to convert from string to X If groupConcat
|
|
||||||
* is empty, null, or all whitespace, returns an empty list.
|
|
||||||
*
|
|
||||||
* @param <X> the type of elements to return
|
|
||||||
* @param groupConcat a string containing the group_concat result ( a comma
|
|
||||||
* separated list)
|
|
||||||
* @param mapper a function from String to X
|
|
||||||
*
|
|
||||||
* @return a Set of X, each element mapped from one element of the original
|
|
||||||
* comma delimited string
|
|
||||||
*
|
|
||||||
* @throws org.sleuthkit.datamodel.TskCoreException
|
|
||||||
*/
|
|
||||||
public static <X> List<X> unGroupConcat(String groupConcat, CheckedFunction<String, X, TskCoreException> mapper) throws TskCoreException {
|
|
||||||
|
|
||||||
if (isBlank(groupConcat)) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<X> result = new ArrayList<>();
|
|
||||||
String[] split = groupConcat.split(",");
|
|
||||||
for (String s : split) {
|
|
||||||
result.add(mapper.apply(s));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,8 @@ import org.sleuthkit.autopsy.ingest.ModuleDataEvent;
|
|||||||
import org.sleuthkit.autopsy.timeline.events.TimelineEventAddedEvent;
|
import org.sleuthkit.autopsy.timeline.events.TimelineEventAddedEvent;
|
||||||
import org.sleuthkit.autopsy.timeline.events.ViewInTimelineRequestedEvent;
|
import org.sleuthkit.autopsy.timeline.events.ViewInTimelineRequestedEvent;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.DetailViewEvent;
|
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.DetailViewEvent;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.FilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DefaultFilterState;
|
||||||
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DescriptionFilterState;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
|
||||||
import org.sleuthkit.autopsy.timeline.utils.IntervalUtils;
|
import org.sleuthkit.autopsy.timeline.utils.IntervalUtils;
|
||||||
import org.sleuthkit.autopsy.timeline.zooming.TimeUnits;
|
import org.sleuthkit.autopsy.timeline.zooming.TimeUnits;
|
||||||
@ -90,7 +91,6 @@ import org.sleuthkit.datamodel.DescriptionLoD;
|
|||||||
import org.sleuthkit.datamodel.TskCoreException;
|
import org.sleuthkit.datamodel.TskCoreException;
|
||||||
import org.sleuthkit.datamodel.timeline.EventType;
|
import org.sleuthkit.datamodel.timeline.EventType;
|
||||||
import org.sleuthkit.datamodel.timeline.EventTypeZoomLevel;
|
import org.sleuthkit.datamodel.timeline.EventTypeZoomLevel;
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter.DescriptionFilter;
|
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter.EventTypeFilter;
|
import org.sleuthkit.datamodel.timeline.TimelineFilter.EventTypeFilter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,9 +157,9 @@ public class TimeLineController {
|
|||||||
private final Case autoCase;
|
private final Case autoCase;
|
||||||
|
|
||||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||||
private final ObservableList<FilterState<DescriptionFilter>> quickHideFilters = FXCollections.observableArrayList();
|
private final ObservableList<DescriptionFilterState> quickHideFilters = FXCollections.observableArrayList();
|
||||||
|
|
||||||
public ObservableList<FilterState<DescriptionFilter>> getQuickHideFilters() {
|
public ObservableList<DescriptionFilterState> getQuickHideFilters() {
|
||||||
return quickHideFilters;
|
return quickHideFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,7 +607,7 @@ public class TimeLineController {
|
|||||||
@Override
|
@Override
|
||||||
protected Collection< Long> call() throws Exception {
|
protected Collection< Long> call() throws Exception {
|
||||||
synchronized (TimeLineController.this) {
|
synchronized (TimeLineController.this) {
|
||||||
return filteredEvents.getEventIDs(timeRange, new EventTypeFilter(type));
|
return filteredEvents.getEventIDs(timeRange, new DefaultFilterState<>(new EventTypeFilter(type), true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
Core/src/org/sleuthkit/autopsy/timeline/images/action_log.png
Normal file
BIN
Core/src/org/sleuthkit/autopsy/timeline/images/action_log.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 674 B |
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
Core/src/org/sleuthkit/autopsy/timeline/images/registry.png
Normal file
BIN
Core/src/org/sleuthkit/autopsy/timeline/images/registry.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 431 B |
@ -85,6 +85,10 @@ final public class EventTypeUtils {
|
|||||||
imageFileName = "message.png";
|
imageFileName = "message.png";
|
||||||
} else if (typeID == EventType.RECENT_DOCUMENTS.getTypeID()) {
|
} else if (typeID == EventType.RECENT_DOCUMENTS.getTypeID()) {
|
||||||
imageFileName = "recent_docs.png";
|
imageFileName = "recent_docs.png";
|
||||||
|
} else if (typeID == EventType.REGISTRY.getTypeID()) {
|
||||||
|
imageFileName = "registry.png";
|
||||||
|
} else if (typeID == EventType.LOG_ENTRY.getTypeID()) {
|
||||||
|
imageFileName = "raw_access_logs.png";
|
||||||
} else {
|
} else {
|
||||||
imageFileName = "timeline_marker.png";
|
imageFileName = "timeline_marker.png";
|
||||||
}
|
}
|
||||||
|
@ -62,8 +62,8 @@ import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.DetailsViewModel;
|
|||||||
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventStripe;
|
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventStripe;
|
||||||
import org.sleuthkit.autopsy.timeline.utils.MappedList;
|
import org.sleuthkit.autopsy.timeline.utils.MappedList;
|
||||||
import org.sleuthkit.autopsy.timeline.zooming.ZoomState;
|
import org.sleuthkit.autopsy.timeline.zooming.ZoomState;
|
||||||
import org.sleuthkit.datamodel.TskCoreException;
|
|
||||||
import org.sleuthkit.datamodel.DescriptionLoD;
|
import org.sleuthkit.datamodel.DescriptionLoD;
|
||||||
|
import org.sleuthkit.datamodel.TskCoreException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller class for a DetailsChart based implementation of a timeline view.
|
* Controller class for a DetailsChart based implementation of a timeline view.
|
||||||
@ -402,7 +402,7 @@ final public class DetailViewPane extends AbstractTimelineChart<DateTime, EventS
|
|||||||
updateMessage(Bundle.DetailViewPane_loggedTask_queryDb());
|
updateMessage(Bundle.DetailViewPane_loggedTask_queryDb());
|
||||||
|
|
||||||
//get the event stripes to be displayed
|
//get the event stripes to be displayed
|
||||||
List<EventStripe> eventStripes = detailsViewModel.getEventStripes();
|
List<EventStripe> eventStripes = detailsViewModel.getEventStripes(newZoom);
|
||||||
final int size = eventStripes.size();
|
final int size = eventStripes.size();
|
||||||
//if there are too many stipes show a confirmation dialog
|
//if there are too many stipes show a confirmation dialog
|
||||||
if (size > 2000) {
|
if (size > 2000) {
|
||||||
|
@ -57,9 +57,9 @@ import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.DetailViewEvent;
|
|||||||
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventCluster;
|
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventCluster;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventStripe;
|
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventStripe;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.SingleDetailsViewEvent;
|
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.SingleDetailsViewEvent;
|
||||||
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DescriptionFilter;
|
||||||
import org.sleuthkit.datamodel.TskCoreException;
|
import org.sleuthkit.datamodel.TskCoreException;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.FilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.FilterState;
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter.DescriptionFilter;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One "lane" of a the details view, contains all the core logic and layout
|
* One "lane" of a the details view, contains all the core logic and layout
|
||||||
|
@ -55,13 +55,14 @@ import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.DetailViewEvent;
|
|||||||
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventCluster;
|
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventCluster;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventStripe;
|
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventStripe;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.SingleDetailsViewEvent;
|
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.SingleDetailsViewEvent;
|
||||||
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DefaultFilterState;
|
||||||
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DescriptionFilter;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
|
||||||
import org.sleuthkit.autopsy.timeline.zooming.ZoomState;
|
import org.sleuthkit.autopsy.timeline.zooming.ZoomState;
|
||||||
import org.sleuthkit.datamodel.DescriptionLoD;
|
import org.sleuthkit.datamodel.DescriptionLoD;
|
||||||
import org.sleuthkit.datamodel.TskCoreException;
|
import org.sleuthkit.datamodel.TskCoreException;
|
||||||
import org.sleuthkit.datamodel.timeline.EventTypeZoomLevel;
|
import org.sleuthkit.datamodel.timeline.EventTypeZoomLevel;
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineEvent;
|
import org.sleuthkit.datamodel.timeline.TimelineEvent;
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter.DescriptionFilter;
|
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter.EventTypeFilter;
|
import org.sleuthkit.datamodel.timeline.TimelineFilter.EventTypeFilter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -168,7 +169,7 @@ final class EventClusterNode extends MultiEventNodeBase<EventCluster, EventStrip
|
|||||||
getChartLane().setCursor(Cursor.WAIT);
|
getChartLane().setCursor(Cursor.WAIT);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* make new ZoomState to query with
|
* Make new ZoomState to query with:
|
||||||
*
|
*
|
||||||
* We need to extend end time for the query by one second, because it is
|
* We need to extend end time for the query by one second, because it is
|
||||||
* treated as an open interval but we want to include events at exactly
|
* treated as an open interval but we want to include events at exactly
|
||||||
@ -176,14 +177,14 @@ final class EventClusterNode extends MultiEventNodeBase<EventCluster, EventStrip
|
|||||||
* to the type and description of this cluster by intersecting a new
|
* to the type and description of this cluster by intersecting a new
|
||||||
* filter with the existing root filter.
|
* filter with the existing root filter.
|
||||||
*/
|
*/
|
||||||
final RootFilterState subClusterFilter = eventsModel.getFilterState().copyOf();
|
RootFilterState subClusterFilter = eventsModel.getFilterState()
|
||||||
subClusterFilter.getFilter().getSubFilters().addAll(
|
.intersect(new DefaultFilterState<>(
|
||||||
new DescriptionFilter(getEvent().getDescriptionLoD(), getDescription(), DescriptionFilter.FilterMode.INCLUDE),
|
new EventTypeFilter(getEventType()), true));
|
||||||
new EventTypeFilter(getEventType()));
|
|
||||||
final Interval subClusterSpan = new Interval(getStartMillis(), getEndMillis() + 1000);
|
final Interval subClusterSpan = new Interval(getStartMillis(), getEndMillis() + 1000);
|
||||||
final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get();
|
final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.getEventTypeZoom();
|
||||||
final ZoomState zoom = new ZoomState(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD());
|
final ZoomState zoom = new ZoomState(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD());
|
||||||
|
|
||||||
|
DescriptionFilter descriptionFilter = new DescriptionFilter(getEvent().getDescriptionLoD(), getDescription());
|
||||||
/*
|
/*
|
||||||
* task to load sub-stripes in a background thread
|
* task to load sub-stripes in a background thread
|
||||||
*/
|
*/
|
||||||
@ -199,7 +200,6 @@ final class EventClusterNode extends MultiEventNodeBase<EventCluster, EventStrip
|
|||||||
//next LoD in diraction of given relativeDetail
|
//next LoD in diraction of given relativeDetail
|
||||||
DescriptionLoD next = loadedDescriptionLoD;
|
DescriptionLoD next = loadedDescriptionLoD;
|
||||||
do {
|
do {
|
||||||
|
|
||||||
loadedDescriptionLoD = next;
|
loadedDescriptionLoD = next;
|
||||||
if (loadedDescriptionLoD == getEvent().getDescriptionLoD()) {
|
if (loadedDescriptionLoD == getEvent().getDescriptionLoD()) {
|
||||||
//if we are back at the level of detail of the original cluster, return empty list to inidicate.
|
//if we are back at the level of detail of the original cluster, return empty list to inidicate.
|
||||||
@ -207,7 +207,7 @@ final class EventClusterNode extends MultiEventNodeBase<EventCluster, EventStrip
|
|||||||
}
|
}
|
||||||
|
|
||||||
//query for stripes at the desired level of detail
|
//query for stripes at the desired level of detail
|
||||||
stripes = chartLane.getParentChart().getDetailsViewModel().getEventStripes(zoom.withDescrLOD(loadedDescriptionLoD));
|
stripes = chartLane.getParentChart().getDetailsViewModel().getEventStripes(descriptionFilter, zoom.withDescrLOD(loadedDescriptionLoD));
|
||||||
//setup next for subsequent go through the "do" loop
|
//setup next for subsequent go through the "do" loop
|
||||||
next = withRelativeDetail(loadedDescriptionLoD, relativeDetail);
|
next = withRelativeDetail(loadedDescriptionLoD, relativeDetail);
|
||||||
} while (stripes.size() == 1 && nonNull(next)); //keep going while there was only on stripe and we havne't reached the end of the LoD continuum.
|
} while (stripes.size() == 1 && nonNull(next)); //keep going while there was only on stripe and we havne't reached the end of the LoD continuum.
|
||||||
@ -244,6 +244,7 @@ final class EventClusterNode extends MultiEventNodeBase<EventCluster, EventStrip
|
|||||||
}
|
}
|
||||||
} catch (TskCoreException | InterruptedException | ExecutionException ex) {
|
} catch (TskCoreException | InterruptedException | ExecutionException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); //NON-NLS
|
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); //NON-NLS
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getChartLane().requestChartLayout();
|
getChartLane().requestChartLayout();
|
||||||
|
@ -1,7 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* To change this license header, choose License Headers in Project Properties.
|
* Autopsy Forensic Browser
|
||||||
* To change this template file, choose Tools | Templates
|
*
|
||||||
* and open the template in the editor.
|
* Copyright 2015-18 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.ui.detailview;
|
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||||
|
|
||||||
@ -9,11 +22,9 @@ import javafx.scene.image.Image;
|
|||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import org.controlsfx.control.action.Action;
|
import org.controlsfx.control.action.Action;
|
||||||
import org.openide.util.NbBundle;
|
import org.openide.util.NbBundle;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DefaultFilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DescriptionFilter;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.FilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DescriptionFilterState;
|
||||||
import org.sleuthkit.datamodel.DescriptionLoD;
|
import org.sleuthkit.datamodel.DescriptionLoD;
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter.DescriptionFilter;
|
|
||||||
import static org.sleuthkit.datamodel.timeline.TimelineFilter.DescriptionFilter.FilterMode.EXCLUDE;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An Action that hides, in the given chart, events that have the given
|
* An Action that hides, in the given chart, events that have the given
|
||||||
@ -39,12 +50,13 @@ class HideDescriptionAction extends Action {
|
|||||||
* as the new filter for the given description. Set the (new) filter
|
* as the new filter for the given description. Set the (new) filter
|
||||||
* active.
|
* active.
|
||||||
*/
|
*/
|
||||||
final FilterState<DescriptionFilter> testFilter
|
final DescriptionFilterState testFilter
|
||||||
= new DefaultFilterState<>(
|
= new DescriptionFilterState(
|
||||||
new DescriptionFilter(descriptionLoD, description, EXCLUDE));
|
new DescriptionFilter(descriptionLoD, description));
|
||||||
|
|
||||||
FilterState<DescriptionFilter> descriptionFilter = chart.getController().getQuickHideFilters().stream()
|
DescriptionFilterState descriptionFilter = chart.getController().getQuickHideFilters().stream()
|
||||||
.filter(testFilter::equals).findFirst()
|
.filter(otherFilterState -> testFilter.getFilter().equals(otherFilterState.getFilter()))
|
||||||
|
.findFirst()
|
||||||
.orElseGet(() -> {
|
.orElseGet(() -> {
|
||||||
//if the selected state of the filter changes, do chart layout
|
//if the selected state of the filter changes, do chart layout
|
||||||
testFilter.selectedProperty().addListener(selectedProperty -> chart.requestLayout());
|
testFilter.selectedProperty().addListener(selectedProperty -> chart.requestLayout());
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* Autopsy Forensic Browser
|
* Autopsy Forensic Browser
|
||||||
*
|
*
|
||||||
* Copyright 2014-16 Basis Technology Corp.
|
* Copyright 2014-18 Basis Technology Corp.
|
||||||
* Contact: carrier <at> sleuthkit <dot> org
|
* Contact: carrier <at> sleuthkit <dot> org
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@ -22,8 +22,8 @@ import javafx.scene.image.Image;
|
|||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import org.controlsfx.control.action.Action;
|
import org.controlsfx.control.action.Action;
|
||||||
import org.openide.util.NbBundle;
|
import org.openide.util.NbBundle;
|
||||||
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DescriptionFilter;
|
||||||
import org.sleuthkit.datamodel.DescriptionLoD;
|
import org.sleuthkit.datamodel.DescriptionLoD;
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter.DescriptionFilter;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An Action that un-hides, in the given chart, events with the given
|
* An Action that un-hides, in the given chart, events with the given
|
||||||
@ -44,9 +44,9 @@ class UnhideDescriptionAction extends Action {
|
|||||||
* test one and checking all the existing filters against it.
|
* test one and checking all the existing filters against it.
|
||||||
* Disable them.
|
* Disable them.
|
||||||
*/
|
*/
|
||||||
final DescriptionFilter testFilter = new DescriptionFilter(descriptionLoD, description, DescriptionFilter.FilterMode.EXCLUDE);
|
final DescriptionFilter testFilter = new DescriptionFilter(descriptionLoD, description);
|
||||||
chart.getController().getQuickHideFilters().stream()
|
chart.getController().getQuickHideFilters().stream()
|
||||||
.filter(testFilter::equals)
|
.filter(otherFilterState -> testFilter.equals(otherFilterState.getFilter()))
|
||||||
.forEach(descriptionfilter -> descriptionfilter.setSelected(false));
|
.forEach(descriptionfilter -> descriptionfilter.setSelected(false));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.sleuthkit.autopsy.timeline.ui.detailview.datamodel;
|
package org.sleuthkit.autopsy.timeline.ui.detailview.datamodel;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.SortedSet;
|
import java.util.SortedSet;
|
||||||
@ -119,4 +120,12 @@ public interface DetailViewEvent {
|
|||||||
* @return The EventClusters that make up this event.
|
* @return The EventClusters that make up this event.
|
||||||
*/
|
*/
|
||||||
SortedSet<EventCluster> getClusters();
|
SortedSet<EventCluster> getClusters();
|
||||||
|
|
||||||
|
static class StartComparator implements Comparator<DetailViewEvent> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(DetailViewEvent o1, DetailViewEvent o2) {
|
||||||
|
return Long.compare(o1.getStartMillis(), o2.getStartMillis());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,13 +25,12 @@ import com.google.common.collect.SetMultimap;
|
|||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
@ -42,12 +41,11 @@ import org.joda.time.Interval;
|
|||||||
import org.joda.time.Period;
|
import org.joda.time.Period;
|
||||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||||
import org.sleuthkit.autopsy.timeline.FilteredEventsModel;
|
import org.sleuthkit.autopsy.timeline.FilteredEventsModel;
|
||||||
import static org.sleuthkit.autopsy.timeline.FilteredEventsModel.unGroupConcat;
|
|
||||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.UIFilter;
|
||||||
import org.sleuthkit.autopsy.timeline.utils.CacheLoaderImpl;
|
import org.sleuthkit.autopsy.timeline.utils.CacheLoaderImpl;
|
||||||
import org.sleuthkit.autopsy.timeline.utils.RangeDivision;
|
import org.sleuthkit.autopsy.timeline.utils.RangeDivision;
|
||||||
import org.sleuthkit.autopsy.timeline.utils.TimelineDBUtils;
|
import org.sleuthkit.autopsy.timeline.zooming.TimeUnits;
|
||||||
import org.sleuthkit.autopsy.timeline.zooming.ZoomState;
|
import org.sleuthkit.autopsy.timeline.zooming.ZoomState;
|
||||||
import org.sleuthkit.datamodel.DescriptionLoD;
|
import org.sleuthkit.datamodel.DescriptionLoD;
|
||||||
import org.sleuthkit.datamodel.SleuthkitCase;
|
import org.sleuthkit.datamodel.SleuthkitCase;
|
||||||
@ -55,6 +53,8 @@ import org.sleuthkit.datamodel.TimelineManager;
|
|||||||
import org.sleuthkit.datamodel.TskCoreException;
|
import org.sleuthkit.datamodel.TskCoreException;
|
||||||
import org.sleuthkit.datamodel.timeline.EventType;
|
import org.sleuthkit.datamodel.timeline.EventType;
|
||||||
import org.sleuthkit.datamodel.timeline.EventTypeZoomLevel;
|
import org.sleuthkit.datamodel.timeline.EventTypeZoomLevel;
|
||||||
|
import org.sleuthkit.datamodel.timeline.TimelineEvent;
|
||||||
|
import org.sleuthkit.datamodel.timeline.TimelineFilter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model for the Details View. Uses FilteredEventsModel as underlying datamodel
|
* Model for the Details View. Uses FilteredEventsModel as underlying datamodel
|
||||||
@ -65,7 +65,7 @@ final public class DetailsViewModel {
|
|||||||
private final static Logger logger = Logger.getLogger(DetailsViewModel.class.getName());
|
private final static Logger logger = Logger.getLogger(DetailsViewModel.class.getName());
|
||||||
|
|
||||||
private final FilteredEventsModel eventsModel;
|
private final FilteredEventsModel eventsModel;
|
||||||
private final LoadingCache<ZoomState, List<EventStripe>> eventStripeCache;
|
private final LoadingCache<ZoomState, List<TimelineEvent>> eventCache;
|
||||||
private final TimelineManager eventManager;
|
private final TimelineManager eventManager;
|
||||||
private final SleuthkitCase sleuthkitCase;
|
private final SleuthkitCase sleuthkitCase;
|
||||||
|
|
||||||
@ -73,41 +73,21 @@ final public class DetailsViewModel {
|
|||||||
this.eventsModel = eventsModel;
|
this.eventsModel = eventsModel;
|
||||||
this.eventManager = eventsModel.getEventManager();
|
this.eventManager = eventsModel.getEventManager();
|
||||||
this.sleuthkitCase = eventsModel.getSleuthkitCase();
|
this.sleuthkitCase = eventsModel.getSleuthkitCase();
|
||||||
eventStripeCache = CacheBuilder.newBuilder()
|
eventCache = CacheBuilder.newBuilder()
|
||||||
.maximumSize(1000L)
|
.maximumSize(1000L)
|
||||||
.expireAfterAccess(10, TimeUnit.MINUTES)
|
.expireAfterAccess(10, TimeUnit.MINUTES)
|
||||||
.build(new CacheLoaderImpl<>(params -> getEventStripes(params, TimeLineController.getJodaTimeZone())));
|
.build(new CacheLoaderImpl<>(params
|
||||||
|
-> getEvents(params, TimeLineController.getJodaTimeZone())));
|
||||||
eventsModel.registerForEvents(this);
|
eventsModel.registerForEvents(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
void handleCacheInvalidation(FilteredEventsModel.CacheInvalidatedEvent event) {
|
void handleCacheInvalidation(FilteredEventsModel.CacheInvalidatedEvent event) {
|
||||||
eventStripeCache.invalidateAll();
|
eventCache.invalidateAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @param zoom
|
||||||
* @return a list of event clusters at the requested zoom levels that are
|
|
||||||
* within the requested time range and pass the requested filter
|
|
||||||
*
|
|
||||||
* @throws org.sleuthkit.datamodel.TskCoreException
|
|
||||||
*/
|
|
||||||
public List<EventStripe> getEventStripes() throws TskCoreException {
|
|
||||||
final Interval range;
|
|
||||||
final RootFilterState filter;
|
|
||||||
final EventTypeZoomLevel zoom;
|
|
||||||
final DescriptionLoD lod;
|
|
||||||
synchronized (this) {
|
|
||||||
range = eventsModel.getTimeRange();
|
|
||||||
filter = eventsModel.getFilterState();
|
|
||||||
zoom = eventsModel.getEventTypeZoom();
|
|
||||||
lod = eventsModel.getDescriptionLOD();
|
|
||||||
}
|
|
||||||
return getEventStripes(new ZoomState(range, zoom, filter, lod));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param params
|
|
||||||
*
|
*
|
||||||
* @return a list of aggregated events that are within the requested time
|
* @return a list of aggregated events that are within the requested time
|
||||||
* range and pass the requested filter, using the given aggregation
|
* range and pass the requested filter, using the given aggregation
|
||||||
@ -115,11 +95,42 @@ final public class DetailsViewModel {
|
|||||||
*
|
*
|
||||||
* @throws org.sleuthkit.datamodel.TskCoreException
|
* @throws org.sleuthkit.datamodel.TskCoreException
|
||||||
*/
|
*/
|
||||||
public List<EventStripe> getEventStripes(ZoomState params) throws TskCoreException {
|
public List<EventStripe> getEventStripes(ZoomState zoom) throws TskCoreException {
|
||||||
|
return getEventStripes(UIFilter.getAllPassFilter(), zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param zoom
|
||||||
|
*
|
||||||
|
* @return a list of aggregated events that are within the requested time
|
||||||
|
* range and pass the requested filter, using the given aggregation
|
||||||
|
* to control the grouping of events
|
||||||
|
*
|
||||||
|
* @throws org.sleuthkit.datamodel.TskCoreException
|
||||||
|
*/
|
||||||
|
public List<EventStripe> getEventStripes(UIFilter uiFilter, ZoomState zoom) throws TskCoreException {
|
||||||
|
DateTimeZone timeZone = TimeLineController.getJodaTimeZone();
|
||||||
|
//unpack params
|
||||||
|
Interval timeRange = zoom.getTimeRange();
|
||||||
|
DescriptionLoD descriptionLOD = zoom.getDescriptionLOD();
|
||||||
|
EventTypeZoomLevel typeZoomLevel = zoom.getTypeZoomLevel();
|
||||||
|
|
||||||
|
//intermediate results
|
||||||
|
Map<EventType, SetMultimap< String, EventCluster>> eventClusters = new HashMap<>();
|
||||||
try {
|
try {
|
||||||
return eventStripeCache.get(params);
|
eventCache.get(zoom).stream()
|
||||||
|
.filter(uiFilter)
|
||||||
|
.forEach(event -> {
|
||||||
|
EventType clusterType = event.getEventType(typeZoomLevel);
|
||||||
|
eventClusters.computeIfAbsent(clusterType, eventType -> HashMultimap.create())
|
||||||
|
.put(event.getDescription(descriptionLOD), new EventCluster(event, clusterType, descriptionLOD));
|
||||||
|
});
|
||||||
|
//get some info about the time range requested
|
||||||
|
TimeUnits periodSize = RangeDivision.getRangeDivision(timeRange, timeZone).getPeriodSize();
|
||||||
|
return mergeClustersToStripes(periodSize.toUnitPeriod(), eventClusters);
|
||||||
|
|
||||||
} catch (ExecutionException ex) {
|
} catch (ExecutionException ex) {
|
||||||
throw new TskCoreException("Failed to load Event Stripes from cache for " + params.toString(), ex); //NON-NLS
|
throw new TskCoreException("Failed to load Event Stripes from cache for " + zoom.toString(), ex); //NON-NLS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,12 +149,10 @@ final public class DetailsViewModel {
|
|||||||
* @throws org.sleuthkit.datamodel.TskCoreException If there is an error
|
* @throws org.sleuthkit.datamodel.TskCoreException If there is an error
|
||||||
* querying the db.
|
* querying the db.
|
||||||
*/
|
*/
|
||||||
public List<EventStripe> getEventStripes(ZoomState zoom, DateTimeZone timeZone) throws TskCoreException {
|
List<TimelineEvent> getEvents(ZoomState zoom, DateTimeZone timeZone) throws TskCoreException {
|
||||||
//unpack params
|
//unpack params
|
||||||
Interval timeRange = zoom.getTimeRange();
|
Interval timeRange = zoom.getTimeRange();
|
||||||
RootFilterState filterState = zoom.getFilterState();
|
TimelineFilter.RootFilter activeFilter = zoom.getFilterState().getActiveFilter();
|
||||||
DescriptionLoD descriptionLOD = zoom.getDescriptionLOD();
|
|
||||||
EventTypeZoomLevel typeZoomLevel = zoom.getTypeZoomLevel();
|
|
||||||
|
|
||||||
long start = timeRange.getStartMillis() / 1000;
|
long start = timeRange.getStartMillis() / 1000;
|
||||||
long end = timeRange.getEndMillis() / 1000;
|
long end = timeRange.getEndMillis() / 1000;
|
||||||
@ -151,101 +160,98 @@ final public class DetailsViewModel {
|
|||||||
//ensure length of querried interval is not 0
|
//ensure length of querried interval is not 0
|
||||||
end = Math.max(end, start + 1);
|
end = Math.max(end, start + 1);
|
||||||
|
|
||||||
//get some info about the time range requested
|
|
||||||
RangeDivision rangeInfo = RangeDivision.getRangeDivision(timeRange, timeZone);
|
|
||||||
|
|
||||||
//build dynamic parts of query
|
//build dynamic parts of query
|
||||||
String descriptionColumn = eventManager.getDescriptionColumn(descriptionLOD);
|
String querySql = "SELECT time, file_obj_id, data_source_obj_id, artifact_id, " // NON-NLS
|
||||||
final boolean useSubTypes = typeZoomLevel.equals(EventTypeZoomLevel.SUB_TYPE);
|
+ " event_id, " //NON-NLS
|
||||||
String typeColumn = TimelineManager.typeColumnHelper(useSubTypes);
|
+ " hash_hit, " //NON-NLS
|
||||||
|
+ " tagged, " //NON-NLS
|
||||||
|
+ " sub_type, base_type, "
|
||||||
|
+ " full_description, med_description, short_description " // NON-NLS
|
||||||
|
+ " FROM " + TimelineManager.getAugmentedEventsTablesSQL(activeFilter) // NON-NLS
|
||||||
|
+ " WHERE time >= " + start + " AND time < " + end + " AND " + eventManager.getSQLWhere(activeFilter) // NON-NLS
|
||||||
|
+ " ORDER BY time"; // NON-NLS
|
||||||
|
|
||||||
TimelineDBUtils dbUtils = new TimelineDBUtils(sleuthkitCase);
|
List<TimelineEvent> events = new ArrayList<>();
|
||||||
String querySql = "SELECT " + formatTimeFunctionHelper(rangeInfo.getPeriodSize().toChronoUnit(), timeZone) + " AS interval, " // NON-NLS
|
|
||||||
+ dbUtils.csvAggFunction("tsk_events.event_id") + " as event_ids, " //NON-NLS
|
|
||||||
+ dbUtils.csvAggFunction("CASE WHEN hash_hit = 1 THEN tsk_events.event_id ELSE NULL END") + " as hash_hits, " //NON-NLS
|
|
||||||
+ dbUtils.csvAggFunction("CASE WHEN tagged = 1 THEN tsk_events.event_id ELSE NULL END") + " as taggeds, " //NON-NLS
|
|
||||||
+ " min(time) AS minTime, max(time) AS maxTime, " + typeColumn + ", " + descriptionColumn // NON-NLS
|
|
||||||
+ " FROM " + TimelineManager.getAugmentedEventsTablesSQL(filterState.getActiveFilter()) // NON-NLS
|
|
||||||
+ " WHERE time >= " + start + " AND time < " + end + " AND " + eventManager.getSQLWhere(filterState.getActiveFilter()) // NON-NLS
|
|
||||||
+ " GROUP BY interval, " + typeColumn + " , " + descriptionColumn // NON-NLS
|
|
||||||
+ " ORDER BY min(time)"; // NON-NLS
|
|
||||||
|
|
||||||
// perform query and map results to AggregateEvent objects
|
|
||||||
List<EventCluster> events = new ArrayList<>();
|
|
||||||
|
|
||||||
try (SleuthkitCase.CaseDbQuery dbQuery = sleuthkitCase.executeQuery(querySql);
|
try (SleuthkitCase.CaseDbQuery dbQuery = sleuthkitCase.executeQuery(querySql);
|
||||||
ResultSet resultSet = dbQuery.getResultSet();) {
|
ResultSet resultSet = dbQuery.getResultSet();) {
|
||||||
while (resultSet.next()) {
|
while (resultSet.next()) {
|
||||||
events.add(eventClusterHelper(resultSet, typeColumn, descriptionLOD, timeZone));
|
events.add(eventHelper(resultSet));
|
||||||
}
|
}
|
||||||
|
} catch (TskCoreException ex) {
|
||||||
|
logger.log(Level.SEVERE, "Failed to get events with query: " + querySql, ex); // NON-NLS
|
||||||
|
throw ex;
|
||||||
} catch (SQLException ex) {
|
} catch (SQLException ex) {
|
||||||
logger.log(Level.SEVERE, "Failed to get events with query: " + querySql, ex); // NON-NLS
|
logger.log(Level.SEVERE, "Failed to get events with query: " + querySql, ex); // NON-NLS
|
||||||
|
throw new TskCoreException("Failed to get events with query: " + querySql, ex);
|
||||||
}
|
}
|
||||||
|
return events;
|
||||||
return mergeClustersToStripes(rangeInfo.getPeriodSize().toUnitPeriod(), events);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* map a single row in a ResultSet to an EventCluster
|
* Map a single row in a ResultSet to an EventCluster
|
||||||
*
|
*
|
||||||
* @param resultSet the result set whose current row should be mapped
|
* @param resultSet the result set whose current row should be mapped
|
||||||
* @param useSubTypes use the sub_type column if true, else use the
|
* @param typeColumn The type column (sub_type or base_type) to use as
|
||||||
* base_type column
|
* the type of the event cluster
|
||||||
* @param descriptionLOD the description level of detail for this event
|
* @param descriptionLOD the description level of detail for this event
|
||||||
* @param filter
|
* cluster
|
||||||
*
|
*
|
||||||
* @return an AggregateEvent corresponding to the current row in the given
|
* @return an EventCluster corresponding to the current row in the given
|
||||||
* result set
|
* result set
|
||||||
*
|
*
|
||||||
* @throws SQLException
|
* @throws SQLException
|
||||||
*/
|
*/
|
||||||
private EventCluster eventClusterHelper(ResultSet resultSet, String typeColumn, DescriptionLoD descriptionLOD, DateTimeZone timeZone) throws SQLException, TskCoreException {
|
private TimelineEvent eventHelper(ResultSet resultSet) throws SQLException, TskCoreException {
|
||||||
Interval interval = new Interval(resultSet.getLong("minTime") * 1000, resultSet.getLong("maxTime") * 1000, timeZone);
|
|
||||||
|
|
||||||
List<Long> eventIDs = unGroupConcat(resultSet.getString("event_ids"), Long::valueOf); // NON-NLS
|
//the event tyepe to use to get the description.
|
||||||
String description = resultSet.getString(eventManager.getDescriptionColumn(descriptionLOD));
|
int eventTypeID = resultSet.getInt("sub_type");
|
||||||
int eventTypeID = resultSet.getInt(typeColumn);
|
|
||||||
EventType eventType = eventManager.getEventType(eventTypeID).orElseThrow(()
|
EventType eventType = eventManager.getEventType(eventTypeID).orElseThrow(()
|
||||||
-> new TskCoreException("Error mapping event type id " + eventTypeID + "to EventType."));//NON-NLS
|
-> new TskCoreException("Error mapping event type id " + eventTypeID + "to EventType."));//NON-NLS
|
||||||
|
|
||||||
List<Long> hashHits = unGroupConcat(resultSet.getString("hash_hits"), Long::valueOf); //NON-NLS
|
return new TimelineEvent(
|
||||||
List<Long> tagged = unGroupConcat(resultSet.getString("taggeds"), Long::valueOf); //NON-NLS
|
resultSet.getLong("event_id"), // NON-NLS
|
||||||
|
resultSet.getLong("data_source_obj_id"), // NON-NLS
|
||||||
return new EventCluster(interval, eventType, eventIDs, hashHits, tagged, description, descriptionLOD);
|
resultSet.getLong("file_obj_id"), // NON-NLS
|
||||||
|
resultSet.getLong("artifact_id"), // NON-NLS
|
||||||
|
resultSet.getLong("time"), // NON-NLS
|
||||||
|
eventType,
|
||||||
|
eventType.getDescription(
|
||||||
|
resultSet.getString("full_description"), // NON-NLS
|
||||||
|
resultSet.getString("med_description"), // NON-NLS
|
||||||
|
resultSet.getString("short_description")), // NON-NLS
|
||||||
|
resultSet.getInt("hash_hit") != 0, //NON-NLS
|
||||||
|
resultSet.getInt("tagged") != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* merge the events in the given list if they are within the same period
|
* Merge the events in the given list if they are within the same period
|
||||||
* General algorithm is as follows:
|
* General algorithm is as follows:
|
||||||
*
|
*
|
||||||
* 1) sort them into a map from (type, description)-> List<aggevent>
|
* 1) sort them into a map from (type, description)-> List<EventCluster>
|
||||||
* 2) for each key in map, merge the events and accumulate them in a list to
|
* 2) for each key in map, merge the events and accumulate them in a list to
|
||||||
* return
|
* return
|
||||||
*
|
*
|
||||||
* @param timeUnitLength
|
* @param timeUnitLength
|
||||||
* @param preMergedEvents
|
* @param eventClusters
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
static private List<EventStripe> mergeClustersToStripes(Period timeUnitLength, List<EventCluster> preMergedEvents) {
|
static private List<EventStripe> mergeClustersToStripes(Period timeUnitLength, Map<EventType, SetMultimap< String, EventCluster>> eventClusters) {
|
||||||
|
|
||||||
//effectively map from type to (map from description to events)
|
|
||||||
Map<EventType, SetMultimap< String, EventCluster>> typeMap = new HashMap<>();
|
|
||||||
|
|
||||||
for (EventCluster aggregateEvent : preMergedEvents) {
|
|
||||||
typeMap.computeIfAbsent(aggregateEvent.getEventType(), eventType -> HashMultimap.create())
|
|
||||||
.put(aggregateEvent.getDescription(), aggregateEvent);
|
|
||||||
}
|
|
||||||
//result list to return
|
//result list to return
|
||||||
ArrayList<EventCluster> aggEvents = new ArrayList<>();
|
ArrayList<EventCluster> mergedClusters = new ArrayList<>();
|
||||||
|
|
||||||
//For each (type, description) key, merge agg events
|
//For each (type, description) key, merge agg events
|
||||||
for (SetMultimap<String, EventCluster> descrMap : typeMap.values()) {
|
for (Map.Entry<EventType, SetMultimap<String, EventCluster>> typeMapEntry : eventClusters.entrySet()) {
|
||||||
|
EventType type = typeMapEntry.getKey();
|
||||||
|
SetMultimap<String, EventCluster> descrMap = typeMapEntry.getValue();
|
||||||
//for each description ...
|
//for each description ...
|
||||||
for (String descr : descrMap.keySet()) {
|
for (String descr : descrMap.keySet()) {
|
||||||
|
Set<EventCluster> events = descrMap.get(descr);
|
||||||
//run through the sorted events, merging together adjacent events
|
//run through the sorted events, merging together adjacent events
|
||||||
Iterator<EventCluster> iterator = descrMap.get(descr).stream()
|
Iterator<EventCluster> iterator = events.stream()
|
||||||
.sorted(Comparator.comparing(event -> event.getSpan().getStartMillis()))
|
.sorted(new DetailViewEvent.StartComparator())
|
||||||
.iterator();
|
.iterator();
|
||||||
EventCluster current = iterator.next();
|
EventCluster current = iterator.next();
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
@ -259,106 +265,25 @@ final public class DetailsViewModel {
|
|||||||
current = EventCluster.merge(current, next);
|
current = EventCluster.merge(current, next);
|
||||||
} else {
|
} else {
|
||||||
//done merging into current, set next as new current
|
//done merging into current, set next as new current
|
||||||
aggEvents.add(current);
|
mergedClusters.add(current);
|
||||||
current = next;
|
current = next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
aggEvents.add(current);
|
mergedClusters.add(current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//merge clusters to stripes
|
//merge clusters to stripes
|
||||||
Map<ImmutablePair<EventType, String>, EventStripe> stripeDescMap = new HashMap<>();
|
Map<ImmutablePair<EventType, String>, EventStripe> stripeDescMap = new HashMap<>();
|
||||||
|
|
||||||
for (EventCluster eventCluster : aggEvents) {
|
for (EventCluster eventCluster : mergedClusters) {
|
||||||
stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()),
|
stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()),
|
||||||
new EventStripe(eventCluster), EventStripe::merge);
|
new EventStripe(eventCluster), EventStripe::merge);
|
||||||
}
|
}
|
||||||
|
|
||||||
return stripeDescMap.values().stream().sorted(Comparator.comparing(EventStripe::getStartMillis)).collect(Collectors.toList());
|
return stripeDescMap.values().stream()
|
||||||
|
.sorted(new DetailViewEvent.StartComparator())
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a column specification that will allow us to group by the requested
|
|
||||||
* period size. That is, with all info more granular than that requested
|
|
||||||
* dropped (replaced with zeros). For use in the select clause of a sql
|
|
||||||
* query.
|
|
||||||
*
|
|
||||||
* @param periodSize The ChronoUnit describing what granularity to use.
|
|
||||||
* @param timeZone
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private String formatTimeFunctionHelper(ChronoUnit periodSize, DateTimeZone timeZone) {
|
|
||||||
switch (sleuthkitCase.getDatabaseType()) {
|
|
||||||
case SQLITE:
|
|
||||||
String strfTimeFormat = getSQLIteTimeFormat(periodSize);
|
|
||||||
String useLocalTime = timeZone.equals(DateTimeZone.getDefault()) ? ", 'localtime'" : ""; // NON-NLS
|
|
||||||
return "strftime('" + strfTimeFormat + "', time , 'unixepoch'" + useLocalTime + ")";
|
|
||||||
case POSTGRESQL:
|
|
||||||
String formatString = getPostgresTimeFormat(periodSize);
|
|
||||||
return "to_char(to_timestamp(time) AT TIME ZONE '" + timeZone.getID() + "', '" + formatString + "')";
|
|
||||||
default:
|
|
||||||
throw new UnsupportedOperationException("Unsupported DB type: " + sleuthkitCase.getDatabaseType().name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Get a format string that will allow us to group by the requested period
|
|
||||||
* size. That is, with all info more granular than that requested dropped
|
|
||||||
* (replaced with zeros).
|
|
||||||
*
|
|
||||||
* @param timeUnit The ChronoUnit describing what granularity to build a
|
|
||||||
* strftime string for
|
|
||||||
*
|
|
||||||
* @return a String formatted according to the sqlite strftime spec
|
|
||||||
*
|
|
||||||
* @see https://www.sqlite.org/lang_datefunc.html
|
|
||||||
*/
|
|
||||||
private static String getSQLIteTimeFormat(ChronoUnit timeUnit) {
|
|
||||||
switch (timeUnit) {
|
|
||||||
case YEARS:
|
|
||||||
return "%Y-01-01T00:00:00"; // NON-NLS
|
|
||||||
case MONTHS:
|
|
||||||
return "%Y-%m-01T00:00:00"; // NON-NLS
|
|
||||||
case DAYS:
|
|
||||||
return "%Y-%m-%dT00:00:00"; // NON-NLS
|
|
||||||
case HOURS:
|
|
||||||
return "%Y-%m-%dT%H:00:00"; // NON-NLS
|
|
||||||
case MINUTES:
|
|
||||||
return "%Y-%m-%dT%H:%M:00"; // NON-NLS
|
|
||||||
case SECONDS:
|
|
||||||
default: //seconds - should never happen
|
|
||||||
return "%Y-%m-%dT%H:%M:%S"; // NON-NLS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a format string that will allow us to group by the requested period
|
|
||||||
* size. That is, with all info more granular than that requested dropped
|
|
||||||
* (replaced with zeros).
|
|
||||||
*
|
|
||||||
* @param timeUnit The ChronoUnit describing what granularity to build a
|
|
||||||
* strftime string for
|
|
||||||
*
|
|
||||||
* @return a String formatted according to the Postgres
|
|
||||||
* to_char(to_timestamp(time) ... ) spec
|
|
||||||
*/
|
|
||||||
private static String getPostgresTimeFormat(ChronoUnit timeUnit) {
|
|
||||||
switch (timeUnit) {
|
|
||||||
case YEARS:
|
|
||||||
return "YYYY-01-01T00:00:00"; // NON-NLS
|
|
||||||
case MONTHS:
|
|
||||||
return "YYYY-MM-01T00:00:00"; // NON-NLS
|
|
||||||
case DAYS:
|
|
||||||
return "YYYY-MM-DDT00:00:00"; // NON-NLS
|
|
||||||
case HOURS:
|
|
||||||
return "YYYY-MM-DDTHH24:00:00"; // NON-NLS
|
|
||||||
case MINUTES:
|
|
||||||
return "YYYY-MM-DDTHH24:MI:00"; // NON-NLS
|
|
||||||
case SECONDS:
|
|
||||||
default: //seconds - should never happen
|
|
||||||
return "YYYY-MM-DDTHH24:MI:SS"; // NON-NLS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -20,16 +20,20 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.datamodel;
|
|||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.ImmutableSortedSet;
|
import com.google.common.collect.ImmutableSortedSet;
|
||||||
import com.google.common.collect.Sets;
|
import static com.google.common.collect.Sets.union;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import static java.util.Collections.emptySet;
|
||||||
|
import static java.util.Collections.singleton;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.SortedSet;
|
import java.util.SortedSet;
|
||||||
import org.joda.time.Interval;
|
import org.joda.time.Interval;
|
||||||
import org.sleuthkit.autopsy.timeline.utils.IntervalUtils;
|
import org.sleuthkit.autopsy.timeline.utils.IntervalUtils;
|
||||||
import org.sleuthkit.datamodel.DescriptionLoD;
|
import org.sleuthkit.datamodel.DescriptionLoD;
|
||||||
import org.sleuthkit.datamodel.timeline.EventType;
|
import org.sleuthkit.datamodel.timeline.EventType;
|
||||||
|
import org.sleuthkit.datamodel.timeline.TimelineEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a set of other events clustered together. All the sub events
|
* Represents a set of other events clustered together. All the sub events
|
||||||
@ -94,14 +98,14 @@ public class EventCluster implements MultiEvent<EventStripe> {
|
|||||||
if (!cluster1.getDescription().equals(cluster2.getDescription())) {
|
if (!cluster1.getDescription().equals(cluster2.getDescription())) {
|
||||||
throw new IllegalArgumentException("event clusters are not compatible: they have different descriptions");
|
throw new IllegalArgumentException("event clusters are not compatible: they have different descriptions");
|
||||||
}
|
}
|
||||||
Sets.SetView<Long> idsUnion
|
|
||||||
= Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs());
|
|
||||||
Sets.SetView<Long> hashHitsUnion
|
|
||||||
= Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits());
|
|
||||||
Sets.SetView<Long> taggedUnion
|
|
||||||
= Sets.union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags());
|
|
||||||
|
|
||||||
return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span),
|
Interval spanningInterval = IntervalUtils.span(cluster1.span, cluster2.span);
|
||||||
|
|
||||||
|
Set<Long> idsUnion = union(cluster1.getEventIDs(), cluster2.getEventIDs());
|
||||||
|
Set<Long> hashHitsUnion = union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits());
|
||||||
|
Set<Long> taggedUnion = union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags());
|
||||||
|
|
||||||
|
return new EventCluster(spanningInterval,
|
||||||
cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion,
|
cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion,
|
||||||
cluster1.getDescription(), cluster1.lod);
|
cluster1.getDescription(), cluster1.lod);
|
||||||
}
|
}
|
||||||
@ -125,6 +129,16 @@ public class EventCluster implements MultiEvent<EventStripe> {
|
|||||||
this(spanningInterval, type, eventIDs, hashHits, tagged, description, lod, null);
|
this(spanningInterval, type, eventIDs, hashHits, tagged, description, lod, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public EventCluster(TimelineEvent event, EventType type, DescriptionLoD lod) {
|
||||||
|
this(new Interval(event.getStartMillis(), event.getEndMillis()),
|
||||||
|
type,
|
||||||
|
singleton(event.getEventID()),
|
||||||
|
event.isHashHit() ? singleton(event.getEventID()) : emptySet(),
|
||||||
|
event.isTagged() ? singleton(event.getEventID()) : emptySet(),
|
||||||
|
event.getDescription(lod),
|
||||||
|
lod);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the EventStripe (if any) that contains this cluster
|
* get the EventStripe (if any) that contains this cluster
|
||||||
*
|
*
|
||||||
|
@ -56,8 +56,8 @@ import static org.sleuthkit.autopsy.timeline.ui.EventTypeUtils.getColor;
|
|||||||
import static org.sleuthkit.autopsy.timeline.ui.EventTypeUtils.getImagePath;
|
import static org.sleuthkit.autopsy.timeline.ui.EventTypeUtils.getImagePath;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
|
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.DetailViewEvent;
|
import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.DetailViewEvent;
|
||||||
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DescriptionFilter;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.FilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.FilterState;
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter.DescriptionFilter;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows all EventBundles from the assigned DetailViewPane in a tree organized
|
* Shows all EventBundles from the assigned DetailViewPane in a tree organized
|
||||||
|
@ -43,10 +43,9 @@ import org.sleuthkit.autopsy.timeline.FXMLConstructor;
|
|||||||
import org.sleuthkit.autopsy.timeline.FilteredEventsModel;
|
import org.sleuthkit.autopsy.timeline.FilteredEventsModel;
|
||||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||||
import org.sleuthkit.autopsy.timeline.actions.ResetFilters;
|
import org.sleuthkit.autopsy.timeline.actions.ResetFilters;
|
||||||
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DescriptionFilterState;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.FilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.FilterState;
|
||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter;
|
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter.DescriptionFilter;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The FXML controller for the filter ui.
|
* The FXML controller for the filter ui.
|
||||||
@ -74,7 +73,7 @@ final public class FilterSetPanel extends BorderPane {
|
|||||||
private TreeTableColumn<FilterState<?>, FilterState<?>> legendColumn;
|
private TreeTableColumn<FilterState<?>, FilterState<?>> legendColumn;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private ListView<FilterState<DescriptionFilter>> hiddenDescriptionsListView;
|
private ListView<DescriptionFilterState> hiddenDescriptionsListView;
|
||||||
@FXML
|
@FXML
|
||||||
private TitledPane hiddenDescriptionsPane;
|
private TitledPane hiddenDescriptionsPane;
|
||||||
@FXML
|
@FXML
|
||||||
@ -87,7 +86,7 @@ final public class FilterSetPanel extends BorderPane {
|
|||||||
* Map from filter to its expansion state in the ui, used to restore the
|
* Map from filter to its expansion state in the ui, used to restore the
|
||||||
* expansion state as we navigate back and forward in the history
|
* expansion state as we navigate back and forward in the history
|
||||||
*/
|
*/
|
||||||
private final ObservableMap< TimelineFilter, Boolean> expansionMap = FXCollections.observableHashMap();
|
private final ObservableMap< Object, Boolean> expansionMap = FXCollections.observableHashMap();
|
||||||
private double dividerPosition;
|
private double dividerPosition;
|
||||||
|
|
||||||
@NbBundle.Messages({
|
@NbBundle.Messages({
|
||||||
@ -119,7 +118,6 @@ final public class FilterSetPanel extends BorderPane {
|
|||||||
expansionMap.put(filteredEvents.getFilterState().getFilter(), true);
|
expansionMap.put(filteredEvents.getFilterState().getFilter(), true);
|
||||||
expansionMap.put(filteredEvents.getFilterState().getEventTypeFilterState().getFilter(), true);
|
expansionMap.put(filteredEvents.getFilterState().getEventTypeFilterState().getFilter(), true);
|
||||||
|
|
||||||
|
|
||||||
InvalidationListener applyFiltersListener = observable -> applyFilters();
|
InvalidationListener applyFiltersListener = observable -> applyFilters();
|
||||||
|
|
||||||
filteredEvents.eventTypeZoomProperty().addListener(applyFiltersListener);
|
filteredEvents.eventTypeZoomProperty().addListener(applyFiltersListener);
|
||||||
@ -166,9 +164,8 @@ final public class FilterSetPanel extends BorderPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void refresh() {
|
private void refresh() {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(()
|
||||||
filterTreeTable.setRoot(new FilterTreeItem(filteredEvents.filterProperty().get().copyOf(), expansionMap));
|
-> filterTreeTable.setRoot(new FilterTreeItem(filteredEvents.filterProperty().get().copyOf(), expansionMap)));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyFilters() {
|
private void applyFilters() {
|
||||||
@ -177,8 +174,8 @@ final public class FilterSetPanel extends BorderPane {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private ListCell<FilterState<DescriptionFilter>> getNewDiscriptionFilterListCell() {
|
private ListCell<DescriptionFilterState> getNewDiscriptionFilterListCell() {
|
||||||
final ListCell<FilterState<DescriptionFilter>> cell = new FilterCheckBoxCellFactory< FilterState<DescriptionFilter>>().forList();
|
final ListCell<DescriptionFilterState> cell = new FilterCheckBoxCellFactory< DescriptionFilterState>().forList();
|
||||||
cell.itemProperty().addListener(itemProperty -> {
|
cell.itemProperty().addListener(itemProperty -> {
|
||||||
if (cell.getItem() == null) {
|
if (cell.getItem() == null) {
|
||||||
cell.setContextMenu(null);
|
cell.setContextMenu(null);
|
||||||
@ -210,7 +207,7 @@ final public class FilterSetPanel extends BorderPane {
|
|||||||
|
|
||||||
private static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS
|
private static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS
|
||||||
|
|
||||||
RemoveDescriptionFilterAction(TimeLineController controller, Cell<FilterState<DescriptionFilter>> cell) {
|
RemoveDescriptionFilterAction(TimeLineController controller, Cell<DescriptionFilterState> cell) {
|
||||||
super(actionEvent -> controller.getQuickHideFilters().remove(cell.getItem()));
|
super(actionEvent -> controller.getQuickHideFilters().remove(cell.getItem()));
|
||||||
setGraphic(new ImageView(SHOW));
|
setGraphic(new ImageView(SHOW));
|
||||||
textProperty().bind(
|
textProperty().bind(
|
||||||
|
@ -40,7 +40,7 @@ final public class FilterTreeItem extends TreeItem<FilterState<?>> {
|
|||||||
* children of this FilterTreeItem
|
* children of this FilterTreeItem
|
||||||
* @param expansionMap Map from filter to whether it is expanded or not.
|
* @param expansionMap Map from filter to whether it is expanded or not.
|
||||||
*/
|
*/
|
||||||
public FilterTreeItem(FilterState<?> filterState, ObservableMap<TimelineFilter, Boolean> expansionMap) {
|
public FilterTreeItem(FilterState<?> filterState, ObservableMap<Object, Boolean> expansionMap) {
|
||||||
super(filterState);
|
super(filterState);
|
||||||
|
|
||||||
//keep expanion map upto date if user expands/collapses filter
|
//keep expanion map upto date if user expands/collapses filter
|
||||||
|
@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.timeline.ui.filtering.datamodel;
|
|||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
@ -156,4 +157,40 @@ class CompoundFilterStateImpl<SubFilterType extends TimelineFilter, FilterType e
|
|||||||
|
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int hash = 7;
|
||||||
|
hash = 41 * hash + Objects.hashCode(this.subFilterStates);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final CompoundFilterStateImpl<?, ?> other = (CompoundFilterStateImpl<?, ?>) obj;
|
||||||
|
if (!Objects.equals(this.getFilter(), other.getFilter())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.isSelected(), other.isSelected())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.isDisabled(), other.isDisabled())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.subFilterStates, other.subFilterStates)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.sleuthkit.autopsy.timeline.ui.filtering.datamodel;
|
package org.sleuthkit.autopsy.timeline.ui.filtering.datamodel;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.BooleanBinding;
|
import javafx.beans.binding.BooleanBinding;
|
||||||
import javafx.beans.binding.BooleanExpression;
|
import javafx.beans.binding.BooleanExpression;
|
||||||
@ -35,7 +36,12 @@ public class DefaultFilterState<FilterType extends TimelineFilter> implements Fi
|
|||||||
private final FilterType filter;
|
private final FilterType filter;
|
||||||
|
|
||||||
public DefaultFilterState(FilterType filter) {
|
public DefaultFilterState(FilterType filter) {
|
||||||
|
this(filter, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultFilterState(FilterType filter, boolean selected) {
|
||||||
this.filter = filter;
|
this.filter = filter;
|
||||||
|
this.selected.set(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final SimpleBooleanProperty selected = new SimpleBooleanProperty(false);
|
private final SimpleBooleanProperty selected = new SimpleBooleanProperty(false);
|
||||||
@ -105,4 +111,37 @@ public class DefaultFilterState<FilterType extends TimelineFilter> implements Fi
|
|||||||
public FilterType getActiveFilter() {
|
public FilterType getActiveFilter() {
|
||||||
return isActive() ? getFilter() : null;
|
return isActive() ? getFilter() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int hash = 7;
|
||||||
|
hash = 37 * hash + Objects.hashCode(this.filter);
|
||||||
|
hash = 37 * hash + Objects.hashCode(this.selected);
|
||||||
|
hash = 37 * hash + Objects.hashCode(this.disabled);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final DefaultFilterState<?> other = (DefaultFilterState<?>) obj;
|
||||||
|
if (!Objects.equals(this.filter, other.filter)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.selected, other.selected)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.disabled, other.disabled)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Autopsy Forensic Browser
|
||||||
|
*
|
||||||
|
* Copyright 2018 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.ui.filtering.datamodel;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.sleuthkit.datamodel.DescriptionLoD;
|
||||||
|
import org.sleuthkit.datamodel.timeline.TimelineEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ui level filter for events that have the given description.
|
||||||
|
*/
|
||||||
|
public final class DescriptionFilter implements UIFilter {
|
||||||
|
|
||||||
|
private final DescriptionLoD descriptionLoD;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
public DescriptionFilter(DescriptionLoD descriptionLoD, String description) {
|
||||||
|
super();
|
||||||
|
this.descriptionLoD = descriptionLoD;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DescriptionLoD getDescriptionLoD() {
|
||||||
|
return descriptionLoD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the description
|
||||||
|
*/
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int hash = 7;
|
||||||
|
hash = 23 * hash + Objects.hashCode(this.descriptionLoD);
|
||||||
|
hash = 23 * hash + Objects.hashCode(this.description);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final DescriptionFilter other = (DescriptionFilter) obj;
|
||||||
|
if (!Objects.equals(this.description, other.description)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.descriptionLoD != other.descriptionLoD) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(TimelineEvent event) {
|
||||||
|
return event.getDescription(descriptionLoD).equalsIgnoreCase(description);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* Autopsy Forensic Browser
|
||||||
|
*
|
||||||
|
* Copyright 2018 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.ui.filtering.datamodel;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.binding.BooleanExpression;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A FilterState implementation for DescriptionFilters
|
||||||
|
*/
|
||||||
|
public class DescriptionFilterState implements FilterState<DescriptionFilter> {
|
||||||
|
|
||||||
|
private final DescriptionFilter filter;
|
||||||
|
|
||||||
|
public DescriptionFilterState(DescriptionFilter filter) {
|
||||||
|
this(filter, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DescriptionFilterState(DescriptionFilter filter, boolean selected) {
|
||||||
|
this.filter = filter;
|
||||||
|
this.selected.set(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final SimpleBooleanProperty selected = new SimpleBooleanProperty(false);
|
||||||
|
private final SimpleBooleanProperty disabled = new SimpleBooleanProperty(false);
|
||||||
|
private final BooleanBinding activeProp = Bindings.and(selected, disabled.not());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BooleanProperty selectedProperty() {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BooleanProperty disabledProperty() {
|
||||||
|
return disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSelected(Boolean act) {
|
||||||
|
selected.set(act);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSelected() {
|
||||||
|
return selected.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDisabled(Boolean act) {
|
||||||
|
disabled.set(act);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDisabled() {
|
||||||
|
return disabledProperty().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isActive() {
|
||||||
|
return activeProperty().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BooleanExpression activeProperty() {
|
||||||
|
return activeProp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayName() {
|
||||||
|
return filter.getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DescriptionFilter getFilter() {
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DescriptionFilter getActiveFilter() {
|
||||||
|
return isActive() ? getFilter() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int hash = 7;
|
||||||
|
hash = 37 * hash + Objects.hashCode(this.filter);
|
||||||
|
hash = 37 * hash + Objects.hashCode(this.selected);
|
||||||
|
hash = 37 * hash + Objects.hashCode(this.disabled);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final DescriptionFilterState other = (DescriptionFilterState) obj;
|
||||||
|
if (!Objects.equals(this.filter, other.filter)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.selected, other.selected)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.disabled, other.disabled)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DescriptionFilterState copyOf() {
|
||||||
|
DescriptionFilterState copy = new DescriptionFilterState(filter);
|
||||||
|
copy.setSelected(isSelected());
|
||||||
|
copy.setDisabled(isDisabled());
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
@ -20,14 +20,13 @@ package org.sleuthkit.autopsy.timeline.ui.filtering.datamodel;
|
|||||||
|
|
||||||
import javafx.beans.binding.BooleanExpression;
|
import javafx.beans.binding.BooleanExpression;
|
||||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The state of a filter: selected, disabled, active, etc.
|
* The state of a filter: selected, disabled, active, etc.
|
||||||
*
|
*
|
||||||
* @param <FilterType> The type of filter this is the state for.
|
* @param <FilterType> The type of filter this is the state for.
|
||||||
*/
|
*/
|
||||||
public interface FilterState<FilterType extends TimelineFilter> {
|
public interface FilterState<FilterType> {
|
||||||
|
|
||||||
String getDisplayName();
|
String getDisplayName();
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import javafx.beans.property.ReadOnlyBooleanProperty;
|
|||||||
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.collections.ObservableSet;
|
||||||
import static org.apache.commons.lang3.ObjectUtils.notEqual;
|
import static org.apache.commons.lang3.ObjectUtils.notEqual;
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter;
|
import org.sleuthkit.datamodel.timeline.TimelineFilter;
|
||||||
import org.sleuthkit.datamodel.timeline.TimelineFilter.DataSourceFilter;
|
import org.sleuthkit.datamodel.timeline.TimelineFilter.DataSourceFilter;
|
||||||
@ -51,7 +52,8 @@ public class RootFilterState implements CompoundFilterState< TimelineFilter, Roo
|
|||||||
private static final ReadOnlyBooleanProperty ALWAYS_TRUE = new ReadOnlyBooleanWrapper(true).getReadOnlyProperty();
|
private static final ReadOnlyBooleanProperty ALWAYS_TRUE = new ReadOnlyBooleanWrapper(true).getReadOnlyProperty();
|
||||||
private final static ReadOnlyBooleanProperty ALWAYS_FALSE = new ReadOnlyBooleanWrapper(false).getReadOnlyProperty();
|
private final static ReadOnlyBooleanProperty ALWAYS_FALSE = new ReadOnlyBooleanWrapper(false).getReadOnlyProperty();
|
||||||
|
|
||||||
private final ObservableList< FilterState< ?>> subFilterStates = FXCollections.observableArrayList();
|
private final ObservableList< FilterState< ? extends TimelineFilter>> subFilterStates = FXCollections.observableArrayList();
|
||||||
|
private final ObservableSet< FilterState< ? extends TimelineFilter>> namedFilterStates = FXCollections.observableSet();
|
||||||
private final RootFilter delegate;
|
private final RootFilter delegate;
|
||||||
|
|
||||||
public RootFilterState(RootFilter delegate) {
|
public RootFilterState(RootFilter delegate) {
|
||||||
@ -90,6 +92,7 @@ public class RootFilterState implements CompoundFilterState< TimelineFilter, Roo
|
|||||||
dataSourcesFilterState,
|
dataSourcesFilterState,
|
||||||
fileTypesFilterState,
|
fileTypesFilterState,
|
||||||
eventTypeFilterState);
|
eventTypeFilterState);
|
||||||
|
namedFilterStates.addAll(subFilterStates);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,28 +103,24 @@ public class RootFilterState implements CompoundFilterState< TimelineFilter, Roo
|
|||||||
* @return A new RootFilter model that intersects the given filter with this
|
* @return A new RootFilter model that intersects the given filter with this
|
||||||
* one.
|
* one.
|
||||||
*/
|
*/
|
||||||
public RootFilterState intersect(TimelineFilter otherFilter) {
|
public RootFilterState intersect(FilterState<? extends TimelineFilter> otherFilter) {
|
||||||
RootFilterState copyOf = copyOf();
|
RootFilterState copyOf = copyOf();
|
||||||
copyOf.addSubFilterState(otherFilter);
|
copyOf.addSubFilterState(otherFilter);
|
||||||
return copyOf;
|
return copyOf;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addSubFilterState(TimelineFilter subFilter) {
|
public void addSubFilterState(FilterState<? extends TimelineFilter> newSubFilterState) {
|
||||||
|
if (subFilterStates.contains(newSubFilterState) == false) {
|
||||||
if (subFilter instanceof TimelineFilter.CompoundFilter<?>) {
|
subFilterStates.add(newSubFilterState);
|
||||||
CompoundFilterStateImpl<? extends TimelineFilter, ? extends TimelineFilter.CompoundFilter<? extends TimelineFilter>> compoundFilterStateImpl = new CompoundFilterStateImpl<>((TimelineFilter.CompoundFilter<?>) subFilter);
|
}
|
||||||
getSubFilterStates().add(compoundFilterStateImpl);
|
if (delegate.getSubFilters().contains(newSubFilterState.getFilter())) {
|
||||||
compoundFilterStateImpl.setSelected(Boolean.TRUE);
|
getFilter().getSubFilters().add(newSubFilterState.getFilter());
|
||||||
} else {
|
|
||||||
DefaultFilterState<TimelineFilter> defaultFilterState = new DefaultFilterState<>(subFilter);
|
|
||||||
getSubFilterStates().add(defaultFilterState);
|
|
||||||
defaultFilterState.setSelected(Boolean.TRUE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RootFilterState copyOf() {
|
public RootFilterState copyOf() {
|
||||||
return new RootFilterState(getFilter().copyOf(),
|
RootFilterState copy = new RootFilterState(getFilter().copyOf(),
|
||||||
getEventTypeFilterState().copyOf(),
|
getEventTypeFilterState().copyOf(),
|
||||||
getKnownFilterState().copyOf(),
|
getKnownFilterState().copyOf(),
|
||||||
getTextFilterState().copyOf(),
|
getTextFilterState().copyOf(),
|
||||||
@ -130,6 +129,10 @@ public class RootFilterState implements CompoundFilterState< TimelineFilter, Roo
|
|||||||
getDataSourcesFilterState().copyOf(),
|
getDataSourcesFilterState().copyOf(),
|
||||||
getFileTypesFilterState().copyOf()
|
getFileTypesFilterState().copyOf()
|
||||||
);
|
);
|
||||||
|
this.getSubFilterStates().stream()
|
||||||
|
.filter(filterState -> namedFilterStates.contains(filterState) == false)
|
||||||
|
.forEach(copy::addSubFilterState);
|
||||||
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompoundFilterState<EventTypeFilter, EventTypeFilter> getEventTypeFilterState() {
|
public CompoundFilterState<EventTypeFilter, EventTypeFilter> getEventTypeFilterState() {
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Autopsy Forensic Browser
|
||||||
|
*
|
||||||
|
* Copyright 2018 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.ui.filtering.datamodel;
|
||||||
|
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import org.sleuthkit.datamodel.timeline.TimelineEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Filter over TimelineEvents that is applied in the UI, not the DB. *
|
||||||
|
*/
|
||||||
|
public interface UIFilter extends Predicate<TimelineEvent> {
|
||||||
|
|
||||||
|
static UIFilter getAllPassFilter() {
|
||||||
|
return event -> true;
|
||||||
|
}
|
||||||
|
}
|
@ -27,9 +27,9 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import org.joda.time.Interval;
|
import org.joda.time.Interval;
|
||||||
import org.sleuthkit.autopsy.timeline.FilteredEventsModel;
|
import org.sleuthkit.autopsy.timeline.FilteredEventsModel;
|
||||||
import static org.sleuthkit.autopsy.timeline.FilteredEventsModel.unGroupConcat;
|
|
||||||
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
|
import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
|
||||||
import org.sleuthkit.autopsy.timeline.utils.TimelineDBUtils;
|
import org.sleuthkit.autopsy.timeline.utils.TimelineDBUtils;
|
||||||
|
import static org.sleuthkit.autopsy.timeline.utils.TimelineDBUtils.unGroupConcat;
|
||||||
import org.sleuthkit.datamodel.SleuthkitCase;
|
import org.sleuthkit.datamodel.SleuthkitCase;
|
||||||
import org.sleuthkit.datamodel.TimelineManager;
|
import org.sleuthkit.datamodel.TimelineManager;
|
||||||
import org.sleuthkit.datamodel.TskCoreException;
|
import org.sleuthkit.datamodel.TskCoreException;
|
||||||
|
@ -18,7 +18,14 @@
|
|||||||
*/
|
*/
|
||||||
package org.sleuthkit.autopsy.timeline.utils;
|
package org.sleuthkit.autopsy.timeline.utils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
import org.sleuthkit.datamodel.SleuthkitCase;
|
import org.sleuthkit.datamodel.SleuthkitCase;
|
||||||
|
import org.sleuthkit.datamodel.TskCoreException;
|
||||||
import org.sleuthkit.datamodel.TskData;
|
import org.sleuthkit.datamodel.TskData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,4 +43,32 @@ public class TimelineDBUtils {
|
|||||||
return (sleuthkitCase.getDatabaseType() == TskData.DbType.POSTGRESQL ? "string_agg" : "group_concat")
|
return (sleuthkitCase.getDatabaseType() == TskData.DbType.POSTGRESQL ? "string_agg" : "group_concat")
|
||||||
+ "(Cast (" + args + " AS VARCHAR) , '" + "," + "')";
|
+ "(Cast (" + args + " AS VARCHAR) , '" + "," + "')";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* take the result of a group_concat SQLite operation and split it into a
|
||||||
|
* set of X using the mapper to to convert from string to X If groupConcat
|
||||||
|
* is empty, null, or all whitespace, returns an empty list.
|
||||||
|
*
|
||||||
|
* @param <X> the type of elements to return
|
||||||
|
* @param groupConcat a string containing the group_concat result ( a comma
|
||||||
|
* separated list)
|
||||||
|
* @param mapper a function from String to X
|
||||||
|
*
|
||||||
|
* @return a Set of X, each element mapped from one element of the original
|
||||||
|
* comma delimited string
|
||||||
|
*
|
||||||
|
* @throws org.sleuthkit.datamodel.TskCoreException
|
||||||
|
*/
|
||||||
|
public static <X> List<X> unGroupConcat(String groupConcat, CheckedFunction<String, X, TskCoreException> mapper) throws TskCoreException {
|
||||||
|
|
||||||
|
if (isBlank(groupConcat)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<X> result = new ArrayList<>();
|
||||||
|
for (String s : groupConcat.split(",")) {
|
||||||
|
result.add(mapper.apply(s));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,9 @@
|
|||||||
package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
|
package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
|
||||||
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.collections.transformation.SortedList;
|
import javafx.collections.transformation.SortedList;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
@ -45,7 +48,7 @@ final public class HashHitGroupList extends NavPanel<DrawableGroup> {
|
|||||||
* sorting in the ListView.
|
* sorting in the ListView.
|
||||||
*/
|
*/
|
||||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||||
private SortedList<DrawableGroup> sorted;
|
private SortedList<DrawableGroup> sorted = FXCollections.<DrawableGroup>observableArrayList().sorted();
|
||||||
|
|
||||||
public HashHitGroupList(ImageGalleryController controller) {
|
public HashHitGroupList(ImageGalleryController controller) {
|
||||||
super(controller);
|
super(controller);
|
||||||
@ -78,9 +81,24 @@ final public class HashHitGroupList extends NavPanel<DrawableGroup> {
|
|||||||
setGraphic(new ImageView("org/sleuthkit/autopsy/imagegallery/images/hashset_hits.png")); //NON-NLS
|
setGraphic(new ImageView("org/sleuthkit/autopsy/imagegallery/images/hashset_hits.png")); //NON-NLS
|
||||||
|
|
||||||
getBorderPane().setCenter(groupList);
|
getBorderPane().setCenter(groupList);
|
||||||
sorted = getController().getGroupManager().getAnalyzedGroups()
|
getController().getGroupManager().getAnalyzedGroups().addListener(new ListChangeListener<DrawableGroup>() {
|
||||||
.filtered(group -> group.getHashSetHitsCount() > 0)
|
@Override
|
||||||
.sorted(getDefaultComparator());
|
public void onChanged(ListChangeListener.Change<? extends DrawableGroup> c) {
|
||||||
|
|
||||||
|
while (c.next()) {
|
||||||
|
|
||||||
|
c.getAddedSubList().forEach((DrawableGroup t) -> {
|
||||||
|
if (t.getHashSetHitsCount() > 0) {
|
||||||
|
Platform.runLater(() -> sorted.add(t));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
c.getRemoved().forEach((DrawableGroup t) -> {
|
||||||
|
Platform.runLater(() -> sorted.remove(t));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
GroupCellFactory groupCellFactory = new GroupCellFactory(getController(), comparatorProperty());
|
GroupCellFactory groupCellFactory = new GroupCellFactory(getController(), comparatorProperty());
|
||||||
groupList.setCellFactory(groupCellFactory::getListCell);
|
groupList.setCellFactory(groupCellFactory::getListCell);
|
||||||
@ -89,7 +107,8 @@ final public class HashHitGroupList extends NavPanel<DrawableGroup> {
|
|||||||
|
|
||||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||||
@Override
|
@Override
|
||||||
void setFocusedGroup(DrawableGroup grouping) {
|
void setFocusedGroup(DrawableGroup grouping
|
||||||
|
) {
|
||||||
groupList.getSelectionModel().select(grouping);
|
groupList.getSelectionModel().select(grouping);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#Updated by build script
|
#Updated by build script
|
||||||
#Tue, 11 Dec 2018 14:41:40 -0500
|
#Wed, 19 Dec 2018 18:37:27 +0100
|
||||||
LBL_splash_window_title=Starting Autopsy
|
LBL_splash_window_title=Starting Autopsy
|
||||||
SPLASH_HEIGHT=314
|
SPLASH_HEIGHT=314
|
||||||
SPLASH_WIDTH=538
|
SPLASH_WIDTH=538
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#Updated by build script
|
#Updated by build script
|
||||||
#Tue, 11 Dec 2018 14:41:40 -0500
|
#Wed, 19 Dec 2018 18:37:27 +0100
|
||||||
CTL_MainWindow_Title=Autopsy 4.9.1
|
CTL_MainWindow_Title=Autopsy 4.9.1
|
||||||
CTL_MainWindow_Title_No_Project=Autopsy 4.9.1
|
CTL_MainWindow_Title_No_Project=Autopsy 4.9.1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user