Merge pull request #1595 from millmanorama/develop

Initial Pull request for new style event clustering.
This commit is contained in:
Richard Cordovano 2015-09-30 15:15:35 -04:00
commit cbbd6c375c
29 changed files with 1605 additions and 854 deletions

View File

@ -0,0 +1,36 @@
/*
* 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.
*/
package org.sleuthkit.autopsy.timeline.datamodel;
import com.google.common.collect.Range;
import java.util.Set;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
/**
*
*/
public interface EventBundle {
String getDescription();
DescriptionLOD getDescriptionLOD();
Set<Long> getEventIDs();
Set<Long> getEventIDsWithHashHits();
Set<Long> getEventIDsWithTags();
EventType getEventType();
long getEndMillis();
long getStartMillis();
Iterable<Range<Long>> getRanges();
}

View File

@ -18,6 +18,7 @@
*/
package org.sleuthkit.autopsy.timeline.datamodel;
import com.google.common.collect.Range;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.Set;
@ -33,7 +34,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
* designated 'zoom level'.
*/
@Immutable
public class AggregateEvent {
public class EventCluster implements EventBundle {
/**
* the smallest time interval containing all the aggregated events
@ -72,7 +73,7 @@ public class AggregateEvent {
*/
private final Set<Long> hashHits;
public AggregateEvent(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLOD lod) {
public EventCluster(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLOD lod) {
this.span = spanningInterval;
this.type = type;
@ -90,6 +91,14 @@ public class AggregateEvent {
return span;
}
public long getStartMillis() {
return span.getStartMillis();
}
public long getEndMillis() {
return span.getEndMillis();
}
public Set<Long> getEventIDs() {
return Collections.unmodifiableSet(eventIDs);
}
@ -106,36 +115,51 @@ public class AggregateEvent {
return description;
}
public EventType getType() {
public EventType getEventType() {
return type;
}
public DescriptionLOD getLOD() {
@Override
public DescriptionLOD getDescriptionLOD() {
return lod;
}
/**
* merge two aggregate events into one new aggregate event.
*
* @param aggEvent1
* @param cluster1
* @param aggEVent2
*
* @return a new aggregate event that is the result of merging the given
* events
*/
public static AggregateEvent merge(AggregateEvent aggEvent1, AggregateEvent ag2) {
public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) {
if (aggEvent1.getType() != ag2.getType()) {
if (cluster1.getEventType() != cluster2.getEventType()) {
throw new IllegalArgumentException("aggregate events are not compatible they have different types");
}
if (!aggEvent1.getDescription().equals(ag2.getDescription())) {
if (!cluster1.getDescription().equals(cluster2.getDescription())) {
throw new IllegalArgumentException("aggregate events are not compatible they have different descriptions");
}
Sets.SetView<Long> idsUnion = Sets.union(aggEvent1.getEventIDs(), ag2.getEventIDs());
Sets.SetView<Long> hashHitsUnion = Sets.union(aggEvent1.getEventIDsWithHashHits(), ag2.getEventIDsWithHashHits());
Sets.SetView<Long> taggedUnion = Sets.union(aggEvent1.getEventIDsWithTags(), ag2.getEventIDsWithTags());
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 AggregateEvent(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getType(), idsUnion, hashHitsUnion, taggedUnion, aggEvent1.getDescription(), aggEvent1.lod);
return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod);
}
Range<Long> getRange() {
if (getEndMillis() > getStartMillis()) {
return Range.closedOpen(getSpan().getStartMillis(), getSpan().getEndMillis());
} else {
return Range.singleton(getStartMillis());
}
}
@Override
public Iterable<Range<Long>> getRanges() {
return Collections.singletonList(getRange());
}
}

View File

@ -0,0 +1,143 @@
/*
* 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.
*/
package org.sleuthkit.autopsy.timeline.datamodel;
import com.google.common.base.Preconditions;
import com.google.common.collect.Range;
import com.google.common.collect.RangeMap;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeMap;
import com.google.common.collect.TreeRangeSet;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.concurrent.Immutable;
import org.python.google.common.base.Objects;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
/**
*
*/
@Immutable
public final class EventStripe implements EventBundle {
private final RangeSet<Long> spans = TreeRangeSet.create();
private final RangeMap<Long, EventCluster> spanMap = TreeRangeMap.create();
/**
* the type of all the aggregted events
*/
private final EventType type;
/**
* the common description of all the aggregated events
*/
private final String description;
/**
* the description level of detail that the events were aggregated at.
*/
private final DescriptionLOD lod;
/**
* the set of ids of the aggregated events
*/
private final Set<Long> eventIDs = new HashSet<>();
/**
* the ids of the subset of aggregated events that have at least one tag
* applied to them
*/
private final Set<Long> tagged = new HashSet<>();
/**
* the ids of the subset of aggregated events that have at least one hash
* set hit
*/
private final Set<Long> hashHits = new HashSet<>();
public EventStripe(EventCluster cluster) {
spans.add(cluster.getRange());
spanMap.put(cluster.getRange(), cluster);
type = cluster.getEventType();
description = cluster.getDescription();
lod = cluster.getDescriptionLOD();
eventIDs.addAll(cluster.getEventIDs());
tagged.addAll(cluster.getEventIDsWithTags());
hashHits.addAll(cluster.getEventIDsWithHashHits());
}
private EventStripe(EventStripe u, EventStripe v) {
spans.addAll(u.spans);
spans.addAll(v.spans);
spanMap.putAll(u.spanMap);
spanMap.putAll(v.spanMap);
type = u.getEventType();
description = u.getDescription();
lod = u.getDescriptionLOD();
eventIDs.addAll(u.getEventIDs());
eventIDs.addAll(v.getEventIDs());
tagged.addAll(u.getEventIDsWithTags());
tagged.addAll(v.getEventIDsWithTags());
hashHits.addAll(u.getEventIDsWithHashHits());
hashHits.addAll(v.getEventIDsWithHashHits());
}
public static EventStripe merge(EventStripe u, EventStripe v) {
Preconditions.checkNotNull(u);
Preconditions.checkNotNull(v);
Preconditions.checkArgument(Objects.equal(u.description, v.description));
Preconditions.checkArgument(Objects.equal(u.lod, v.lod));
Preconditions.checkArgument(Objects.equal(u.type, v.type));
return new EventStripe(u, v);
}
@Override
public String getDescription() {
return description;
}
@Override
public EventType getEventType() {
return type;
}
@Override
public DescriptionLOD getDescriptionLOD() {
return lod;
}
@Override
public Set<Long> getEventIDs() {
return Collections.unmodifiableSet(eventIDs);
}
@Override
public Set<Long> getEventIDsWithHashHits() {
return Collections.unmodifiableSet(hashHits);
}
@Override
public Set<Long> getEventIDsWithTags() {
return Collections.unmodifiableSet(tagged);
}
@Override
public long getStartMillis() {
return spans.span().lowerEndpoint();
}
@Override
public long getEndMillis() {
return spans.span().upperEndpoint();
}
@Override
public Iterable<Range<Long>> getRanges() {
return spans.asRanges();
}
}

View File

@ -326,7 +326,7 @@ public final class FilteredEventsModel {
* range and pass the requested filter, using the given aggregation
* to control the grouping of events
*/
public List<AggregateEvent> getAggregatedEvents() {
public List<EventCluster> getAggregatedEvents() {
final Interval range;
final RootFilter filter;
final EventTypeZoomLevel zoom;
@ -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<AggregateEvent> 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

@ -54,7 +54,7 @@ import org.joda.time.Period;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.BaseTypes;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
@ -413,10 +413,7 @@ public class EventDB {
try (ResultSet rs = getDataSourceIDsStmt.executeQuery()) {
while (rs.next()) {
long datasourceID = rs.getLong("datasource_id");
//this relies on the fact that no tskObj has ID 0 but 0 is the default value for the datasource_id column in the events table.
if (datasourceID != 0) {
hashSet.add(datasourceID);
}
hashSet.add(datasourceID);
}
} catch (SQLException ex) {
LOGGER.log(Level.SEVERE, "Failed to get MAX time.", ex); // NON-NLS
@ -583,6 +580,10 @@ public class EventDB {
initializeTagsTable();
createIndex("events", Arrays.asList("datasource_id"));
createIndex("events", Arrays.asList("event_id", "hash_hit"));
createIndex("events", Arrays.asList("event_id", "tagged"));
createIndex("events", Arrays.asList("file_id"));
createIndex("events", Arrays.asList("file_id"));
createIndex("events", Arrays.asList("artifact_id"));
createIndex("events", Arrays.asList("time"));
@ -595,7 +596,7 @@ public class EventDB {
"INSERT INTO events (datasource_id,file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state, hash_hit, tagged) " // NON-NLS
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"); // NON-NLS
getHashSetNamesStmt = prepareStatement("SELECT hash_set_id, hash_set_name FROM hash_sets"); // NON-NLS
getDataSourceIDsStmt = prepareStatement("SELECT DISTINCT datasource_id FROM events"); // NON-NLS
getDataSourceIDsStmt = prepareStatement("SELECT DISTINCT datasource_id FROM events WHERE datasource_id != 0"); // NON-NLS
getMaxTimeStmt = prepareStatement("SELECT Max(time) AS max FROM events"); // NON-NLS
getMinTimeStmt = prepareStatement("SELECT Min(time) AS min FROM events"); // NON-NLS
getEventByIDStmt = prepareStatement("SELECT * FROM events WHERE event_id = ?"); // NON-NLS
@ -1031,7 +1032,7 @@ public class EventDB {
}
/**
* get a list of {@link AggregateEvent}s, clustered according to the given
* get a list of {@link EventCluster}s, clustered according to the given
* zoom paramaters.
*
* @param params the zoom params that determine the zooming, filtering and
@ -1041,7 +1042,7 @@ public class EventDB {
* the supplied filter, aggregated according to the given event type
* and description zoom levels
*/
List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
List<EventCluster> getClusteredEvents(ZoomParams params) {
//unpack params
Interval timeRange = params.getTimeRange();
RootFilter filter = params.getFilter();
@ -1067,8 +1068,8 @@ public class EventDB {
//compose query string, new-lines only for nicer formatting if printing the entire query
String query = "SELECT strftime('" + strfTimeFormat + "',time , 'unixepoch'" + timeZone + ") AS interval," // NON-NLS
+ "\n group_concat(events.event_id) as event_ids,"
+ "\n group_concat(CASE WHEN hash_hit = 1 THEN event_id ELSE NULL END) as hash_hits,"
+ "\n group_concat(CASE WHEN tagged = 1 THEN event_id ELSE NULL END) as taggeds,"
+ "\n group_concat(CASE WHEN hash_hit = 1 THEN events.event_id ELSE NULL END) as hash_hits,"
+ "\n group_concat(CASE WHEN tagged = 1 THEN events.event_id ELSE NULL END) as taggeds,"
+ "\n min(time), max(time), " + typeColumn + ", " + descriptionColumn // NON-NLS
+ "\n FROM events" + useHashHitTablesHelper(filter) + useTagTablesHelper(filter) // NON-NLS
+ "\n WHERE time >= " + start + " AND time < " + end + " AND " + SQLHelper.getSQLWhere(filter) // NON-NLS
@ -1076,14 +1077,14 @@ public class EventDB {
+ "\n ORDER BY min(time)"; // NON-NLS
// perform query and map results to AggregateEvent objects
List<AggregateEvent> events = new ArrayList<>();
List<EventCluster> events = new ArrayList<>();
DBLock.lock();
try (Statement createStatement = con.createStatement();
ResultSet rs = createStatement.executeQuery(query)) {
while (rs.next()) {
events.add(aggregateEventHelper(rs, useSubTypes, descriptionLOD, filter.getTagsFilter()));
events.add(eventClusterHelper(rs, useSubTypes, descriptionLOD, filter.getTagsFilter()));
}
} catch (SQLException ex) {
LOGGER.log(Level.SEVERE, "Failed to get aggregate events with query: " + query, ex); // NON-NLS
@ -1091,11 +1092,11 @@ public class EventDB {
DBLock.unlock();
}
return mergeAggregateEvents(rangeInfo.getPeriodSize().getPeriod(), events);
return mergeEventClusters(rangeInfo.getPeriodSize().getPeriod(), events);
}
/**
* map a single row in a ResultSet to an AggregateEvent
* map a single row in a ResultSet to an EventCluster
*
* @param rs the result set whose current row should be mapped
* @param useSubTypes use the sub_type column if true, else use the
@ -1107,7 +1108,7 @@ public class EventDB {
*
* @throws SQLException
*/
private AggregateEvent aggregateEventHelper(ResultSet rs, boolean useSubTypes, DescriptionLOD descriptionLOD, TagsFilter filter) throws SQLException {
private EventCluster eventClusterHelper(ResultSet rs, boolean useSubTypes, DescriptionLOD descriptionLOD, TagsFilter filter) throws SQLException {
Interval interval = new Interval(rs.getLong("min(time)") * 1000, rs.getLong("max(time)") * 1000, TimeLineController.getJodaTimeZone());// NON-NLS
String eventIDsString = rs.getString("event_ids");// NON-NLS
Set<Long> eventIDs = SQLHelper.unGroupConcat(eventIDsString, Long::valueOf);
@ -1117,7 +1118,7 @@ public class EventDB {
Set<Long> hashHits = SQLHelper.unGroupConcat(rs.getString("hash_hits"), Long::valueOf);
Set<Long> tagged = SQLHelper.unGroupConcat(rs.getString("taggeds"), Long::valueOf);
return new AggregateEvent(interval, type, eventIDs, hashHits, tagged,
return new EventCluster(interval, type, eventIDs, hashHits, tagged,
description, descriptionLOD);
}
@ -1134,36 +1135,36 @@ public class EventDB {
*
* @return
*/
static private List<AggregateEvent> mergeAggregateEvents(Period timeUnitLength, List<AggregateEvent> preMergedEvents) {
static private List<EventCluster> mergeEventClusters(Period timeUnitLength, List<EventCluster> preMergedEvents) {
//effectively map from type to (map from description to events)
Map<EventType, SetMultimap< String, AggregateEvent>> typeMap = new HashMap<>();
Map<EventType, SetMultimap< String, EventCluster>> typeMap = new HashMap<>();
for (AggregateEvent aggregateEvent : preMergedEvents) {
typeMap.computeIfAbsent(aggregateEvent.getType(), eventType -> HashMultimap.create())
for (EventCluster aggregateEvent : preMergedEvents) {
typeMap.computeIfAbsent(aggregateEvent.getEventType(), eventType -> HashMultimap.create())
.put(aggregateEvent.getDescription(), aggregateEvent);
}
//result list to return
ArrayList<AggregateEvent> aggEvents = new ArrayList<>();
ArrayList<EventCluster> aggEvents = new ArrayList<>();
//For each (type, description) key, merge agg events
for (SetMultimap<String, AggregateEvent> descrMap : typeMap.values()) {
for (SetMultimap<String, EventCluster> descrMap : typeMap.values()) {
//for each description ...
for (String descr : descrMap.keySet()) {
//run through the sorted events, merging together adjacent events
Iterator<AggregateEvent> iterator = descrMap.get(descr).stream()
Iterator<EventCluster> iterator = descrMap.get(descr).stream()
.sorted(Comparator.comparing(event -> event.getSpan().getStartMillis()))
.iterator();
AggregateEvent current = iterator.next();
EventCluster current = iterator.next();
while (iterator.hasNext()) {
AggregateEvent next = iterator.next();
EventCluster next = iterator.next();
Interval gap = current.getSpan().gap(next.getSpan());
//if they overlap or gap is less one quarter timeUnitLength
//TODO: 1/4 factor is arbitrary. review! -jm
if (gap == null || gap.toDuration().getMillis() <= timeUnitLength.toDurationFrom(gap.getStart()).getMillis() / 4) {
//merge them
current = AggregateEvent.merge(current, next);
current = EventCluster.merge(current, next);
} else {
//done merging into current, set next as new current
aggEvents.add(current);

View File

@ -45,7 +45,7 @@ import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.services.TagsManager;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.ProgressWindow;
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.ArtifactEventType;
@ -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<AggregateEvent>> 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,10 +146,10 @@ 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::getAggregatedEvents));
).build(CacheLoader.from(eventDB::getClusteredEvents));
maxCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMaxTime));
minCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMinTime));
this.modelInstance = new FilteredEventsModel(this, currentStateProperty);
@ -206,8 +206,8 @@ public class EventsRepository {
}
synchronized public List<AggregateEvent> 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());
@ -487,26 +487,29 @@ public class EventsRepository {
final String uniquePath = f.getUniquePath();
final String parentPath = f.getParentPath();
long datasourceID = f.getDataSource().getId();
String datasourceName = StringUtils.substringBefore(StringUtils.stripStart(uniquePath, "/"), parentPath);
String rootFolder = StringUtils.substringBetween(parentPath, "/", "/");
String shortDesc = datasourceName + "/" + StringUtils.defaultIfBlank(rootFolder, "");
String medD = datasourceName + parentPath;
String datasourceName = StringUtils.substringBeforeLast(uniquePath, parentPath);
String rootFolder = StringUtils.substringBefore(StringUtils.substringAfter(parentPath, "/"), "/");
String shortDesc = datasourceName + "/" + StringUtils.defaultString(rootFolder);
shortDesc = shortDesc.endsWith("/") ? shortDesc : shortDesc + "/";
String medDesc = datasourceName + parentPath;
final TskData.FileKnown known = f.getKnown();
Set<String> hashSets = f.getHashSetNames();
List<ContentTag> tags = tagsManager.getContentTagsByContent(f);
//insert it into the db if time is > 0 => time is legitimate (drops logical files)
if (f.getAtime() > 0) {
eventDB.insertEvent(f.getAtime(), FileSystemTypes.FILE_ACCESSED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans);
eventDB.insertEvent(f.getAtime(), FileSystemTypes.FILE_ACCESSED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans);
}
if (f.getMtime() > 0) {
eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans);
eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans);
}
if (f.getCtime() > 0) {
eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans);
eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans);
}
if (f.getCrtime() > 0) {
eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans);
eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans);
}
publish(new ProgressWindow.ProgressUpdate(i, numFiles,

View File

@ -30,6 +30,7 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType;
import org.sleuthkit.autopsy.timeline.filters.AbstractFilter;
import org.sleuthkit.autopsy.timeline.filters.DataSourceFilter;
import org.sleuthkit.autopsy.timeline.filters.DataSourcesFilter;
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
import org.sleuthkit.autopsy.timeline.filters.Filter;
import org.sleuthkit.autopsy.timeline.filters.HashHitsFilter;
import org.sleuthkit.autopsy.timeline.filters.HashSetFilter;
@ -105,10 +106,20 @@ public class SQLHelper {
return getSQLWhere((IntersectionFilter) filter);
}
/**
* NOTE: I don't like this if-else instance of chain, but I can't decide
* what to do instead -jm
*
* @param filter
*
* @return
*/
private static String getSQLWhere(Filter filter) {
String result = "";
if (filter == null) {
return "1";
} else if (filter instanceof DescriptionFilter) {
result = getSQLWhere((DescriptionFilter) filter);
} else if (filter instanceof TagsFilter) {
result = getSQLWhere((TagsFilter) filter);
} else if (filter instanceof HashHitsFilter) {
@ -130,7 +141,7 @@ public class SQLHelper {
} else if (filter instanceof UnionFilter) {
result = getSQLWhere((UnionFilter) filter);
} else {
return "1";
throw new IllegalArgumentException("getSQLWhere not defined for " + filter.getClass().getCanonicalName());
}
result = StringUtils.deleteWhitespace(result).equals("(1and1and1)") ? "1" : result;
result = StringUtils.deleteWhitespace(result).equals("()") ? "1" : result;
@ -145,6 +156,14 @@ public class SQLHelper {
}
}
private static String getSQLWhere(DescriptionFilter filter) {
if (filter.isSelected()) {
return "(" + getDescriptionColumn(filter.getDescriptionLoD()) + " LIKE '" + filter.getDescription() + "')"; // NON-NLS
} else {
return "1";
}
}
private static String getSQLWhere(TagsFilter filter) {
if (filter.isSelected()
&& (false == filter.isDisabled())

View File

@ -0,0 +1,53 @@
/*
* 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.
*/
package org.sleuthkit.autopsy.timeline.filters;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
public class DescriptionFilter extends AbstractFilter {
private final DescriptionLOD descriptionLoD;
private final String description;
public DescriptionFilter(DescriptionLOD descriptionLoD, String description) {
this.descriptionLoD = descriptionLoD;
this.description = description;
}
@Override
public DescriptionFilter copyOf() {
DescriptionFilter filterCopy = new DescriptionFilter(getDescriptionLoD(), getDescription());
filterCopy.setSelected(isSelected());
filterCopy.setDisabled(isDisabled());
return filterCopy;
}
@Override
public String getDisplayName() {
return "description";
}
@Override
public String getHTMLReportString() {
return getDescriptionLoD().getDisplayName() + " " + getDisplayName() + " = " + getDescription();
}
/**
* @return the descriptionLoD
*/
public DescriptionLOD getDescriptionLoD() {
return descriptionLoD;
}
/**
* @return the description
*/
public String getDescription() {
return description;
}
}

View File

@ -79,4 +79,6 @@ public interface Filter {
SimpleBooleanProperty getDisabledProperty();
boolean isDisabled();
}

View File

@ -73,7 +73,7 @@ import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent;
* {@link XYChart} doing the rendering. Is this a good idea? -jm TODO: pull up
* common history context menu items out of derived classes? -jm
*/
public abstract class AbstractVisualization<X, Y, N extends Node, C extends XYChart<X, Y> & TimeLineChart<X>> extends BorderPane implements TimeLineView {
public abstract class AbstractVisualization<X, Y, N, C extends XYChart<X, Y> & TimeLineChart<X>> extends BorderPane implements TimeLineView {
protected final SimpleBooleanProperty hasEvents = new SimpleBooleanProperty(true);
@ -173,8 +173,8 @@ public abstract class AbstractVisualization<X, Y, N extends Node, C extends XYCh
protected abstract Axis<Y> getYAxis();
/**
* update this visualization based on current state of zoom /
* filters. Primarily this invokes the background {@link Task} returned by
* update this visualization based on current state of zoom / filters.
* Primarily this invokes the background {@link Task} returned by
* {@link #getUpdateTask()} which derived classes must implement.
*/
synchronized public void update() {

View File

@ -0,0 +1,554 @@
/*
* 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;
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import org.apache.commons.lang3.StringUtils;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.LoggedTask;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
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;
public abstract class AbstractDetailViewNode< T extends EventBundle, S extends AbstractDetailViewNode<T, S>> extends StackPane implements DetailViewNode<AbstractDetailViewNode<T, S>> {
static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png");
static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS
static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS
static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS
static final CornerRadii CORNER_RADII = new CornerRadii(3);
/**
* the border to apply when this node is 'selected'
*/
static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2)));
private static final Logger LOGGER = Logger.getLogger(AbstractDetailViewNode.class
.getName());
static void configureLODButton(Button b) {
b.setMinSize(16, 16);
b.setMaxSize(16, 16);
b.setPrefSize(16, 16);
show(b, false);
}
static void show(Node b, boolean show) {
b.setVisible(show);
b.setManaged(show);
}
private final Map<EventType, DropShadow> dropShadowMap = new HashMap<>();
final Color evtColor;
private final S parentNode;
private DescriptionVisibility descrVis;
/**
* Pane that contains AggregateEventNodes of any 'subevents' if they are
* displayed
*
* //TODO: move more of the control of subnodes/events here and out of
* EventDetail Chart
*/
private final Pane subNodePane = new Pane();
Pane getSubNodePane() {
return subNodePane;
}
/**
* The ImageView used to show the icon for this node's event's type
*/
private final ImageView eventTypeImageView = new ImageView();
/**
* The label used to display this node's event's description
*/
final Label descrLabel = new Label();
/**
* The label used to display this node's event count
*/
final Label countLabel = new Label();
private final T eventBundle;
private final EventDetailChart chart;
private final SleuthkitCase sleuthkitCase;
SleuthkitCase getSleuthkitCase() {
return sleuthkitCase;
}
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;
Region getSpacer() {
return spacer;
}
private final Region spacer = new Region();
private final CollapseClusterAction collapseClusterAction;
private final ExpandClusterAction expandClusterAction;
public AbstractDetailViewNode(EventDetailChart chart, T bundle, S parentEventNode) {
this.eventBundle = bundle;
this.parentNode = parentEventNode;
this.chart = chart;
descLOD.set(bundle.getDescriptionLOD());
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
eventsModel = chart.getController().getEventsModel();
ImageView hashIV = new ImageView(HASH_PIN);
ImageView tagIV = new ImageView(TAG);
if (eventBundle.getEventIDsWithHashHits().isEmpty()) {
show(hashIV, false);
}
if (eventBundle.getEventIDsWithTags().isEmpty()) {
show(tagIV, false);
}
expandClusterAction = new ExpandClusterAction();
plusButton = ActionUtils.createButton(expandClusterAction, ActionUtils.ActionTextBehavior.HIDE);
configureLODButton(plusButton);
collapseClusterAction = new CollapseClusterAction();
minusButton = ActionUtils.createButton(collapseClusterAction, ActionUtils.ActionTextBehavior.HIDE);
configureLODButton(minusButton);
HBox.setHgrow(spacer, Priority.ALWAYS);
header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, minusButton, plusButton);
header.setMinWidth(USE_PREF_SIZE);
header.setPadding(new Insets(2, 5, 2, 5));
header.setAlignment(Pos.CENTER_LEFT);
//setup description label
evtColor = getEventType().getColor();
eventTypeImageView.setImage(getEventType().getFXImage());
descrLabel.setGraphic(eventTypeImageView);
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
descrLabel.setMouseTransparent(true);
//set up subnode pane sizing contraints
subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
subNodePane.setMinHeight(USE_PREF_SIZE);
subNodePane.setMinWidth(USE_PREF_SIZE);
subNodePane.setMaxHeight(USE_PREF_SIZE);
subNodePane.setMaxWidth(USE_PREF_SIZE);
subNodePane.setPickOnBounds(false);
setAlignment(Pos.TOP_LEFT);
setMinHeight(24);
setPrefHeight(USE_COMPUTED_SIZE);
setMaxHeight(USE_PREF_SIZE);
setOnMouseClicked(new EventMouseHandler());
//set up mouse hover effect and tooltip
setOnMouseEntered((MouseEvent e) -> {
//defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
installTooltip();
showDescriptionLoDControls(true);
toFront();
});
setOnMouseExited((MouseEvent e) -> {
showDescriptionLoDControls(false);
});
setCursor(Cursor.HAND);
setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
setLayoutX(getChart().getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation());
}
@Override
@SuppressWarnings("unchecked")
public List<S> getSubNodes() {
return subNodePane.getChildrenUnmodifiable().stream()
.map(t -> (S) t)
.collect(Collectors.toList());
}
/**
* apply the 'effect' to visually indicate selection
*
* @param applied true to apply the selection 'effect', false to remove it
*/
@Override
public void applySelectionEffect(boolean applied) {
Platform.runLater(() -> {
if (applied) {
setBorder(selectionBorder);
} else {
setBorder(null);
}
});
}
/**
*
* @param showControls the value of par
*/
void showDescriptionLoDControls(final boolean showControls) {
DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
eventType -> new DropShadow(10, eventType.getColor()));
getSpanFillNode().setEffect(showControls ? dropShadow : null);
show(minusButton, showControls);
show(plusButton, showControls);
}
/**
* make a new filter intersecting the global filter with description and
* type filters to restrict sub-clusters
*
*/
RootFilter getSubClusterFilter() {
RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf();
subClusterFilter.getSubFilters().addAll(
new DescriptionFilter(getEventBundle().getDescriptionLOD(), getDescription()),
new TypeFilter(getEventType()));
return subClusterFilter;
}
abstract Collection<T> makeBundlesFromClusters(List<EventCluster> eventClusters);
abstract void showSpans(final boolean showSpans);
/**
* @param w the maximum width the description label should have
*/
@Override
public void setDescriptionWidth(double w) {
getDescrLabel().setMaxWidth(w);
}
abstract void installTooltip();
/**
* apply the 'effect' to visually indicate highlighted nodes
*
* @param applied true to apply the highlight 'effect', false to remove it
*/
@Override
public synchronized void applyHighlightEffect(boolean applied) {
if (applied) {
getDescrLabel().setStyle("-fx-font-weight: bold;"); // NON-NLS
getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY)));
setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
} else {
getDescrLabel().setStyle("-fx-font-weight: normal;"); // NON-NLS
getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
}
}
String getDisplayedDescription() {
return getDescrLabel().getText();
}
abstract Region getSpanFillNode();
Button getPlusButton() {
return plusButton;
}
Button getMinusButton() {
return minusButton;
}
public final Label getDescrLabel() {
return descrLabel;
}
final public Label getCountLabel() {
return countLabel;
}
public S getParentNode() {
return parentNode;
}
@Override
public final T getEventBundle() {
return eventBundle;
}
public final EventDetailChart getChart() {
return chart;
}
public DescriptionLOD getDescLOD() {
return descLOD.get();
}
/**
* loads sub-bundles at the given Description LOD, continues
*
* @param requestedDescrLoD
* @param expand
*/
private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) {
subNodePane.getChildren().clear();
if (descLOD.get().withRelativeDetail(relativeDetail) == getEventBundle().getDescriptionLOD()) {
descLOD.set(getEventBundle().getDescriptionLOD());
showSpans(true);
chart.setRequiresLayout(true);
chart.requestChartLayout();
} else {
showSpans(false);
// 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());
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 {
do {
loadedDescriptionLoD = next;
if (loadedDescriptionLoD == getEventBundle().getDescriptionLOD()) {
return Collections.emptyList();
}
bundles = loadBundles();
next = loadedDescriptionLoD.withRelativeDetail(relativeDetail);
} while (bundles.size() == 1 && nonNull(next));
// 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 {
List<S> subBundleNodes = get();
if (subBundleNodes.isEmpty()) {
showSpans(true);
} else {
showSpans(false);
}
descLOD.set(loadedDescriptionLoD);
//assign subNodes and request chart layout
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
chart.getController().monitorTask(loggedTask);
}
}
final double getLayoutXCompensation() {
return (getParentNode() != null ? getParentNode().getLayoutXCompensation() : 0)
+ getBoundsInParent().getMinX();
}
@Override
public final void setDescriptionVisibility(DescriptionVisibility descrVis) {
this.descrVis = descrVis;
final int size = getEventBundle().getEventIDs().size();
switch (this.descrVis) {
case COUNT_ONLY:
descrLabel.setText("");
countLabel.setText(String.valueOf(size));
break;
case HIDDEN:
countLabel.setText("");
descrLabel.setText("");
break;
default:
case SHOWN:
String description = getEventBundle().getDescription();
description = getParentNode() != null
? " ..." + StringUtils.substringAfter(description, getParentNode().getDescription())
: description;
descrLabel.setText(description);
countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
break;
}
}
abstract S getNodeForBundle(T bundle);
/**
* event handler used for mouse events on {@link AggregateEventNode}s
*/
private class EventMouseHandler implements EventHandler<MouseEvent> {
private ContextMenu contextMenu;
@Override
public void handle(MouseEvent t) {
if (t.getButton() == MouseButton.PRIMARY) {
t.consume();
if (t.isShiftDown()) {
if (chart.selectedNodes.contains(AbstractDetailViewNode.this) == false) {
chart.selectedNodes.add(AbstractDetailViewNode.this);
}
} else if (t.isShortcutDown()) {
chart.selectedNodes.removeAll(AbstractDetailViewNode.this);
} else if (t.getClickCount() > 1) {
final DescriptionLOD next = descLOD.get().moreDetailed();
if (next != null) {
loadSubBundles(DescriptionLOD.RelativeDetail.MORE);
}
} else {
chart.selectedNodes.setAll(AbstractDetailViewNode.this);
}
t.consume();
} else if (t.getButton() == MouseButton.SECONDARY) {
ContextMenu chartContextMenu = chart.getChartContextMenu(t);
if (contextMenu == null) {
contextMenu = new ContextMenu();
contextMenu.setAutoHide(true);
contextMenu.getItems().add(ActionUtils.createMenuItem(expandClusterAction));
contextMenu.getItems().add(ActionUtils.createMenuItem(collapseClusterAction));
contextMenu.getItems().add(new SeparatorMenuItem());
contextMenu.getItems().addAll(chartContextMenu.getItems());
}
contextMenu.show(AbstractDetailViewNode.this, t.getScreenX(), t.getScreenY());
t.consume();
}
}
}
private class ExpandClusterAction extends Action {
ExpandClusterAction() {
super("Expand");
setGraphic(new ImageView(PLUS));
setEventHandler((ActionEvent t) -> {
final DescriptionLOD next = descLOD.get().moreDetailed();
if (next != null) {
loadSubBundles(DescriptionLOD.RelativeDetail.MORE);
}
});
disabledProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL));
}
}
private class CollapseClusterAction extends Action {
CollapseClusterAction() {
super("Collapse");
setGraphic(new ImageView(MINUS));
setEventHandler((ActionEvent t) -> {
final DescriptionLOD previous = descLOD.get().lessDetailed();
if (previous != null) {
loadSubBundles(DescriptionLOD.RelativeDetail.LESS);
}
});
disabledProperty().bind(descLOD.isEqualTo(getEventBundle().getDescriptionLOD()));
}
}
}

View File

@ -1,523 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-15 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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.Tooltip;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
import org.sleuthkit.autopsy.coreutils.LoggedTask;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.filters.RootFilter;
import org.sleuthkit.autopsy.timeline.filters.TextFilter;
import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Represents an {@link AggregateEvent} in a {@link EventDetailChart}.
*/
public class AggregateEventNode extends StackPane {
private static final Logger LOGGER = Logger.getLogger(AggregateEventNode.class.getName());
private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png");
private final static Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS
private final static Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS
private final static Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS
private static final CornerRadii CORNER_RADII = new CornerRadii(3);
/**
* the border to apply when this node is 'selected'
*/
private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2)));
/**
* The event this AggregateEventNode represents visually
*/
private AggregateEvent aggEvent;
private final AggregateEventNode parentEventNode;
/**
* the region that represents the time span of this node's event
*/
private final Region spanRegion = new Region();
/**
* The label used to display this node's event's description
*/
private final Label descrLabel = new Label();
/**
* The label used to display this node's event count
*/
private final Label countLabel = new Label();
/**
* The IamgeView used to show the icon for this node's event's type
*/
private final ImageView eventTypeImageView = new ImageView();
/**
* Pane that contains AggregateEventNodes of any 'subevents' if they are
* displayed
*
* //TODO: move more of the control of subnodes/events here and out of
* EventDetail Chart
*/
private final Pane subNodePane = new Pane();
/**
* the context menu that with the slider that controls subnode/event display
*
* //TODO: move more of the control of subnodes/events here and out of
* EventDetail Chart
*/
private final SimpleObjectProperty<ContextMenu> contextMenu = new SimpleObjectProperty<>();
/**
* the Background used to fill the spanRegion, this varies epending on the
* selected/highlighted state of this node in its parent EventDetailChart
*/
private Background spanFill;
private final Button plusButton = new Button(null, new ImageView(PLUS)) {
{
setMinSize(16, 16);
setMaxSize(16, 16);
setPrefSize(16, 16);
}
};
private final Button minusButton = new Button(null, new ImageView(MINUS)) {
{
setMinSize(16, 16);
setMaxSize(16, 16);
setPrefSize(16, 16);
}
};
private final EventDetailChart chart;
private SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
private DescriptionVisibility descrVis;
private final SleuthkitCase sleuthkitCase;
private final FilteredEventsModel eventsModel;
private Tooltip tooltip;
private final ImageView hashIV = new ImageView(HASH_PIN);
private final ImageView tagIV = new ImageView(TAG);
public AggregateEventNode(final AggregateEvent aggEvent, AggregateEventNode parentEventNode, EventDetailChart chart) {
this.aggEvent = aggEvent;
descLOD.set(aggEvent.getLOD());
this.parentEventNode = parentEventNode;
this.chart = chart;
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
eventsModel = chart.getController().getEventsModel();
final Region region = new Region();
HBox.setHgrow(region, Priority.ALWAYS);
final HBox hBox = new HBox(descrLabel, countLabel, region, hashIV, tagIV, minusButton, plusButton);
if (aggEvent.getEventIDsWithHashHits().isEmpty()) {
hashIV.setManaged(false);
hashIV.setVisible(false);
}
if (aggEvent.getEventIDsWithTags().isEmpty()) {
tagIV.setManaged(false);
tagIV.setVisible(false);
}
hBox.setPrefWidth(USE_COMPUTED_SIZE);
hBox.setMinWidth(USE_PREF_SIZE);
hBox.setPadding(new Insets(2, 5, 2, 5));
hBox.setAlignment(Pos.CENTER_LEFT);
minusButton.setVisible(false);
plusButton.setVisible(false);
minusButton.setManaged(false);
plusButton.setManaged(false);
final BorderPane borderPane = new BorderPane(subNodePane, hBox, null, null, null);
BorderPane.setAlignment(subNodePane, Pos.TOP_LEFT);
borderPane.setPrefWidth(USE_COMPUTED_SIZE);
getChildren().addAll(spanRegion, borderPane);
setAlignment(Pos.TOP_LEFT);
setMinHeight(24);
minWidthProperty().bind(spanRegion.widthProperty());
setPrefHeight(USE_COMPUTED_SIZE);
setMaxHeight(USE_PREF_SIZE);
//set up subnode pane sizing contraints
subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
subNodePane.setMinHeight(USE_PREF_SIZE);
subNodePane.setMinWidth(USE_PREF_SIZE);
subNodePane.setMaxHeight(USE_PREF_SIZE);
subNodePane.setMaxWidth(USE_PREF_SIZE);
subNodePane.setPickOnBounds(false);
//setup description label
eventTypeImageView.setImage(aggEvent.getType().getFXImage());
descrLabel.setGraphic(eventTypeImageView);
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
descrLabel.setMouseTransparent(true);
setDescriptionVisibility(chart.getDescrVisibility().get());
//setup backgrounds
final Color evtColor = aggEvent.getType().getColor();
spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
setCursor(Cursor.HAND);
spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS
spanRegion.setBackground(spanFill);
//set up mouse hover effect and tooltip
setOnMouseEntered((MouseEvent e) -> {
//defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
installTooltip();
spanRegion.setEffect(new DropShadow(10, evtColor));
minusButton.setVisible(true);
plusButton.setVisible(true);
minusButton.setManaged(true);
plusButton.setManaged(true);
toFront();
});
setOnMouseExited((MouseEvent e) -> {
spanRegion.setEffect(null);
minusButton.setVisible(false);
plusButton.setVisible(false);
minusButton.setManaged(false);
plusButton.setManaged(false);
});
setOnMouseClicked(new EventMouseHandler());
plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL));
minusButton.disableProperty().bind(descLOD.isEqualTo(aggEvent.getLOD()));
plusButton.setOnMouseClicked(e -> {
final DescriptionLOD next = descLOD.get().next();
if (next != null) {
loadSubClusters(next);
descLOD.set(next);
}
});
minusButton.setOnMouseClicked(e -> {
final DescriptionLOD previous = descLOD.get().previous();
if (previous != null) {
loadSubClusters(previous);
descLOD.set(previous);
}
});
}
synchronized private void installTooltip() {
//TODO: all this work should probably go on a background thread...
if (tooltip == null) {
HashMap<String, Long> hashSetCounts = new HashMap<>();
if (!aggEvent.getEventIDsWithHashHits().isEmpty()) {
hashSetCounts = new HashMap<>();
try {
for (TimeLineEvent tle : eventsModel.getEventsById(aggEvent.getEventIDsWithHashHits())) {
Set<String> hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames();
for (String hashSetName : hashSetNames) {
hashSetCounts.merge(hashSetName, 1L, Long::sum);
}
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex);
}
}
Map<String, Long> tagCounts = new HashMap<>();
if (!aggEvent.getEventIDsWithTags().isEmpty()) {
tagCounts.putAll( eventsModel.getTagCountsByTagName(aggEvent.getEventIDsWithTags()));
}
String hashSetCountsString = hashSetCounts.entrySet().stream()
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
.collect(Collectors.joining("\n"));
String tagCountsString = tagCounts.entrySet().stream()
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
.collect(Collectors.joining("\n"));
tooltip = new Tooltip(
NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text",
getEvent().getEventIDs().size(), getEvent().getType(), getEvent().getDescription(),
getEvent().getSpan().getStart().toString(TimeLineController.getZonedFormatter()),
getEvent().getSpan().getEnd().toString(TimeLineController.getZonedFormatter()))
+ (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString)
+ (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString)
);
Tooltip.install(AggregateEventNode.this, tooltip);
}
}
public Pane getSubNodePane() {
return subNodePane;
}
synchronized public AggregateEvent getEvent() {
return aggEvent;
}
/**
* sets the width of the {@link Region} with border and background used to
* indicate the temporal span of this aggregate event
*
* @param w
*/
public void setSpanWidth(double w) {
spanRegion.setPrefWidth(w);
spanRegion.setMaxWidth(w);
spanRegion.setMinWidth(Math.max(2, w));
}
/**
*
* @param w the maximum width the description label should have
*/
public void setDescriptionWidth(double w) {
descrLabel.setMaxWidth(w);
}
/**
* @param descrVis the level of description that should be displayed
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
synchronized final void setDescriptionVisibility(DescriptionVisibility descrVis) {
this.descrVis = descrVis;
final int size = aggEvent.getEventIDs().size();
switch (descrVis) {
case COUNT_ONLY:
descrLabel.setText("");
countLabel.setText(String.valueOf(size));
break;
case HIDDEN:
countLabel.setText("");
descrLabel.setText("");
break;
default:
case SHOWN:
String description = aggEvent.getDescription();
description = parentEventNode != null
? " ..." + StringUtils.substringAfter(description, parentEventNode.getEvent().getDescription())
: description;
descrLabel.setText(description);
countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
break;
}
}
/**
* apply the 'effect' to visually indicate selection
*
* @param applied true to apply the selection 'effect', false to remove it
*/
void applySelectionEffect(final boolean applied) {
Platform.runLater(() -> {
if (applied) {
setBorder(selectionBorder);
} else {
setBorder(null);
}
});
}
/**
* apply the 'effect' to visually indicate highlighted nodes
*
* @param applied true to apply the highlight 'effect', false to remove it
*/
synchronized void applyHighlightEffect(boolean applied) {
if (applied) {
descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS
spanFill = new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY));
spanRegion.setBackground(spanFill);
setBackground(new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
} else {
descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS
spanFill = new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
spanRegion.setBackground(spanFill);
setBackground(new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
}
}
String getDisplayedDescription() {
return descrLabel.getText();
}
double getLayoutXCompensation() {
return (parentEventNode != null ? parentEventNode.getLayoutXCompensation() : 0)
+ getBoundsInParent().getMinX();
}
/**
* @return the contextMenu
*/
public ContextMenu getContextMenu() {
return contextMenu.get();
}
/**
* @param contextMenu the contextMenu to set
*/
public void setContextMenu(ContextMenu contextMenu) {
this.contextMenu.set(contextMenu);
}
/**
* loads sub-clusters at the given Description LOD
*
* @param newDescriptionLOD
*/
synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) {
getSubNodePane().getChildren().clear();
if (newDescriptionLOD == aggEvent.getLOD()) {
chart.setRequiresLayout(true);
chart.requestChartLayout();
} else {
RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf();
//make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters
combinedFilter.getSubFilters().addAll(new TextFilter(aggEvent.getDescription()),
new TypeFilter(aggEvent.getType()));
//make a new end inclusive span (to 'filter' with)
final Interval span = aggEvent.getSpan().withEndMillis(aggEvent.getSpan().getEndMillis() + 1000);
//make a task to load the subnodes
LoggedTask<List<AggregateEventNode>> loggedTask = new LoggedTask<List<AggregateEventNode>>(
NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) {
@Override
protected List<AggregateEventNode> call() throws Exception {
//query for the sub-clusters
List<AggregateEvent> aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span,
eventsModel.eventTypeZoomProperty().get(),
combinedFilter,
newDescriptionLOD));
//for each sub cluster make an AggregateEventNode to visually represent it, and set x-position
return aggregatedEvents.stream().map(aggEvent -> {
AggregateEventNode subNode = new AggregateEventNode(aggEvent, AggregateEventNode.this, chart);
subNode.setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis())) - getLayoutXCompensation());
return subNode;
}).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters
}
@Override
protected void succeeded() {
try {
chart.setCursor(Cursor.WAIT);
//assign subNodes and request chart layout
getSubNodePane().getChildren().setAll(get());
setDescriptionVisibility(descrVis);
chart.setRequiresLayout(true);
chart.requestChartLayout();
chart.setCursor(null);
} catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex);
}
}
};
//start task
chart.getController().monitorTask(loggedTask);
}
}
/**
* event handler used for mouse events on {@link AggregateEventNode}s
*/
private class EventMouseHandler implements EventHandler<MouseEvent> {
@Override
public void handle(MouseEvent t) {
if (t.getButton() == MouseButton.PRIMARY) {
t.consume();
if (t.isShiftDown()) {
if (chart.selectedNodes.contains(AggregateEventNode.this) == false) {
chart.selectedNodes.add(AggregateEventNode.this);
}
} else if (t.isShortcutDown()) {
chart.selectedNodes.removeAll(AggregateEventNode.this);
} else if (t.getClickCount() > 1) {
final DescriptionLOD next = descLOD.get().next();
if (next != null) {
loadSubClusters(next);
descLOD.set(next);
}
} else {
chart.selectedNodes.setAll(AggregateEventNode.this);
}
}
}
}
}

