fix description generation and clustering

- rename getAggregatredEvents to getEventClusters
- make more members private
- improve logic to get subclusters
- introduce notion of RelativeDetail for navigating DescriptionLoDs
This commit is contained in:
jmillman 2015-09-17 14:06:29 -04:00
parent ce930506bb
commit bd1e9a58d8
6 changed files with 160 additions and 84 deletions

View File

@ -337,7 +337,7 @@ public final class FilteredEventsModel {
zoom = requestedTypeZoom.get();
lod = requestedLOD.get();
}
return repo.getAggregatedEvents(new ZoomParams(range, zoom, filter, lod));
return repo.getEventClusters(new ZoomParams(range, zoom, filter, lod));
}
/**
@ -347,8 +347,8 @@ public final class FilteredEventsModel {
* range and pass the requested filter, using the given aggregation
* to control the grouping of events
*/
public List<EventCluster> getAggregatedEvents(ZoomParams params) {
return repo.getAggregatedEvents(params);
public List<EventCluster> getEventClusters(ZoomParams params) {
return repo.getEventClusters(params);
}
synchronized public boolean handleContentTagAdded(ContentTagAddedEvent evt) {

View File

@ -98,7 +98,7 @@ public class EventsRepository {
private final LoadingCache<Long, TimeLineEvent> idToEventCache;
private final LoadingCache<ZoomParams, Map<EventType, Long>> eventCountsCache;
private final LoadingCache<ZoomParams, List<EventCluster>> aggregateEventsCache;
private final LoadingCache<ZoomParams, List<EventCluster>> eventClusterCache;
private final ObservableMap<Long, String> datasourcesMap = FXCollections.observableHashMap();
private final ObservableMap<Long, String> hashSetMap = FXCollections.observableHashMap();
@ -146,7 +146,7 @@ public class EventsRepository {
.maximumSize(1000L)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build(CacheLoader.from(eventDB::countEventsByType));
aggregateEventsCache = CacheBuilder.newBuilder()
eventClusterCache = CacheBuilder.newBuilder()
.maximumSize(1000L)
.expireAfterAccess(10, TimeUnit.MINUTES
).build(CacheLoader.from(eventDB::getClusteredEvents));
@ -206,8 +206,8 @@ public class EventsRepository {
}
synchronized public List<EventCluster> getAggregatedEvents(ZoomParams params) {
return aggregateEventsCache.getUnchecked(params);
synchronized public List<EventCluster> getEventClusters(ZoomParams params) {
return eventClusterCache.getUnchecked(params);
}
synchronized public Map<EventType, Long> countEvents(ZoomParams params) {
@ -218,7 +218,7 @@ public class EventsRepository {
minCache.invalidateAll();
maxCache.invalidateAll();
eventCountsCache.invalidateAll();
aggregateEventsCache.invalidateAll();
eventClusterCache.invalidateAll();
idToEventCache.invalidateAll();
}
@ -292,7 +292,7 @@ public class EventsRepository {
synchronized private void invalidateCaches(Set<Long> updatedEventIDs) {
eventCountsCache.invalidateAll();
aggregateEventsCache.invalidateAll();
eventClusterCache.invalidateAll();
idToEventCache.invalidateAll(updatedEventIDs);
try {
tagNames.setAll(autoCase.getSleuthkitCase().getTagNamesInUse());

View File

@ -1,14 +1,29 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
* Autopsy Forensic Browser
*
* Copyright 2015 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;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.util.Objects.nonNull;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.stream.Collectors;
@ -61,6 +76,7 @@ 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.zooming.DescriptionLOD;
import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
import org.sleuthkit.datamodel.SleuthkitCase;
@ -89,11 +105,11 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A
b.setVisible(show);
b.setManaged(show);
}
Map<EventType, DropShadow> dropShadowMap = new HashMap<>();
private final Map<EventType, DropShadow> dropShadowMap = new HashMap<>();
final Color evtColor;
private final S parentNode;
DescriptionVisibility descrVis;
private DescriptionVisibility descrVis;
/**
* Pane that contains AggregateEventNodes of any 'subevents' if they are
@ -102,7 +118,11 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A
* //TODO: move more of the control of subnodes/events here and out of
* EventDetail Chart
*/
final Pane subNodePane = new Pane();
private final Pane subNodePane = new Pane();
Pane getSubNodePane() {
return subNodePane;
}
/**
* The ImageView used to show the icon for this node's event's type
@ -121,16 +141,28 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A
private final T eventBundle;
private final EventDetailChart chart;
final SleuthkitCase sleuthkitCase;
final FilteredEventsModel eventsModel;
private final SleuthkitCase sleuthkitCase;
final Button plusButton;
final Button minusButton;
SleuthkitCase getSleuthkitCase() {
return sleuthkitCase;
}
final SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
FilteredEventsModel getEventsModel() {
return eventsModel;
}
private final FilteredEventsModel eventsModel;
private final Button plusButton;
private final Button minusButton;
private final SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
final HBox header;
final Region spacer = new Region();
Region getSpacer() {
return spacer;
}
private final Region spacer = new Region();
private final CollapseClusterAction collapseClusterAction;
private final ExpandClusterAction expandClusterAction;
@ -160,9 +192,7 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A
configureLODButton(minusButton);
HBox.setHgrow(spacer, Priority.ALWAYS);
header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, /*
* spacer,
*/ minusButton, plusButton);
header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, minusButton, plusButton);
header.setMinWidth(USE_PREF_SIZE);
header.setPadding(new Insets(2, 5, 2, 5));
@ -244,12 +274,17 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A
show(plusButton, showControls);
}
/**
* make a new filter intersecting the global filter with description and
* type filters to restrict sub-clusters
*
*/
RootFilter getSubClusterFilter() {
RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf();
//make a new filter intersecting the global filter with description and type filters to restrict sub-clusters
combinedFilter.getSubFilters().addAll(new DescriptionFilter(getEventBundle().getDescriptionLOD(), getDescription()),
RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf();
subClusterFilter.getSubFilters().addAll(
new DescriptionFilter(getEventBundle().getDescriptionLOD(), getDescription()),
new TypeFilter(getEventType()));
return combinedFilter;
return subClusterFilter;
}
abstract Collection<T> makeBundlesFromClusters(List<EventCluster> eventClusters);
@ -324,59 +359,84 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A
}
/**
* loads sub-clusters at the given Description LOD
* loads sub-bundles at the given Description LOD, continues
*
* @param newDescriptionLOD
* @param requestedDescrLoD
* @param expand
*/
final synchronized void loadSubClusters(DescriptionLOD newDescriptionLOD) {
private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) {
subNodePane.getChildren().clear();
if (newDescriptionLOD == getEventBundle().getDescriptionLOD()) {
if (descLOD.get().withRelativeDetail(relativeDetail) == getEventBundle().getDescriptionLOD()) {
descLOD.set(getEventBundle().getDescriptionLOD());
showSpans(true);
getChart().setRequiresLayout(true);
getChart().requestChartLayout();
chart.setRequiresLayout(true);
chart.requestChartLayout();
} else {
showSpans(false);
RootFilter combinedFilter = getSubClusterFilter();
//make a new end inclusive span (to 'filter' with)
final Interval span = new Interval(getEventBundle().getStartMillis(), getEventBundle().getEndMillis() + 1000);
// make new ZoomParams to query with
final RootFilter subClusterFilter = getSubClusterFilter();
/*
* We need to extend end time because for the query by one second,
* because it is treated as an open interval but we want to include
* events at exactly the time of the last event in this cluster
*/
final Interval subClusterSpan = new Interval(getEventBundle().getStartMillis(), getEventBundle().getEndMillis() + 1000);
final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get();
final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescLOD());
//make a task to load the subnodes
LoggedTask<List<S>> loggedTask = new LoggedTask<List<S>>(
LoggedTask<List<S>> loggedTask;
loggedTask = new LoggedTask<List<S>>(
NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) {
private Collection<T> bundles;
private volatile DescriptionLOD loadedDescriptionLoD = getDescLOD().withRelativeDetail(relativeDetail);
private DescriptionLOD next = loadedDescriptionLoD;
@Override
protected List<S> call() throws Exception {
//query for the sub-clusters
List<EventCluster> aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span,
eventsModel.eventTypeZoomProperty().get(),
combinedFilter,
newDescriptionLOD));
do {
loadedDescriptionLoD = next;
if (loadedDescriptionLoD == getEventBundle().getDescriptionLOD()) {
return Collections.emptyList();
}
bundles = loadBundles();
next = loadedDescriptionLoD.withRelativeDetail(relativeDetail);
} while (bundles.size() == 1 && nonNull(next));
return makeBundlesFromClusters(aggregatedEvents).stream()
.map(aggEvent -> {
return getNodeForCluser(aggEvent);
}).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters
// return list of AbstractDetailViewNodes representing sub-bundles
return bundles.stream()
.map(AbstractDetailViewNode.this::getNodeForBundle)
.collect(Collectors.toList());
}
private Collection<T> loadBundles() {
return makeBundlesFromClusters(eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)));
}
@Override
protected void succeeded() {
chart.setCursor(Cursor.WAIT);
try {
getChart().setCursor(Cursor.WAIT);
List<S> subBundleNodes = get();
if (subBundleNodes.isEmpty()) {
showSpans(true);
} else {
showSpans(false);
}
descLOD.set(loadedDescriptionLoD);
//assign subNodes and request chart layout
subNodePane.getChildren().setAll(get());
setDescriptionVisibility(descrVis);
getChart().setRequiresLayout(true);
getChart().requestChartLayout();
getChart().setCursor(null);
subNodePane.getChildren().setAll(subBundleNodes);
chart.setRequiresLayout(true);
chart.requestChartLayout();
} catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex);
}
chart.setCursor(null);
}
};
//start task
getChart().getController().monitorTask(loggedTask);
chart.getController().monitorTask(loggedTask);
}
}
@ -390,7 +450,7 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A
this.descrVis = descrVis;
final int size = getEventBundle().getEventIDs().size();
switch (descrVis) {
switch (this.descrVis) {
case COUNT_ONLY:
descrLabel.setText("");
countLabel.setText(String.valueOf(size));
@ -411,7 +471,7 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A
}
}
abstract S getNodeForCluser(T cluster);
abstract S getNodeForBundle(T bundle);
/**
* event handler used for mouse events on {@link AggregateEventNode}s
@ -432,10 +492,10 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A
} else if (t.isShortcutDown()) {
chart.selectedNodes.removeAll(AbstractDetailViewNode.this);
} else if (t.getClickCount() > 1) {
final DescriptionLOD next = descLOD.get().next();
final DescriptionLOD next = descLOD.get().moreDetailed();
if (next != null) {
loadSubClusters(next);
descLOD.set(next);
loadSubBundles(DescriptionLOD.RelativeDetail.MORE);
}
} else {
chart.selectedNodes.setAll(AbstractDetailViewNode.this);
@ -461,14 +521,15 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A
private class ExpandClusterAction extends Action {
public ExpandClusterAction() {
ExpandClusterAction() {
super("Expand");
setGraphic(new ImageView(PLUS));
setEventHandler((ActionEvent t) -> {
final DescriptionLOD next = descLOD.get().next();
final DescriptionLOD next = descLOD.get().moreDetailed();
if (next != null) {
loadSubClusters(next);
descLOD.set(next);
loadSubBundles(DescriptionLOD.RelativeDetail.MORE);
}
});
disabledProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL));
@ -477,15 +538,14 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A
private class CollapseClusterAction extends Action {
public CollapseClusterAction() {
CollapseClusterAction() {
super("Collapse");
setGraphic(new ImageView(MINUS));
setEventHandler((ActionEvent t) -> {
final DescriptionLOD previous = descLOD.get().previous();
final DescriptionLOD previous = descLOD.get().lessDetailed();
if (previous != null) {
loadSubClusters(previous);
descLOD.set(previous);
loadSubBundles(DescriptionLOD.RelativeDetail.LESS);
}
});
disabledProperty().bind(descLOD.isEqualTo(getEventBundle().getDescriptionLOD()));

View File

@ -66,8 +66,8 @@ public class EventClusterNode extends AbstractDetailViewNode<EventCluster, Event
minWidthProperty().bind(spanRegion.widthProperty());
header.setPrefWidth(USE_COMPUTED_SIZE);
final BorderPane borderPane = new BorderPane(subNodePane, header, null, null, null);
BorderPane.setAlignment(subNodePane, Pos.TOP_LEFT);
final BorderPane borderPane = new BorderPane(getSubNodePane(), header, null, null, null);
BorderPane.setAlignment(getSubNodePane(), Pos.TOP_LEFT);
borderPane.setPrefWidth(USE_COMPUTED_SIZE);
getChildren().addAll(spanRegion, borderPane);
@ -86,8 +86,8 @@ public class EventClusterNode extends AbstractDetailViewNode<EventCluster, Event
if (!getEventCluster().getEventIDsWithHashHits().isEmpty()) {
hashSetCounts = new HashMap<>();
try {
for (TimeLineEvent tle : eventsModel.getEventsById(getEventCluster().getEventIDsWithHashHits())) {
Set<String> hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames();
for (TimeLineEvent tle : getEventsModel().getEventsById(getEventCluster().getEventIDsWithHashHits())) {
Set<String> hashSetNames = getSleuthkitCase().getAbstractFileById(tle.getFileID()).getHashSetNames();
for (String hashSetName : hashSetNames) {
hashSetCounts.merge(hashSetName, 1L, Long::sum);
}
@ -99,7 +99,7 @@ public class EventClusterNode extends AbstractDetailViewNode<EventCluster, Event
Map<String, Long> tagCounts = new HashMap<>();
if (!getEventCluster().getEventIDsWithTags().isEmpty()) {
tagCounts.putAll(eventsModel.getTagCountsByTagName(getEventCluster().getEventIDsWithTags()));
tagCounts.putAll(getEventsModel().getTagCountsByTagName(getEventCluster().getEventIDsWithTags()));
}
@ -174,7 +174,7 @@ public class EventClusterNode extends AbstractDetailViewNode<EventCluster, Event
}
@Override
EventClusterNode getNodeForCluser(EventCluster cluster) {
EventClusterNode getNodeForBundle(EventCluster cluster) {
return new EventClusterNode(cluster, this, getChart());
}

View File

@ -18,7 +18,6 @@ import javafx.scene.layout.Region;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
import javafx.scene.layout.VBox;
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import static org.sleuthkit.autopsy.timeline.ui.detailview.AbstractDetailViewNode.show;
@ -28,14 +27,12 @@ import static org.sleuthkit.autopsy.timeline.ui.detailview.AbstractDetailViewNod
*/
public class EventStripeNode extends AbstractDetailViewNode<EventStripe, EventStripeNode> {
private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName());
private final HBox rangesHBox = new HBox();
EventStripeNode(EventStripe eventStripe, EventStripeNode parentNode, EventDetailChart chart) {
super(chart, eventStripe, parentNode);
minWidthProperty().bind(rangesHBox.widthProperty());
final VBox internalVBox = new VBox(header, subNodePane);
final VBox internalVBox = new VBox(header, getSubNodePane());
internalVBox.setAlignment(Pos.CENTER_LEFT);
for (Range<Long> range : eventStripe.getRanges()) {
@ -57,7 +54,7 @@ public class EventStripeNode extends AbstractDetailViewNode<EventStripe, EventSt
@Override
void showDescriptionLoDControls(final boolean showControls) {
super.showDescriptionLoDControls(showControls);
show(spacer, showControls);
show(getSpacer(), showControls);
}
@Override
@ -105,8 +102,7 @@ public class EventStripeNode extends AbstractDetailViewNode<EventStripe, EventSt
}
@Override
EventStripeNode getNodeForCluser(EventStripe cluster) {
EventStripeNode getNodeForBundle(EventStripe cluster) {
return new EventStripeNode(cluster, this, getChart());
}
}

View File

@ -21,7 +21,7 @@ package org.sleuthkit.autopsy.timeline.zooming;
import org.openide.util.NbBundle;
/**
*
* Enumeration of all description levels of detail.
*/
public enum DescriptionLOD {
@ -39,7 +39,7 @@ public enum DescriptionLOD {
this.displayName = displayName;
}
public DescriptionLOD next() {
public DescriptionLOD moreDetailed() {
try {
return values()[ordinal() + 1];
} catch (ArrayIndexOutOfBoundsException e) {
@ -47,11 +47,31 @@ public enum DescriptionLOD {
}
}
public DescriptionLOD previous() {
public DescriptionLOD lessDetailed() {
try {
return values()[ordinal() - 1];
} catch (ArrayIndexOutOfBoundsException e) {
return null;
}
}
public DescriptionLOD withRelativeDetail(RelativeDetail relativeDetail) {
switch (relativeDetail) {
case EQUAL:
return this;
case MORE:
return moreDetailed();
case LESS:
return lessDetailed();
default:
throw new IllegalArgumentException("Unknown RelativeDetail value " + relativeDetail);
}
}
public enum RelativeDetail {
EQUAL,
MORE,
LESS;
}
}