View File

@ -22,8 +22,9 @@ package org.sleuthkit.autopsy.timeline.ui.detailview;
* Level of description shown in UI NOTE: this is a separate concept form
* {@link DescriptionLOD}
*/
enum DescriptionVisibility {
HIDDEN, COUNT_ONLY, SHOWN;
public enum DescriptionVisibility {
HIDDEN,
COUNT_ONLY,
SHOWN;
}

View File

@ -0,0 +1,67 @@
/*
* 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.
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
/**
*
*/
public interface DetailViewNode<S extends DetailViewNode<S>> {
public void setDescriptionVisibility(DescriptionVisibility get);
public List<? extends S> getSubNodes();
public void setSpanWidths(List<Double> spanWidths);
public void setDescriptionWidth(double max);
public EventBundle getEventBundle();
/**
* apply the 'effect' to visually indicate highlighted nodes
*
* @param applied true to apply the highlight 'effect', false to remove it
*/
void applyHighlightEffect(boolean applied);
public void applySelectionEffect(boolean applied);
default String getDescription() {
return getEventBundle().getDescription();
}
default EventType getEventType() {
return getEventBundle().getEventType();
}
default Set<Long> getEventIDs() {
return getEventBundle().getEventIDs();
}
default public long getStartMillis() {
return getEventBundle().getStartMillis();
}
default long getEndMillis() {
return getEventBundle().getEndMillis();
}
static class StartTimeComparator implements Comparator<DetailViewNode<?>> {
@Override
public int compare(DetailViewNode<?> o1, DetailViewNode<?> o2) {
return Long.compare(o1.getStartMillis(), o2.getStartMillis());
}
}
}

View File

@ -30,7 +30,6 @@ import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Orientation;
import javafx.scene.Cursor;
@ -70,7 +69,7 @@ import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.FXMLConstructor;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.ui.AbstractVisualization;
@ -100,7 +99,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
* TODO: refactor common code out of this class and CountsChartPane into
* {@link AbstractVisualization}
*/
public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEvent, AggregateEventNode, EventDetailChart> {
public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster, DetailViewNode<?>, EventDetailChart> {
private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName());
@ -109,20 +108,20 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
//these three could be injected from fxml but it was causing npe's
private final DateAxis dateAxis = new DateAxis();
private final Axis<AggregateEvent> verticalAxis = new EventAxis();
private final Axis<EventCluster> verticalAxis = new EventAxis();
//private access to barchart data
private final Map<EventType, XYChart.Series<DateTime, AggregateEvent>> eventTypeToSeriesMap = new ConcurrentHashMap<>();
private final Map<EventType, XYChart.Series<DateTime, EventCluster>> eventTypeToSeriesMap = new ConcurrentHashMap<>();
private final ScrollBar vertScrollBar = new ScrollBar();
private final Region region = new Region();
private final ObservableList<AggregateEvent> aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
private final ObservableList<EventCluster> aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
private final ObservableList<AggregateEventNode> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
private final ObservableList<DetailViewNode<?>> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
public ObservableList<AggregateEvent> getAggregatedEvents() {
public ObservableList<EventCluster> getAggregatedEvents() {
return aggregatedEvents;
}
@ -149,7 +148,7 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
vertScrollBar.visibleAmountProperty().bind(chart.heightProperty().multiply(100).divide(chart.getMaxVScroll()));
requestLayout();
highlightedNodes.addListener((ListChangeListener.Change<? extends AggregateEventNode> change) -> {
highlightedNodes.addListener((ListChangeListener.Change<? extends DetailViewNode<?>> change) -> {
while (change.next()) {
change.getAddedSubList().forEach(aeNode -> {
aeNode.applyHighlightEffect(true);
@ -166,7 +165,7 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
//These scroll related handlers don't affect any other view or the model, so they are handled internally
//mouse wheel scroll handler
this.onScrollProperty().set((EventHandler<ScrollEvent>) (ScrollEvent t) -> {
this.onScrollProperty().set((ScrollEvent t) -> {
vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() - t.getDeltaY() / 200.0)));
});
@ -212,8 +211,8 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
selectedNodes.addListener((Observable observable) -> {
highlightedNodes.clear();
selectedNodes.stream().forEach((tn) -> {
for (AggregateEventNode n : chart.getNodes((AggregateEventNode t)
-> t.getEvent().getDescription().equals(tn.getEvent().getDescription()))) {
for (DetailViewNode<?> n : chart.getNodes((DetailViewNode<?> t) ->
t.getDescription().equals(tn.getDescription()))) {
highlightedNodes.add(n);
}
});
@ -236,8 +235,8 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
treeSelectionModel.getSelectedItems().addListener((Observable observable) -> {
highlightedNodes.clear();
for (TreeItem<NavTreeNode> tn : treeSelectionModel.getSelectedItems()) {
for (AggregateEventNode n : chart.getNodes((AggregateEventNode t)
-> t.getEvent().getDescription().equals(tn.getValue().getDescription()))) {
for (DetailViewNode<?> n : chart.getNodes((DetailViewNode<?> t) ->
t.getDescription().equals(tn.getValue().getDescription()))) {
highlightedNodes.add(n);
}
}
@ -250,7 +249,7 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
}
@Override
protected Axis<AggregateEvent> getYAxis() {
protected Axis<EventCluster> getYAxis() {
return verticalAxis;
}
@ -279,15 +278,13 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
* EventType
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private XYChart.Series<DateTime, AggregateEvent> getSeries(final EventType et) {
XYChart.Series<DateTime, AggregateEvent> series = eventTypeToSeriesMap.get(et);
if (series == null) {
series = new XYChart.Series<>();
private XYChart.Series<DateTime, EventCluster> getSeries(final EventType et) {
return eventTypeToSeriesMap.computeIfAbsent(et, (EventType t) -> {
XYChart.Series<DateTime, EventCluster> series = new XYChart.Series<>();
series.setName(et.getDisplayName());
eventTypeToSeriesMap.put(et, series);
dataSets.add(series);
}
return series;
return series;
});
}
@Override
@ -328,17 +325,17 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
});
final int size = aggregatedEvents.size();
int i = 0;
for (final AggregateEvent e : aggregatedEvents) {
for (final EventCluster e : aggregatedEvents) {
if (isCancelled()) {
break;
}
updateProgress(i++, size);
updateMessage(NbBundle.getMessage(this.getClass(), "DetailViewPane.loggedTask.updateUI"));
final XYChart.Data<DateTime, AggregateEvent> xyData = new BarChart.Data<>(new DateTime(e.getSpan().getStartMillis()), e);
final XYChart.Data<DateTime, EventCluster> xyData = new BarChart.Data<>(new DateTime(e.getSpan().getStartMillis()), e);
Platform.runLater(() -> {
if (isCancelled() == false) {
getSeries(e.getType()).getData().add(xyData);
getSeries(e.getEventType()).getData().add(xyData);
}
});
}
@ -359,8 +356,8 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
}
@Override
protected void applySelectionEffect(AggregateEventNode c1, Boolean applied) {
c1.applySelectionEffect(applied);
protected void applySelectionEffect(DetailViewNode<?> c1, Boolean selected) {
chart.applySelectionEffect(c1, selected);
}
private class DetailViewSettingsPane extends HBox {
@ -435,9 +432,10 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
assert oneEventPerRowBox != null : "fx:id=\"oneEventPerRowBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS
assert truncateAllBox != null : "fx:id=\"truncateAllBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS
assert truncateWidthSlider != null : "fx:id=\"truncateAllSlider\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS
bandByTypeBox.selectedProperty().bindBidirectional(chart.getBandByType());
truncateAllBox.selectedProperty().bindBidirectional(chart.getTruncateAll());
oneEventPerRowBox.selectedProperty().bindBidirectional(chart.getOneEventPerRow());
bandByTypeBox.selectedProperty().bindBidirectional(chart.bandByTypeProperty());
truncateAllBox.selectedProperty().bindBidirectional(chart.truncateAllProperty());
oneEventPerRowBox.selectedProperty().bindBidirectional(chart.oneEventPerRowProperty());
truncateSliderLabel.disableProperty().bind(truncateAllBox.selectedProperty().not());
truncateSliderLabel.setText(NbBundle.getMessage(this.getClass(), "DetailViewPane.truncateSliderLabel.text"));
final InvalidationListener sliderListener = o -> {

View File

@ -22,21 +22,21 @@ import java.util.Collections;
import java.util.List;
import javafx.scene.chart.Axis;
import javafx.scene.chart.XYChart;
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
/**
* No-Op axis that doesn't do anything usefull but is necessary to pass
* AggregateEvent as the second member of {@link XYChart.Data} objects
*/
class EventAxis extends Axis<AggregateEvent> {
class EventAxis extends Axis<EventCluster> {
@Override
public double getDisplayPosition(AggregateEvent value) {
public double getDisplayPosition(EventCluster value) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public AggregateEvent getValueForDisplay(double displayPosition) {
public EventCluster getValueForDisplay(double displayPosition) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@ -46,17 +46,17 @@ class EventAxis extends Axis<AggregateEvent> {
}
@Override
public boolean isValueOnAxis(AggregateEvent value) {
public boolean isValueOnAxis(EventCluster value) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public double toNumericValue(AggregateEvent value) {
public double toNumericValue(EventCluster value) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public AggregateEvent toRealValue(double value) {
public EventCluster toRealValue(double value) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@ -66,7 +66,7 @@ class EventAxis extends Axis<AggregateEvent> {
}
@Override
protected List<AggregateEvent> calculateTickValues(double length, Object range) {
protected List<EventCluster> calculateTickValues(double length, Object range) {
return Collections.emptyList();
}
@ -76,7 +76,7 @@ class EventAxis extends Axis<AggregateEvent> {
}
@Override
protected String getTickMarkLabel(AggregateEvent value) {
protected String getTickMarkLabel(EventCluster value) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}

View File

@ -0,0 +1,181 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-15 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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Pos;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Represents an {@link EventCluster} in a {@link EventDetailChart}.
*/
public class EventClusterNode extends AbstractDetailViewNode<EventCluster, EventClusterNode> {
private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName());
/**
* the region that represents the time span of this node's event
*/
private final Region spanRegion = new Region();
/**
* the context menu that with the slider that controls subnode/event display
*
* //TODO: move more of the control of subnodes/events here and out of
* EventDetail Chart
*/
private final SimpleObjectProperty<ContextMenu> contextMenu = new SimpleObjectProperty<>();
private Tooltip tooltip;
public EventClusterNode(final EventCluster eventCluster, EventClusterNode parentEventNode, EventDetailChart chart) {
super(chart, eventCluster, parentEventNode);
minWidthProperty().bind(spanRegion.widthProperty());
header.setPrefWidth(USE_COMPUTED_SIZE);
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);
//setup backgrounds
spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS
spanRegion.setBackground(getBackground());
}
@Override
synchronized void installTooltip() {
//TODO: all this work should probably go on a background thread...
if (tooltip == null) {
HashMap<String, Long> hashSetCounts = new HashMap<>();
if (!getEventCluster().getEventIDsWithHashHits().isEmpty()) {
hashSetCounts = new HashMap<>();
try {
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);
}
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex);
}
}
Map<String, Long> tagCounts = new HashMap<>();
if (!getEventCluster().getEventIDsWithTags().isEmpty()) {
tagCounts.putAll(getEventsModel().getTagCountsByTagName(getEventCluster().getEventIDsWithTags()));
}
String hashSetCountsString = hashSetCounts.entrySet().stream()
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
.collect(Collectors.joining("\n"));
String tagCountsString = tagCounts.entrySet().stream()
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
.collect(Collectors.joining("\n"));
tooltip = new Tooltip(
NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text",
getEventCluster().getEventIDs().size(), getEventCluster().getEventType(), getEventCluster().getDescription(),
getEventCluster().getSpan().getStart().toString(TimeLineController.getZonedFormatter()),
getEventCluster().getSpan().getEnd().toString(TimeLineController.getZonedFormatter()))
+ (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString)
+ (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString)
);
Tooltip.install(EventClusterNode.this, tooltip);
}
}
synchronized public EventCluster getEventCluster() {
return getEventBundle();
}
/**
* sets the width of the {@link Region} with border and background used to
* indicate the temporal span of this aggregate event
*
* @param w
*/
private void setSpanWidth(double w) {
spanRegion.setPrefWidth(w);
spanRegion.setMaxWidth(w);
spanRegion.setMinWidth(Math.max(2, w));
}
@Override
public void setSpanWidths(List<Double> spanWidths) {
setSpanWidth(spanWidths.get(0));
}
@Override
Region getSpanFillNode() {
return spanRegion;
}
/**
* @return the contextMenu
*/
public ContextMenu getContextMenu() {
return contextMenu.get();
}
/**
* @param contextMenu the contextMenu to set
*/
public void setContextMenu(ContextMenu contextMenu) {
this.contextMenu.set(contextMenu);
}
@Override
void showSpans(boolean showSpans) {
//no-op for now
}
@Override
Collection<EventCluster> makeBundlesFromClusters(List<EventCluster> eventClusters) {
return eventClusters;
}
@Override
EventClusterNode getNodeForBundle(EventCluster cluster) {
return new EventClusterNode(cluster, this, getChart());
}
}

View File

@ -18,22 +18,27 @@
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import com.google.common.collect.Collections2;
import com.google.common.collect.Range;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.SimpleBooleanProperty;
@ -41,9 +46,7 @@ import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
@ -62,6 +65,7 @@ import javafx.scene.shape.Line;
import javafx.scene.shape.StrokeLineCap;
import javafx.util.Duration;
import javax.annotation.concurrent.GuardedBy;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionGroup;
import org.controlsfx.control.action.ActionUtils;
@ -71,7 +75,8 @@ import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.actions.Back;
import org.sleuthkit.autopsy.timeline.actions.Forward;
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
@ -90,7 +95,7 @@ import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
*
* //TODO: refactor the projected lines to a separate class. -jm
*/
public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> implements TimeLineChart<DateTime> {
public final class EventDetailChart extends XYChart<DateTime, EventCluster> implements TimeLineChart<DateTime> {
private static final int PROJECTED_LINE_Y_OFFSET = 5;
@ -102,7 +107,6 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
*/
private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false);
// I don't like having these package visible, but it was the easiest way to
private ContextMenu chartContextMenu;
private TimeLineController controller;
@ -151,17 +155,9 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
/**
* map from event to node
*/
private final Map<AggregateEvent, AggregateEventNode> nodeMap = new TreeMap<>((
AggregateEvent o1,
AggregateEvent o2) -> {
int comp = Long.compare(o1.getSpan().getStartMillis(), o2.getSpan().getStartMillis());
if (comp != 0) {
return comp;
} else {
return Comparator.comparing(AggregateEvent::hashCode).compare(o1, o2);
}
});
private final Map<EventCluster, EventClusterNode> clusterNodeMap = new HashMap<>();
private final Map<ImmutablePair<EventType, String>, EventStripe> stripeDescMap = new HashMap<>();
private final Map<EventStripe, EventStripeNode> stripeNodeMap = new HashMap<>();
/**
* true == enforce that no two events can share the same 'row', leading to
* sparser but possibly clearer layout. false == put unrelated events in the
@ -169,7 +165,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
*/
private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false);
private final ObservableMap<AggregateEventNode, Line> projectionMap = FXCollections.observableHashMap();
private final Map<Range<Long>, Line> projectionMap = new HashMap<>();
/**
* flag indicating whether this chart actually needs a layout pass
@ -177,16 +173,16 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
@GuardedBy(value = "this")
private boolean requiresLayout = true;
final ObservableList<AggregateEventNode> selectedNodes;
final ObservableList<DetailViewNode<?>> selectedNodes;
/**
* list of series of data added to this chart TODO: replace this with a map
* from name to series? -jm
*/
private final ObservableList<Series<DateTime, AggregateEvent>> seriesList
= FXCollections.<Series<DateTime, AggregateEvent>>observableArrayList();
private final ObservableList<Series<DateTime, EventCluster>> seriesList
= FXCollections.<Series<DateTime, EventCluster>>observableArrayList();
private final ObservableList<Series<DateTime, AggregateEvent>> sortedSeriesList = seriesList
private final ObservableList<Series<DateTime, EventCluster>> sortedSeriesList = seriesList
.sorted((s1, s2) -> {
final List<String> collect = EventType.allTypes.stream().map(EventType::getDisplayName).collect(Collectors.toList());
return Integer.compare(collect.indexOf(s1.getName()), collect.indexOf(s2.getName()));
@ -205,12 +201,13 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
* via slider if truncateAll is true
*/
private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true);
EventDetailChart(DateAxis dateAxis, final Axis<AggregateEvent> verticalAxis, ObservableList<AggregateEventNode> selectedNodes) {
EventDetailChart(DateAxis dateAxis, final Axis<EventCluster> verticalAxis, ObservableList<DetailViewNode<?>> selectedNodes) {
super(dateAxis, verticalAxis);
dateAxis.setAutoRanging(false);
//yAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm
//verticalAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm
verticalAxis.setTickLabelsVisible(false);
verticalAxis.setTickMarkVisible(false);
@ -224,7 +221,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
//bind listener to events that should trigger layout
widthProperty().addListener(layoutInvalidationListener);
heightProperty().addListener(layoutInvalidationListener);
// boundsInLocalProperty().addListener(layoutInvalidationListener);
bandByType.addListener(layoutInvalidationListener);
oneEventPerRow.addListener(layoutInvalidationListener);
truncateAll.addListener(layoutInvalidationListener);
@ -242,36 +239,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
chartContextMenu.hide();
}
if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) {
chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new Action(
NbBundle.getMessage(this.getClass(), "EventDetailChart.chartContextMenu.placeMarker.name")) {
{
setGraphic(new ImageView(new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true))); // NON-NLS
setEventHandler((ActionEvent t) -> {
if (guideLine == null) {
guideLine = new GuideLine(0, 0, 0, getHeight(), dateAxis);
guideLine.relocate(clickEvent.getX(), 0);
guideLine.endYProperty().bind(heightProperty().subtract(dateAxis.heightProperty().subtract(dateAxis.tickLengthProperty())));
getChartChildren().add(guideLine);
guideLine.setOnMouseClicked((MouseEvent event) -> {
if (event.getButton() == MouseButton.SECONDARY) {
clearGuideLine();
event.consume();
}
});
} else {
guideLine.relocate(clickEvent.getX(), 0);
}
});
}
}, new ActionGroup(
NbBundle.getMessage(this.getClass(), "EventDetailChart.contextMenu.zoomHistory.name"),
new Back(controller),
new Forward(controller))));
chartContextMenu.setAutoHide(true);
getChartContextMenu(clickEvent);
chartContextMenu.show(EventDetailChart.this, clickEvent.getScreenX(), clickEvent.getScreenY());
clickEvent.consume();
}
@ -285,51 +253,85 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
setOnMouseReleased(dragHandler);
setOnMouseDragged(dragHandler);
projectionMap.addListener((MapChangeListener.Change<? extends AggregateEventNode, ? extends Line> change) -> {
final Line valueRemoved = change.getValueRemoved();
if (valueRemoved != null) {
getChartChildren().removeAll(valueRemoved);
}
final Line valueAdded = change.getValueAdded();
if (valueAdded != null) {
getChartChildren().add(valueAdded);
}
});
this.selectedNodes = selectedNodes;
this.selectedNodes.addListener((
ListChangeListener.Change<? extends AggregateEventNode> c) -> {
ListChangeListener.Change<? extends DetailViewNode<?>> c) -> {
while (c.next()) {
c.getRemoved().forEach((AggregateEventNode t) -> {
projectionMap.remove(t);
});
c.getAddedSubList().forEach((AggregateEventNode t) -> {
Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET,
dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET
);
line.setStroke(t.getEvent().getType().getColor().deriveColor(0, 1, 1, .5));
line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH);
line.setStrokeLineCap(StrokeLineCap.ROUND);
projectionMap.put(t, line);
});
c.getRemoved().forEach((DetailViewNode<?> t) -> {
t.getEventBundle().getRanges().forEach((Range<Long> t1) -> {
Line removedLine = projectionMap.remove(t1);
getChartChildren().removeAll(removedLine);
});
});
c.getAddedSubList().forEach((DetailViewNode<?> t) -> {
for (Range<Long> range : t.getEventBundle().getRanges()) {
Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.lowerEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET,
dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.upperEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET
);
line.setStroke(t.getEventType().getColor().deriveColor(0, 1, 1, .5));
line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH);
line.setStrokeLineCap(StrokeLineCap.ROUND);
projectionMap.put(range, line);
getChartChildren().add(line);
}
});
}
this.controller.selectEventIDs(selectedNodes.stream()
.flatMap((AggregateEventNode aggNode) -> aggNode.getEvent().getEventIDs().stream())
.flatMap(detailNode -> detailNode.getEventIDs().stream())
.collect(Collectors.toList()));
});
requestChartLayout();
}
ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException {
if (chartContextMenu != null) {
chartContextMenu.hide();
}
chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new Action(
NbBundle.getMessage(this.getClass(), "EventDetailChart.chartContextMenu.placeMarker.name")) {
{
setGraphic(new ImageView(new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true))); // NON-NLS
setEventHandler((ActionEvent t) -> {
if (guideLine == null) {
guideLine = new GuideLine(0, 0, 0, getHeight(), getXAxis());
guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0);
guideLine.endYProperty().bind(heightProperty().subtract(getXAxis().heightProperty().subtract(getXAxis().tickLengthProperty())));
getChartChildren().add(guideLine);
guideLine.setOnMouseClicked((MouseEvent event) -> {
if (event.getButton() == MouseButton.SECONDARY) {
clearGuideLine();
event.consume();
}
});
} else {
guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0);
}
});
}
}, new ActionGroup(
NbBundle.getMessage(this.getClass(), "EventDetailChart.contextMenu.zoomHistory.name"),
new Back(controller),
new Forward(controller))));
chartContextMenu.setAutoHide(true);
return chartContextMenu;
}
@Override
public void clearIntervalSelector() {
getChartChildren().remove(intervalSelector);
intervalSelector = null;
}
public synchronized SimpleBooleanProperty getBandByType() {
public synchronized SimpleBooleanProperty bandByTypeProperty() {
return bandByType;
}
@ -389,11 +391,11 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
getChartChildren().add(getIntervalSelector());
}
public synchronized SimpleBooleanProperty getOneEventPerRow() {
public synchronized SimpleBooleanProperty oneEventPerRowProperty() {
return oneEventPerRow;
}
public synchronized SimpleBooleanProperty getTruncateAll() {
public synchronized SimpleBooleanProperty truncateAllProperty() {
return truncateAll;
}
@ -407,30 +409,49 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
}
@Override
protected synchronized void dataItemAdded(Series<DateTime, AggregateEvent> series, int i, Data<DateTime, AggregateEvent> data) {
final AggregateEvent aggEvent = data.getYValue();
AggregateEventNode eventNode = nodeMap.get(aggEvent);
if (eventNode == null) {
eventNode = new AggregateEventNode(aggEvent, null, this);
eventNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis())));
data.setNode(eventNode);
nodeMap.put(aggEvent, eventNode);
nodeGroup.getChildren().add(eventNode);
requiresLayout = true;
protected synchronized void dataItemAdded(Series<DateTime, EventCluster> series, int i, Data<DateTime, EventCluster> data) {
final EventCluster aggEvent = data.getYValue();
if (alternateLayout.get()) {
EventStripe eventCluster = stripeDescMap.merge(ImmutablePair.of(aggEvent.getEventType(), aggEvent.getDescription()),
new EventStripe(aggEvent),
(EventStripe u, EventStripe v) -> {
EventStripeNode remove = stripeNodeMap.remove(u);
nodeGroup.getChildren().remove(remove);
remove = stripeNodeMap.remove(v);
nodeGroup.getChildren().remove(remove);
return EventStripe.merge(u, v);
}
);
EventStripeNode clusterNode = new EventStripeNode(eventCluster, null, EventDetailChart.this);
stripeNodeMap.put(eventCluster, clusterNode);
nodeGroup.getChildren().add(clusterNode);
} else {
clusterNodeMap.computeIfAbsent(aggEvent, (EventCluster t) -> {
EventClusterNode eventNode = new EventClusterNode(aggEvent, null, EventDetailChart.this);
eventNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis())));
clusterNodeMap.put(aggEvent, eventNode);
nodeGroup.getChildren().add(eventNode);
return eventNode;
});
}
}
@Override
protected synchronized void dataItemChanged(Data<DateTime, AggregateEvent> data) {
protected synchronized void dataItemChanged(Data<DateTime, EventCluster> data) {
//TODO: can we use this to help with local detail level adjustment -jm
throw new UnsupportedOperationException("Not supported yet."); // NON-NLS //To change body of generated methods, choose Tools | Templates.
}
@Override
protected synchronized void dataItemRemoved(Data<DateTime, AggregateEvent> data, Series<DateTime, AggregateEvent> series) {
nodeMap.remove(data.getYValue());
nodeGroup.getChildren().remove(data.getNode());
protected synchronized void dataItemRemoved(Data<DateTime, EventCluster> data, Series<DateTime, EventCluster> series) {
EventCluster aggEvent = data.getYValue();
Node removedNode = clusterNodeMap.remove(aggEvent);
nodeGroup.getChildren().remove(removedNode);
EventStripe removedCluster = stripeDescMap.remove(ImmutablePair.of(aggEvent.getEventType(), aggEvent.getDescription()));
removedNode = stripeNodeMap.remove(removedCluster);
nodeGroup.getChildren().remove(removedNode);
data.setNode(null);
}
@ -465,17 +486,36 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
maxY.set(0.0);
if (bandByType.get() == false) {
if (alternateLayout.get() == true) {
List<EventStripeNode> nodes = new ArrayList<>(stripeNodeMap.values());
nodes.sort(Comparator.comparing(DetailViewNode<?>::getStartMillis));
layoutNodes(nodes, minY, 0);
} else {
List<EventClusterNode> nodes = new ArrayList<>(clusterNodeMap.values());
nodes.sort(Comparator.comparing(DetailViewNode<?>::getStartMillis));
layoutNodes(nodes, minY, 0);
}
ObservableList<Node> nodes = FXCollections.observableArrayList(nodeMap.values());
FXCollections.sort(nodes, new StartTimeComparator());
layoutNodes(nodes, minY, 0);
// layoutNodes(new ArrayList<>(nodeMap.values()), minY, 0);
} else {
for (Series<DateTime, AggregateEvent> s : sortedSeriesList) {
ObservableList<Node> nodes = FXCollections.observableArrayList(Collections2.transform(s.getData(), Data::getNode));
FXCollections.sort(nodes, new StartTimeComparator());
layoutNodes(nodes.filtered((Node n) -> n != null), minY, 0);
for (Series<DateTime, EventCluster> s : sortedSeriesList) {
if (alternateLayout.get() == true) {
List<EventStripeNode> nodes = s.getData().stream()
.map(Data::getYValue)
.map(cluster -> stripeDescMap.get(ImmutablePair.of(cluster.getEventType(), cluster.getDescription())))
.distinct()
.sorted(Comparator.comparing(EventStripe::getStartMillis))
.map(stripeNodeMap::get)
.collect(Collectors.toList());
layoutNodes(nodes, minY, 0);
} else {
List<EventClusterNode> nodes = s.getData().stream()
.map(Data::getYValue)
.map(clusterNodeMap::get)
.filter(Objects::nonNull)
.sorted(Comparator.comparing(EventClusterNode::getStartMillis))
.collect(Collectors.toList());
layoutNodes(nodes, minY, 0);
}
minY = maxY.get();
}
}
@ -486,7 +526,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
}
@Override
protected synchronized void seriesAdded(Series<DateTime, AggregateEvent> series, int i) {
protected synchronized void seriesAdded(Series<DateTime, EventCluster> series, int i) {
for (int j = 0; j < series.getData().size(); j++) {
dataItemAdded(series, j, series.getData().get(j));
}
@ -495,7 +535,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
}
@Override
protected synchronized void seriesRemoved(Series<DateTime, AggregateEvent> series) {
protected synchronized void seriesRemoved(Series<DateTime, EventCluster> series) {
for (int j = 0; j < series.getData().size(); j++) {
dataItemRemoved(series.getData().get(j), series);
}
@ -503,7 +543,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
requiresLayout = true;
}
synchronized SimpleObjectProperty<DescriptionVisibility> getDescrVisibility() {
synchronized SimpleObjectProperty< DescriptionVisibility> getDescrVisibility() {
return descrVisibility;
}
@ -511,40 +551,39 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
return maxY.getReadOnlyProperty();
}
Iterable<AggregateEventNode> getNodes(Predicate<AggregateEventNode> p) {
List<AggregateEventNode> nodes = new ArrayList<>();
Iterable<DetailViewNode<?>> getNodes(Predicate<DetailViewNode<?>> p) {
Collection<? extends DetailViewNode<?>> values = alternateLayout.get()
? stripeNodeMap.values()
: clusterNodeMap.values();
for (AggregateEventNode node : nodeMap.values()) {
checkNode(node, p, nodes);
}
return nodes;
//collapse tree of DetailViewNoeds to list and then filter on given predicate
return values.stream()
.flatMap(EventDetailChart::flatten)
.filter(p).collect(Collectors.toList());
}
Iterable<AggregateEventNode> getAllNodes() {
public static Stream<? extends DetailViewNode<?>> flatten(DetailViewNode<?> node) {
return Stream.concat(
Stream.of(node),
node.getSubNodes().stream().flatMap(EventDetailChart::flatten));
}
Iterable<DetailViewNode<?>> getAllNodes() {
return getNodes(x -> true);
}
synchronized SimpleDoubleProperty getTruncateWidth() {
synchronized SimpleDoubleProperty
getTruncateWidth() {
return truncateWidth;
}
synchronized void setVScroll(double d) {
synchronized void
setVScroll(double d
) {
final double h = maxY.get() - (getHeight() * .9);
nodeGroup.setTranslateY(-d * h);
}
private static void checkNode(AggregateEventNode node, Predicate<AggregateEventNode> p, List<AggregateEventNode> nodes) {
if (node != null) {
if (p.test(node)) {
nodes.add(node);
}
for (Node n : node.getSubNodePane().getChildrenUnmodifiable()) {
checkNode((AggregateEventNode) n, p, nodes);
}
}
}
private void clearGuideLine() {
getChartChildren().remove(guideLine);
guideLine = null;
@ -557,41 +596,75 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
* @param nodes
* @param minY
*/
private synchronized double layoutNodes(final List<Node> nodes, final double minY, final double xOffset) {
private synchronized double layoutNodes(final Collection<? extends AbstractDetailViewNode<?, ?>> nodes, final double minY, final double xOffset) {
//hash map from y value to right most occupied x value. This tells you for a given 'row' what is the first avaialable slot
Map<Integer, Double> maxXatY = new HashMap<>();
double localMax = minY;
//for each node lay size it and position it in first available slot
for (Node n : nodes) {
final AggregateEventNode tlNode = (AggregateEventNode) n;
tlNode.setDescriptionVisibility(descrVisibility.get());
for (AbstractDetailViewNode<?, ?> node : nodes) {
node.setDescriptionVisibility(descrVisibility.get());
double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(node.getStartMillis()));
AggregateEvent ie = tlNode.getEvent();
final double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(ie.getSpan().getStartMillis()));
//position of start and end according to range of axis
double xPos = rawDisplayPosition - xOffset;
double startX = rawDisplayPosition - xOffset;
double layoutNodesResultHeight = 0;
if (tlNode.getSubNodePane().getChildren().isEmpty() == false) {
FXCollections.sort(tlNode.getSubNodePane().getChildren(), new StartTimeComparator());
layoutNodesResultHeight = layoutNodes(tlNode.getSubNodePane().getChildren(), 0, rawDisplayPosition);
}
double xPos2 = getXAxis().getDisplayPosition(new DateTime(ie.getSpan().getEndMillis())) - xOffset;
double span = xPos2 - xPos;
//size timespan border
tlNode.setSpanWidth(span);
if (truncateAll.get()) { //if truncate option is selected limit width of description label
tlNode.setDescriptionWidth(Math.max(span, truncateWidth.get()));
} else { //else set it unbounded
tlNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth());
double span = 0;
List<? extends AbstractDetailViewNode<?, ?>> subNodes = node.getSubNodes();
if (subNodes.isEmpty() == false) {
subNodes.sort(new DetailViewNode.StartTimeComparator());
layoutNodesResultHeight = layoutNodes(subNodes, 0, rawDisplayPosition);
}
tlNode.autosize(); //compute size of tlNode based on constraints and event data
if (alternateLayout.get() == false) {
double endX = getXAxis().getDisplayPosition(new DateTime(node.getEndMillis())) - xOffset;
span = endX - startX;
//size timespan border
node.setSpanWidths(Arrays.asList(span));
} else {
EventStripeNode stripeNode = (EventStripeNode) node;
List<Double> spanWidths = new ArrayList<>();
double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));;
double x2;
Iterator<Range<Long>> ranges = stripeNode.getStripe().getRanges().iterator();
Range<Long> range = ranges.next();
do {
x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint()));
double clusterSpan = x2 - x;
span += clusterSpan;
spanWidths.add(clusterSpan);
if (ranges.hasNext()) {
range = ranges.next();
x = getXAxis().getDisplayPosition(new DateTime(range.lowerEndpoint()));
double gapSpan = x - x2;
span += gapSpan;
spanWidths.add(gapSpan);
if (ranges.hasNext() == false) {
x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint()));
clusterSpan = x2 - x;
span += clusterSpan;
spanWidths.add(clusterSpan);
}
}
} while (ranges.hasNext());
stripeNode.setSpanWidths(spanWidths);
}
if (truncateAll.get()) { //if truncate option is selected limit width of description label
node.setDescriptionWidth(Math.max(span, truncateWidth.get()));
} else { //else set it unbounded
node.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth());
}
node.autosize(); //compute size of tlNode based on constraints and event data
//get position of right edge of node ( influenced by description label)
double xRight = xPos + tlNode.getWidth();
double xRight = startX + node.getWidth();
//get the height of the node
final double h = layoutNodesResultHeight == 0 ? tlNode.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT;
final double h = layoutNodesResultHeight == 0 ? node.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT;
//initial test position
double yPos = minY;
@ -612,7 +685,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
//check each pixel from bottom to top.
for (double y = yPos2; y >= yPos; y--) {
final Double maxX = maxXatY.get((int) y);
if (maxX != null && maxX >= xPos - 4) {
if (maxX != null && maxX >= startX - 4) {
//if that pixel is already used
//jump top to this y value and repeat until free slot is found.
overlapping = true;
@ -630,24 +703,24 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
localMax = Math.max(yPos2, localMax);
Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0),
new KeyValue(tlNode.layoutXProperty(), xPos),
new KeyValue(tlNode.layoutYProperty(), yPos)));
new KeyValue(node.layoutXProperty(), startX),
new KeyValue(node.layoutYProperty(), yPos)));
tm.play();
// tlNode.relocate(xPos, yPos);
}
maxY.set(Math.max(maxY.get(), localMax));
return localMax - minY;
}
private static final int DEFAULT_ROW_HEIGHT = 24;
private void layoutProjectionMap() {
for (final Map.Entry<AggregateEventNode, Line> entry : projectionMap.entrySet()) {
final AggregateEventNode aggNode = entry.getKey();
for (final Map.Entry<Range<Long>, Line> entry : projectionMap.entrySet()) {
final Range<Long> eventBundle = entry.getKey();
final Line line = entry.getValue();
line.setStartX(getParentXForValue(new DateTime(aggNode.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone())));
line.setEndX(getParentXForValue(new DateTime(aggNode.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone())));
line.setStartX(getParentXForValue(new DateTime(eventBundle.lowerEndpoint(), TimeLineController.getJodaTimeZone())));
line.setEndX(getParentXForValue(new DateTime(eventBundle.upperEndpoint(), TimeLineController.getJodaTimeZone())));
line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
}
@ -671,29 +744,8 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
return filteredEvents;
}
/**
* @return the chartContextMenu
*/
public ContextMenu getChartContextMenu() {
return chartContextMenu;
}
private static class StartTimeComparator implements Comparator<Node> {
@Override
public int compare(Node n1, Node n2) {
if (n1 == null) {
return 1;
} else if (n2 == null) {
return -1;
} else {
return Long.compare(((AggregateEventNode) n1).getEvent().getSpan().getStartMillis(),
(((AggregateEventNode) n2).getEvent().getSpan().getStartMillis()));
}
}
Property<Boolean> alternateLayoutProperty() {
return alternateLayout;
}
private class DetailIntervalSelector extends IntervalSelector<DateTime> {
@ -730,4 +782,8 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
protected void requestChartLayout() {
super.requestChartLayout();
}
void applySelectionEffect(DetailViewNode<?> c1, Boolean selected) {
c1.applySelectionEffect(selected);
}
}

View File

@ -0,0 +1,108 @@
/*
* 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.
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import com.google.common.collect.Range;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.HBox;
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.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import static org.sleuthkit.autopsy.timeline.ui.detailview.AbstractDetailViewNode.show;
/**
*
*/
public class EventStripeNode extends AbstractDetailViewNode<EventStripe, EventStripeNode> {
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, getSubNodePane());
internalVBox.setAlignment(Pos.CENTER_LEFT);
for (Range<Long> range : eventStripe.getRanges()) {
Region rangeRegion = new Region();
rangeRegion.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS
rangeRegion.setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
rangesHBox.getChildren().addAll(rangeRegion, new Region());
}
rangesHBox.getChildren().remove(rangesHBox.getChildren().size() - 1);
rangesHBox.setMaxWidth(USE_PREF_SIZE);
setMaxWidth(USE_PREF_SIZE);
getChildren().addAll(rangesHBox, internalVBox);
}
/**
*
* @param showControls the value of par
*/
@Override
void showDescriptionLoDControls(final boolean showControls) {
super.showDescriptionLoDControls(showControls);
show(getSpacer(), showControls);
}
@Override
public void setSpanWidths(List<Double> spanWidths) {
for (int i = 0; i < spanWidths.size(); i++) {
Region spanRegion = (Region) rangesHBox.getChildren().get(i);
Double w = spanWidths.get(i);
spanRegion.setPrefWidth(w);
spanRegion.setMaxWidth(w);
spanRegion.setMinWidth(Math.max(2, w));
}
}
EventStripe getStripe() {
return getEventBundle();
}
@Override
HBox getSpanFillNode() {
return rangesHBox;
}
@Override
Collection<EventStripe> makeBundlesFromClusters(List<EventCluster> eventClusters) {
return eventClusters.stream().collect(
Collectors.toMap(
EventCluster::getDescription, //key
EventStripe::new, //value
EventStripe::merge)//merge method
).values();
}
/**
*
* @param showSpans the value of showSpans
*/
@Override
void showSpans(final boolean showSpans) {
rangesHBox.setVisible(showSpans);
}
@Override
void installTooltip() {
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
EventStripeNode getNodeForBundle(EventStripe cluster) {
return new EventStripeNode(cluster, this, getChart());
}
}

View File

@ -19,6 +19,7 @@
package org.sleuthkit.autopsy.timeline.ui.detailview;
import javafx.scene.Cursor;
import javafx.scene.chart.Axis;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
@ -32,7 +33,7 @@ import org.sleuthkit.autopsy.timeline.TimeLineController;
*/
class GuideLine extends Line {
private final DateAxis dateAxis;
private final Axis<DateTime> dateAxis;
private double startLayoutX;
@ -40,7 +41,7 @@ class GuideLine extends Line {
private double dragStartX = 0;
GuideLine(double startX, double startY, double endX, double endY, DateAxis axis) {
GuideLine(double startX, double startY, double endX, double endY, Axis<DateTime> axis) {
super(startX, startY, endX, endY);
dateAxis = axis;
setCursor(Cursor.E_RESIZE);

View File

@ -20,15 +20,16 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
import java.util.Comparator;
import javafx.scene.control.TreeItem;
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
/**
*
*/
class EventDescriptionTreeItem extends NavTreeItem {
public EventDescriptionTreeItem(AggregateEvent g) {
setValue(new NavTreeNode(g.getType().getBaseType(), g.getDescription(), g.getEventIDs().size()));
public EventDescriptionTreeItem(EventCluster g) {
setValue(new NavTreeNode(g.getEventType().getBaseType(), g.getDescription(), g.getEventIDs().size()));
}
@Override
@ -37,9 +38,9 @@ class EventDescriptionTreeItem extends NavTreeItem {
}
@Override
public void insert(AggregateEvent g) {
public void insert(EventCluster g) {
NavTreeNode value = getValue();
if ((value.getType().getBaseType().equals(g.getType().getBaseType()) == false) || ((value.getDescription().equals(g.getDescription()) == false))) {
if ((value.getType().getBaseType().equals(g.getEventType().getBaseType()) == false) || ((value.getDescription().equals(g.getDescription()) == false))) {
throw new IllegalArgumentException();
}
@ -52,8 +53,8 @@ class EventDescriptionTreeItem extends NavTreeItem {
}
@Override
public TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t) {
if (getValue().getType().getBaseType() == t.getType().getBaseType() && getValue().getDescription().equals(t.getDescription())) {
public TreeItem<NavTreeNode> findTreeItemForEvent(EventBundle t) {
if (getValue().getType().getBaseType() == t.getEventType().getBaseType() && getValue().getDescription().equals(t.getDescription())) {
return this;
}
return null;

View File

@ -24,7 +24,8 @@ import java.util.concurrent.ConcurrentHashMap;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.scene.control.TreeItem;
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
class EventTypeTreeItem extends NavTreeItem {
@ -35,8 +36,8 @@ class EventTypeTreeItem extends NavTreeItem {
private final Comparator<TreeItem<NavTreeNode>> comparator = TreeComparator.Description;
EventTypeTreeItem(AggregateEvent g) {
setValue(new NavTreeNode(g.getType().getBaseType(), g.getType().getBaseType().getDisplayName(), 0));
EventTypeTreeItem(EventCluster g) {
setValue(new NavTreeNode(g.getEventType().getBaseType(), g.getEventType().getBaseType().getDisplayName(), 0));
}
@Override
@ -52,7 +53,7 @@ class EventTypeTreeItem extends NavTreeItem {
* @param tree True if it is part of a tree (versus a list)
*/
@Override
public void insert(AggregateEvent g) {
public void insert(EventCluster g) {
EventDescriptionTreeItem treeItem = childMap.get(g.getDescription());
if (treeItem == null) {
@ -77,8 +78,8 @@ class EventTypeTreeItem extends NavTreeItem {
}
@Override
public TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t) {
if (t.getType().getBaseType() == getValue().getType().getBaseType()) {
public TreeItem<NavTreeNode> findTreeItemForEvent(EventBundle t) {
if (t.getEventType().getBaseType() == getValue().getType().getBaseType()) {
for (TreeItem<NavTreeNode> child : getChildren()) {
final TreeItem<NavTreeNode> findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t);

View File

@ -41,9 +41,9 @@ import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.timeline.FXMLConstructor;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.TimeLineView;
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.ui.detailview.AggregateEventNode;
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewNode;
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
/**
@ -91,8 +91,8 @@ public class NavPanel extends BorderPane implements TimeLineView {
});
detailViewPane.getSelectedNodes().addListener((Observable observable) -> {
eventsTree.getSelectionModel().clearSelection();
detailViewPane.getSelectedNodes().forEach((AggregateEventNode t) -> {
eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEvent()));
detailViewPane.getSelectedNodes().forEach((DetailViewNode<?> t) -> {
eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEventBundle()));
});
});
@ -100,10 +100,10 @@ public class NavPanel extends BorderPane implements TimeLineView {
private void setRoot() {
RootItem root = new RootItem();
final ObservableList<AggregateEvent> aggregatedEvents = detailViewPane.getAggregatedEvents();
final ObservableList<EventCluster> aggregatedEvents = detailViewPane.getAggregatedEvents();
synchronized (aggregatedEvents) {
for (AggregateEvent agg : aggregatedEvents) {
for (EventCluster agg : aggregatedEvents) {
root.insert(agg);
}
}
@ -131,7 +131,7 @@ public class NavPanel extends BorderPane implements TimeLineView {
sortByBox.getItems().setAll(Arrays.asList(TreeComparator.Description, TreeComparator.Count));
sortByBox.getSelectionModel().select(TreeComparator.Description);
sortByBox.getSelectionModel().selectedItemProperty().addListener((Observable o) -> {
((RootItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem());
((NavTreeItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem());
});
eventsTree.setShowRoot(false);
eventsTree.setCellFactory((TreeView<NavTreeNode> p) -> new EventTreeCell());

View File

@ -20,7 +20,8 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
import java.util.Comparator;
import javafx.scene.control.TreeItem;
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
/**
* A node in the nav tree. Manages inserts and resorts. Has parents and
@ -30,12 +31,12 @@ import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
*/
abstract class NavTreeItem extends TreeItem<NavTreeNode> {
abstract void insert(AggregateEvent g);
abstract void insert(EventCluster g);
abstract int getCount();
abstract void resort(Comparator<TreeItem<NavTreeNode>> comp);
abstract TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t);
abstract TreeItem<NavTreeNode> findTreeItemForEvent(EventBundle t);
}

View File

@ -24,8 +24,9 @@ import java.util.Map;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.scene.control.TreeItem;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
/**
*
@ -56,13 +57,13 @@ class RootItem extends NavTreeItem {
* @param g Group to add
*/
@Override
public void insert(AggregateEvent g) {
public void insert(EventCluster g) {
EventTypeTreeItem treeItem = childMap.get(g.getType().getBaseType());
EventTypeTreeItem treeItem = childMap.get(g.getEventType().getBaseType());
if (treeItem == null) {
final EventTypeTreeItem newTreeItem = new EventTypeTreeItem(g);
newTreeItem.setExpanded(true);
childMap.put(g.getType().getBaseType(), newTreeItem);
childMap.put(g.getEventType().getBaseType(), newTreeItem);
newTreeItem.insert(g);
Platform.runLater(() -> {
@ -85,7 +86,7 @@ class RootItem extends NavTreeItem {
}
@Override
public TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t) {
public TreeItem<NavTreeNode> findTreeItemForEvent(EventBundle t) {
for (TreeItem<NavTreeNode> child : getChildren()) {
final TreeItem<NavTreeNode> findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t);
if (findTreeItemForEvent != null) {

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;
}
}

View File

@ -1,5 +1,3 @@
<ivy-module version="2.0">
<info organisation="org.sleuthkit.autopsy" module="corelibs"/>
<configurations >
@ -18,6 +16,7 @@
<dependency conf="autopsy_core->*" org="org.jbundle.thin.base.screen" name="jcalendarbutton" rev="1.4.6"/>
<!-- commmon -->
<dependency org="com.google.guava" name="guava" rev="18.0"/>
<dependency conf="autopsy_core->*" org="org.apache.commons" name="commons-lang3" rev="3.0"/>
<!-- keep old commons-lang because some deps may need it at runtime.
Note there is no namespace collision with ver 3 -->

View File

@ -20,7 +20,7 @@ file.reference.dom4j-1.6.1.jar=release/modules/ext/dom4j-1.6.1.jar
file.reference.geronimo-jms_1.1_spec-1.0.jar=release/modules/ext/geronimo-jms_1.1_spec-1.0.jar
file.reference.gson-1.4.jar=release/modules/ext/gson-1.4.jar
file.reference.gstreamer-java-1.5.jar=release/modules/ext/gstreamer-java-1.5.jar
file.reference.guava-11.0.2.jar=release/modules/ext/guava-11.0.2.jar
file.reference.guava-18.0.jar=release/modules/ext/guava-18.0.jar
file.reference.imageio-bmp-3.1.1.jar=release/modules/ext/imageio-bmp-3.1.1.jar
file.reference.imageio-core-3.1.1.jar=release/modules/ext/imageio-core-3.1.1.jar
file.reference.imageio-icns-3.1.1.jar=release/modules/ext/imageio-icns-3.1.1.jar
@ -71,13 +71,13 @@ file.reference.xmlbeans-2.3.0.jar=release/modules/ext/xmlbeans-2.3.0.jar
javac.source=1.7
javac.compilerargs=-Xlint -Xlint:-serial
javadoc.reference.controlsfx-8.40.9.jar=release/modules/ext/controlsfx-8.40.9-javadoc.jar
javadoc.reference.guava-11.0.2.jar=release/modules/ext/guava-11.0.2-javadoc.jar
javadoc.reference.guava-18.0.jar=release/modules/ext/guava-18.0-javadoc.jar
javadoc.reference.jfxtras-common-8.0-r4.jar=release/modules/ext/jfxtras-common-8.0-r4-javadoc.jar
javadoc.reference.jfxtras-controls-8.0-r4.jar=release/modules/ext/jfxtras-controls-8.0-r4-javadoc.jar
javadoc.reference.jfxtras-fxml-8.0-r4.jar=release/modules/ext/jfxtras-fxml-8.0-r4-javadoc.jar
nbm.needs.restart=true
source.reference.controlsfx-8.40.9.jar=release/modules/ext/controlsfx-8.40.9-sources.jar
source.reference.guava-11.0.2.jar=release/modules/ext/guava-11.0.2-sources.jar
source.reference.guava-18.0.jar=release/modules/ext/guava-18.0-sources.jar
source.reference.jfxtras-common-8.0-r4.jar=release/modules/ext/jfxtras-common-8.0-r4-sources.jar
source.reference.jfxtras-controls-8.0-r4.jar=release/modules/ext/jfxtras-controls-8.0-r4-sources.jar
source.reference.jfxtras-fxml-8.0-r4.jar=release/modules/ext/jfxtras-fxml-8.0-r4-sources.jar

View File

@ -34,13 +34,17 @@
<package>com.google.common.base.internal</package>
<package>com.google.common.cache</package>
<package>com.google.common.collect</package>
<package>com.google.common.escape</package>
<package>com.google.common.eventbus</package>
<package>com.google.common.hash</package>
<package>com.google.common.html</package>
<package>com.google.common.io</package>
<package>com.google.common.math</package>
<package>com.google.common.net</package>
<package>com.google.common.primitives</package>
<package>com.google.common.reflect</package>
<package>com.google.common.util.concurrent</package>
<package>com.google.common.xml</package>
<package>com.google.gson</package>
<package>com.google.gson.annotations</package>
<package>com.google.gson.reflect</package>
@ -708,10 +712,6 @@
<runtime-relative-path>ext/commons-lang3-3.0-sources.jar</runtime-relative-path>
<binary-origin>release/modules/ext/commons-lang3-3.0-sources.jar</binary-origin>
</class-path-extension>
<class-path-extension>
<runtime-relative-path>ext/guava-11.0.2.jar</runtime-relative-path>
<binary-origin>release/modules/ext/guava-11.0.2.jar</binary-origin>
</class-path-extension>
<class-path-extension>
<runtime-relative-path>ext/mail-1.4.3.jar</runtime-relative-path>
<binary-origin>release/modules/ext/mail-1.4.3.jar</binary-origin>
@ -728,6 +728,10 @@
<runtime-relative-path>ext/common-lang-3.1.1.jar</runtime-relative-path>
<binary-origin>release/modules/ext/common-lang-3.1.1.jar</binary-origin>
</class-path-extension>
<class-path-extension>
<runtime-relative-path>ext/guava-18.0.jar</runtime-relative-path>
<binary-origin>release/modules/ext/guava-18.0.jar</binary-origin>
</class-path-extension>
<class-path-extension>
<runtime-relative-path>ext/slf4j-api-1.6.1.jar</runtime-relative-path>
<binary-origin>release/modules/ext/slf4j-api-1.6.1.jar</binary-origin>