show tag icons on detail view event groups, and tag counts in tooltip;

cleanup;  use NbBundle.messages to generate bundle strings
This commit is contained in:
jmillman 2015-07-31 15:37:48 -04:00
parent 6259869baf
commit 703c780f27
6 changed files with 270 additions and 155 deletions

View File

@ -27,37 +27,51 @@ import org.sleuthkit.autopsy.timeline.events.type.EventType;
import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; import org.sleuthkit.autopsy.timeline.utils.IntervalUtils;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
/** An event that represent a set of other events aggregated together. All the /** Represents a set of other (TimeLineEvent) events aggregated together. All
* sub events should have the same type and matching descriptions at the * the sub events should have the same type and matching descriptions at the
* designated 'zoom level'. * designated 'zoom level'.
*/ */
@Immutable @Immutable
public class AggregateEvent { public class AggregateEvent {
/** the smallest time interval containing all the aggregated events */
final private Interval span; final private Interval span;
/** the type of all the aggregted events */
final private EventType type; final private EventType type;
final private Set<Long> eventIDs; /** the common description of all the aggregated events */
final private String description; final private String description;
/** the description level of detail that the events were aggregated at. */
private final DescriptionLOD lod; private final DescriptionLOD lod;
/** the set of ids of the aggregated events */
final private Set<Long> eventIDs;
/**
* the ids of the subset of aggregated events that have at least one tag
* applied to them
*/
private final Set<Long> tagged;
/**
* the ids of the subset of aggregated events that have at least one hash
* set hit
*/
private final Set<Long> hashHits; private final Set<Long> hashHits;
public AggregateEvent(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, String description, DescriptionLOD lod) { public AggregateEvent(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLOD lod) {
this.span = spanningInterval; this.span = spanningInterval;
this.type = type; this.type = type;
this.hashHits = hashHits; this.hashHits = hashHits;
this.tagged = tagged;
this.description = description; this.description = description;
this.eventIDs = eventIDs; this.eventIDs = eventIDs;
this.lod = lod; this.lod = lod;
} }
/** @return the actual interval from the first event to the last event */
public Interval getSpan() { public Interval getSpan() {
return span; return span;
} }
@ -70,6 +84,10 @@ public class AggregateEvent {
return Collections.unmodifiableSet(hashHits); return Collections.unmodifiableSet(hashHits);
} }
public Set<Long> getEventIDsWithTags() {
return Collections.unmodifiableSet(tagged);
}
public String getDescription() { public String getDescription() {
return description; return description;
} }
@ -78,30 +96,33 @@ public class AggregateEvent {
return type; return type;
} }
/**
* merge two aggregate events into one new aggregate event.
*
* @param ag1
* @param ag2
*
* @return
*/
public static AggregateEvent merge(AggregateEvent ag1, AggregateEvent ag2) {
if (ag1.getType() != ag2.getType()) {
throw new IllegalArgumentException("aggregate events are not compatible they have different types");
}
if (!ag1.getDescription().equals(ag2.getDescription())) {
throw new IllegalArgumentException("aggregate events are not compatible they have different descriptions");
}
Sets.SetView<Long> idsUnion = Sets.union(ag1.getEventIDs(), ag2.getEventIDs());
Sets.SetView<Long> hashHitsUnion = Sets.union(ag1.getEventIDsWithHashHits(), ag2.getEventIDsWithHashHits());
return new AggregateEvent(IntervalUtils.span(ag1.span, ag2.span), ag1.getType(), idsUnion, hashHitsUnion, ag1.getDescription(), ag1.lod);
}
public DescriptionLOD getLOD() { public DescriptionLOD getLOD() {
return lod; return lod;
} }
/**
* merge two aggregate events into one new aggregate event.
*
* @param aggEvent1
* @param aggEVent2
*
* @return a new aggregate event that is the result of merging the given
* events
*/
public static AggregateEvent merge(AggregateEvent aggEvent1, AggregateEvent ag2) {
if (aggEvent1.getType() != ag2.getType()) {
throw new IllegalArgumentException("aggregate events are not compatible they have different types");
}
if (!aggEvent1.getDescription().equals(ag2.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());
return new AggregateEvent(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getType(), idsUnion, hashHitsUnion, taggedUnion, aggEvent1.getDescription(), aggEvent1.lod);
}
} }

View File

@ -18,6 +18,7 @@
*/ */
package org.sleuthkit.autopsy.timeline.events; package org.sleuthkit.autopsy.timeline.events;
import javax.annotation.Nullable;
import org.sleuthkit.autopsy.timeline.events.type.EventType; import org.sleuthkit.autopsy.timeline.events.type.EventType;
import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.TskData;
@ -42,7 +43,7 @@ public class TimeLineEvent {
private final boolean hashHit; private final boolean hashHit;
public TimeLineEvent(Long eventID, Long objID, Long artifactID, Long time, EventType type, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known, boolean hashHit) { public TimeLineEvent(Long eventID, Long objID, @Nullable Long artifactID, Long time, EventType type, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known, boolean hashHit) {
this.eventID = eventID; this.eventID = eventID;
this.fileID = objID; this.fileID = objID;
this.artifactID = artifactID; this.artifactID = artifactID;
@ -60,6 +61,7 @@ public class TimeLineEvent {
return hashHit; return hashHit;
} }
@Nullable
public Long getArtifactID() { public Long getArtifactID() {
return artifactID; return artifactID;
} }

View File

@ -1,6 +0,0 @@
EventsRepository.progressWindow.msg.reinit_db=(re)initializing events database
EventsRepository.progressWindow.msg.populateMacEventsFiles=populating mac events for files\:
EventsRepository.progressWindow.msg.populateMacEventsFiles2=populating mac events for files\:
EventsRepository.progressWindow.msg.commitingDb=committing events db
EventsRepository.msgdlg.problem.text=There was a problem populating the timeline. Not all events may be present or accurate. See the log for details.
EventsRepository.progressWindow.populatingXevents=populating {0} events

View File

@ -88,9 +88,10 @@ import org.sqlite.SQLiteJDBCLoader;
*/ */
public class EventDB { public class EventDB {
private PreparedStatement insertHashSetStmt; private PreparedStatement dropEventsTableStmt;
private PreparedStatement insertHashHitStmt; private PreparedStatement dropHashSetHitsTableStmt;
private PreparedStatement selectHashSetStmt; private PreparedStatement dropHashSetsTableStmt;
private PreparedStatement dropDBInfoTableStmt;
/** enum to represent columns in the events table */ /** enum to represent columns in the events table */
enum EventTableColumn { enum EventTableColumn {
@ -105,8 +106,9 @@ public class EventDB {
FULL_DESCRIPTION("full_description"), // NON-NLS FULL_DESCRIPTION("full_description"), // NON-NLS
MED_DESCRIPTION("med_description"), // NON-NLS MED_DESCRIPTION("med_description"), // NON-NLS
SHORT_DESCRIPTION("short_description"), // NON-NLS SHORT_DESCRIPTION("short_description"), // NON-NLS
TIME("time"), TIME("time"), // NON-NLS
HASH_HIT("hash_hit"); // NON-NLS HASH_HIT("hash_hit"), // NON-NLS
TAGGED("tagged"); // NON-NLS
private final String columnName; private final String columnName;
@ -183,12 +185,14 @@ public class EventDB {
private PreparedStatement getDataSourceIDsStmt; private PreparedStatement getDataSourceIDsStmt;
private PreparedStatement insertRowStmt; private PreparedStatement insertRowStmt;
private PreparedStatement recordDBInfoStmt; private PreparedStatement recordDBInfoStmt;
private PreparedStatement insertHashSetStmt;
private PreparedStatement insertHashHitStmt;
private PreparedStatement selectHashSetStmt;
private PreparedStatement countAllEventsStmt;
private final Set<PreparedStatement> preparedStatements = new HashSet<>(); private final Set<PreparedStatement> preparedStatements = new HashSet<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); //use fairness policy private final Lock DBLock = new ReentrantReadWriteLock(true).writeLock(); //using exclusive lock for all db ops for now
private final Lock DBLock = rwLock.writeLock(); //using exclusive lock for all db ops for now
private EventDB(Case autoCase) throws SQLException, Exception { private EventDB(Case autoCase) throws SQLException, Exception {
//should this go into module output (or even cache, we should be able to rebuild it)? //should this go into module output (or even cache, we should be able to rebuild it)?
@ -205,30 +209,6 @@ public class EventDB {
} }
} }
public Interval getSpanningInterval(Collection<Long> eventIDs) {
Interval span = null;
DBLock.lock();
try (Statement stmt = con.createStatement();
//You can't inject multiple values into one ? paramater in prepared statement,
//so we make new statement each time...
ResultSet rs = stmt.executeQuery("select Min(time), Max(time) from events where event_id in (" + StringUtils.join(eventIDs, ", ") + ")");) { // NON-NLS
while (rs.next()) {
span = new Interval(rs.getLong("Min(time)"), rs.getLong("Max(time)") + 1, DateTimeZone.UTC); // NON-NLS
}
} catch (SQLException ex) {
LOGGER.log(Level.SEVERE, "Error executing get spanning interval query.", ex); // NON-NLS
} finally {
DBLock.unlock();
}
return span;
}
EventTransaction beginTransaction() {
return new EventTransaction();
}
void closeDBCon() { void closeDBCon() {
if (con != null) { if (con != null) {
try { try {
@ -241,6 +221,27 @@ public class EventDB {
con = null; con = null;
} }
public Interval getSpanningInterval(Collection<Long> eventIDs) {
DBLock.lock();
try (Statement stmt = con.createStatement();
//You can't inject multiple values into one ? paramater in prepared statement,
//so we make new statement each time...
ResultSet rs = stmt.executeQuery("select Min(time), Max(time) from events where event_id in (" + StringUtils.join(eventIDs, ", ") + ")");) { // NON-NLS
while (rs.next()) {
return new Interval(rs.getLong("Min(time)"), rs.getLong("Max(time)") + 1, DateTimeZone.UTC); // NON-NLS
}
} catch (SQLException ex) {
LOGGER.log(Level.SEVERE, "Error executing get spanning interval query.", ex); // NON-NLS
} finally {
DBLock.unlock();
}
return null;
}
EventTransaction beginTransaction() {
return new EventTransaction();
}
void commitTransaction(EventTransaction tr, Boolean notify) { void commitTransaction(EventTransaction tr, Boolean notify) {
if (tr.isClosed()) { if (tr.isClosed()) {
throw new IllegalArgumentException("can't close already closed transaction"); // NON-NLS throw new IllegalArgumentException("can't close already closed transaction"); // NON-NLS
@ -248,24 +249,34 @@ public class EventDB {
tr.commit(notify); tr.commit(notify);
} }
/**
* @return the total number of events in the database or,
* -1 if there is an error.
*/
int countAllEvents() { int countAllEvents() {
int result = -1;
DBLock.lock(); DBLock.lock();
//TODO convert this to prepared statement -jm try (ResultSet rs = countAllEventsStmt.executeQuery()) { // NON-NLS
try (ResultSet rs = con.createStatement().executeQuery("select count(*) as count from events")) { // NON-NLS
while (rs.next()) { while (rs.next()) {
result = rs.getInt("count"); // NON-NLS return rs.getInt("count"); // NON-NLS
break;
} }
} catch (SQLException ex) { } catch (SQLException ex) {
Exceptions.printStackTrace(ex); LOGGER.log(Level.SEVERE, "Error counting all events", ex);
} finally { } finally {
DBLock.unlock(); DBLock.unlock();
} }
return result; return -1;
} }
Map<EventType, Long> countEvents(ZoomParams params) { /**
* get the count of all events that fit the given zoom params organized by
* the EvenType of the level spcified in the ZoomParams
*
* @param params the params that control what events to count and how to
* organize the returned map
*
* @return a map from event type( of the requested level) to event counts
*/
Map<EventType, Long> countEventsByType(ZoomParams params) {
if (params.getTimeRange() != null) { if (params.getTimeRange() != null) {
return countEvents(params.getTimeRange().getStartMillis() / 1000, return countEvents(params.getTimeRange().getStartMillis() / 1000,
params.getTimeRange().getEndMillis() / 1000, params.getTimeRange().getEndMillis() / 1000,
@ -275,22 +286,25 @@ public class EventDB {
} }
} }
void dropEventsTable() { /**
//TODO: use prepared statement - jm * drop the tables from this database and recreate them in order to start
* over.
*/
void reInitializeDB() {
DBLock.lock(); DBLock.lock();
try (Statement createStatement = con.createStatement()) { try {
createStatement.execute("drop table if exists events"); // NON-NLS dropEventsTableStmt.executeUpdate();
dropHashSetHitsTableStmt.executeUpdate();
dropHashSetsTableStmt.executeUpdate();
dropDBInfoTableStmt.executeUpdate();
initializeDB();;
} catch (SQLException ex) { } catch (SQLException ex) {
LOGGER.log(Level.SEVERE, "could not drop old events table", ex); // NON-NLS LOGGER.log(Level.SEVERE, "could not drop old tables table", ex); // NON-NLS
} finally { } finally {
DBLock.unlock(); DBLock.unlock();
} }
} }
List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
return getAggregatedEvents(params.getTimeRange(), params.getFilter(), params.getTypeZoomLevel(), params.getDescrLOD());
}
Interval getBoundingEventsInterval(Interval timeRange, RootFilter filter) { Interval getBoundingEventsInterval(Interval timeRange, RootFilter filter) {
long start = timeRange.getStartMillis() / 1000; long start = timeRange.getStartMillis() / 1000;
long end = timeRange.getEndMillis() / 1000; long end = timeRange.getEndMillis() / 1000;
@ -378,7 +392,7 @@ public class EventDB {
boolean hasNewColumns() { boolean hasNewColumns() {
/* this relies on the fact that no tskObj has ID 0 but 0 is the default /* 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. */ * value for the datasource_id column in the events table. */
return hasHashHitColumn() && hasDataSourceIDColumn() return hasHashHitColumn() && hasDataSourceIDColumn() && hasTaggedColumn()
&& (getDataSourceIDs().isEmpty() == false); && (getDataSourceIDs().isEmpty() == false);
} }
@ -516,6 +530,15 @@ public class EventDB {
LOGGER.log(Level.SEVERE, "problem upgrading events table", ex); // NON-NLS LOGGER.log(Level.SEVERE, "problem upgrading events table", ex); // NON-NLS
} }
} }
if (hasTaggedColumn() == false) {
try (Statement stmt = con.createStatement()) {
String sql = "ALTER TABLE events ADD COLUMN tagged INTEGER"; // NON-NLS
stmt.execute(sql);
} catch (SQLException ex) {
LOGGER.log(Level.SEVERE, "problem upgrading events table", ex); // NON-NLS
}
}
if (hasHashHitColumn() == false) { if (hasHashHitColumn() == false) {
try (Statement stmt = con.createStatement()) { try (Statement stmt = con.createStatement()) {
@ -553,8 +576,8 @@ public class EventDB {
try { try {
insertRowStmt = prepareStatement( insertRowStmt = prepareStatement(
"INSERT INTO events (datasource_id,file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state, hash_hit) " // NON-NLS "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 + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"); // NON-NLS
getDataSourceIDsStmt = prepareStatement("select distinct datasource_id from events"); // NON-NLS getDataSourceIDsStmt = prepareStatement("select distinct datasource_id from events"); // NON-NLS
getMaxTimeStmt = prepareStatement("select Max(time) as max from events"); // NON-NLS getMaxTimeStmt = prepareStatement("select Max(time) as max from events"); // NON-NLS
@ -565,6 +588,11 @@ public class EventDB {
insertHashSetStmt = prepareStatement("insert or ignore into hash_sets (hash_set_name) values (?)"); insertHashSetStmt = prepareStatement("insert or ignore into hash_sets (hash_set_name) values (?)");
selectHashSetStmt = prepareStatement("select hash_set_id from hash_sets where hash_set_name = ?"); selectHashSetStmt = prepareStatement("select hash_set_id from hash_sets where hash_set_name = ?");
insertHashHitStmt = prepareStatement("insert or ignore into hash_set_hits (hash_set_id, event_id) values (?,?)"); insertHashHitStmt = prepareStatement("insert or ignore into hash_set_hits (hash_set_id, event_id) values (?,?)");
countAllEventsStmt = prepareStatement("select count(*) as count from events");
dropEventsTableStmt = prepareStatement("drop table if exists events");
dropHashSetHitsTableStmt = prepareStatement("drop table if exists hash_set_hits");
dropHashSetsTableStmt = prepareStatement("drop table if exists hash_sets");
dropDBInfoTableStmt = prepareStatement("drop table if exists db_ino");
} catch (SQLException sQLException) { } catch (SQLException sQLException) {
LOGGER.log(Level.SEVERE, "failed to prepareStatment", sQLException); // NON-NLS LOGGER.log(Level.SEVERE, "failed to prepareStatment", sQLException); // NON-NLS
} }
@ -624,17 +652,21 @@ public class EventDB {
return hasDBColumn(EventTableColumn.DATA_SOURCE_ID); return hasDBColumn(EventTableColumn.DATA_SOURCE_ID);
} }
private boolean hasTaggedColumn() {
return hasDBColumn(EventTableColumn.TAGGED);
}
private boolean hasHashHitColumn() { private boolean hasHashHitColumn() {
return hasDBColumn(EventTableColumn.HASH_HIT); return hasDBColumn(EventTableColumn.HASH_HIT);
} }
void insertEvent(long time, EventType type, long datasourceID, Long objID, void insertEvent(long time, EventType type, long datasourceID, Long objID,
Long artifactID, String fullDescription, String medDescription, Long artifactID, String fullDescription, String medDescription,
String shortDescription, TskData.FileKnown known, Set<String> hashSets) { String shortDescription, TskData.FileKnown known, Set<String> hashSets, boolean tagged) {
EventTransaction trans = beginTransaction(); EventTransaction transaction = beginTransaction();
insertEvent(time, type, datasourceID, objID, artifactID, fullDescription, medDescription, shortDescription, known, hashSets, trans); insertEvent(time, type, datasourceID, objID, artifactID, fullDescription, medDescription, shortDescription, known, hashSets, tagged, transaction);
commitTransaction(trans, true); commitTransaction(transaction, true);
} }
/** /**
@ -646,6 +678,7 @@ public class EventDB {
void insertEvent(long time, EventType type, long datasourceID, Long objID, void insertEvent(long time, EventType type, long datasourceID, Long objID,
Long artifactID, String fullDescription, String medDescription, Long artifactID, String fullDescription, String medDescription,
String shortDescription, TskData.FileKnown known, Set<String> hashSetNames, String shortDescription, TskData.FileKnown known, Set<String> hashSetNames,
boolean tagged,
EventTransaction transaction) { EventTransaction transaction) {
if (transaction.isClosed()) { if (transaction.isClosed()) {
@ -660,7 +693,7 @@ public class EventDB {
DBLock.lock(); DBLock.lock();
try { try {
//"INSERT INTO events (datasource_id,file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state, hashHit) " //"INSERT INTO events (datasource_id,file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state, hashHit, tagged) "
insertRowStmt.clearParameters(); insertRowStmt.clearParameters();
insertRowStmt.setLong(1, datasourceID); insertRowStmt.setLong(1, datasourceID);
if (objID != null) { if (objID != null) {
@ -689,6 +722,7 @@ public class EventDB {
insertRowStmt.setByte(10, known == null ? TskData.FileKnown.UNKNOWN.getFileKnownValue() : known.getFileKnownValue()); insertRowStmt.setByte(10, known == null ? TskData.FileKnown.UNKNOWN.getFileKnownValue() : known.getFileKnownValue());
insertRowStmt.setInt(11, hashSetNames.isEmpty() ? 0 : 1); insertRowStmt.setInt(11, hashSetNames.isEmpty() ? 0 : 1);
insertRowStmt.setInt(12, tagged ? 1 : 0);
insertRowStmt.executeUpdate(); insertRowStmt.executeUpdate();
@ -866,6 +900,10 @@ public class EventDB {
return typeMap; return typeMap;
} }
List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
return getAggregatedEvents(params.getTimeRange(), params.getFilter(), params.getTypeZoomLevel(), params.getDescrLOD());
}
/** /**
* //TODO: update javadoc //TODO: split this into helper methods * //TODO: update javadoc //TODO: split this into helper methods
* *
@ -938,6 +976,14 @@ public class EventDB {
hashHits.add(executeQuery.getLong(EventTableColumn.EVENT_ID.toString())); hashHits.add(executeQuery.getLong(EventTableColumn.EVENT_ID.toString()));
} }
} }
HashSet<Long> tagged = new HashSet<>();
try (Statement st3 = con.createStatement();) {
ResultSet executeQuery = st3.executeQuery("select event_id from events where event_id in (" + eventIDS + ") and tagged = 1");
while (executeQuery.next()) {
tagged.add(executeQuery.getLong(EventTableColumn.EVENT_ID.toString()));
}
}
EventType type = useSubTypes ? RootEventType.allTypes.get(rs.getInt(EventTableColumn.SUB_TYPE.toString())) : BaseTypes.values()[rs.getInt(EventTableColumn.BASE_TYPE.toString())]; EventType type = useSubTypes ? RootEventType.allTypes.get(rs.getInt(EventTableColumn.SUB_TYPE.toString())) : BaseTypes.values()[rs.getInt(EventTableColumn.BASE_TYPE.toString())];
@ -946,6 +992,7 @@ public class EventDB {
type, type,
Stream.of(eventIDS.split(",")).map(Long::valueOf).collect(Collectors.toSet()), // NON-NLS Stream.of(eventIDS.split(",")).map(Long::valueOf).collect(Collectors.toSet()), // NON-NLS
hashHits, hashHits,
tagged,
rs.getString(descriptionColumn), lod); rs.getString(descriptionColumn), lod);
//put events in map from type/descrition -> event //put events in map from type/descrition -> event

View File

@ -44,12 +44,18 @@ import org.apache.commons.lang3.StringUtils;
import org.joda.time.Interval; import org.joda.time.Interval;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.services.TagsManager;
import org.sleuthkit.autopsy.coreutils.HashHitUtils; import org.sleuthkit.autopsy.coreutils.HashHitUtils;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.ProgressWindow; import org.sleuthkit.autopsy.timeline.ProgressWindow;
import org.sleuthkit.autopsy.timeline.events.AggregateEvent; import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.events.TimeLineEvent; import org.sleuthkit.autopsy.timeline.events.TimeLineEvent;
import static org.sleuthkit.autopsy.timeline.events.db.Bundle.msgdlg_problem_text;
import static org.sleuthkit.autopsy.timeline.events.db.Bundle.progressWindow_msg_commitingDb;
import static org.sleuthkit.autopsy.timeline.events.db.Bundle.progressWindow_msg_populateMacEventsFiles;
import static org.sleuthkit.autopsy.timeline.events.db.Bundle.progressWindow_msg_reinit_db;
import static org.sleuthkit.autopsy.timeline.events.db.Bundle.progressWindow_populatingXevents;
import org.sleuthkit.autopsy.timeline.events.type.ArtifactEventType; import org.sleuthkit.autopsy.timeline.events.type.ArtifactEventType;
import org.sleuthkit.autopsy.timeline.events.type.EventType; import org.sleuthkit.autopsy.timeline.events.type.EventType;
import org.sleuthkit.autopsy.timeline.events.type.FileSystemTypes; import org.sleuthkit.autopsy.timeline.events.type.FileSystemTypes;
@ -131,14 +137,13 @@ public class EventsRepository {
}).build(CacheLoader.from(eventDB::getEventById)); }).build(CacheLoader.from(eventDB::getEventById));
eventCountsCache = CacheBuilder.newBuilder().maximumSize(1000L).expireAfterAccess(10, TimeUnit.MINUTES).removalListener((RemovalNotification<ZoomParams, Map<EventType, Long>> rn) -> { eventCountsCache = CacheBuilder.newBuilder().maximumSize(1000L).expireAfterAccess(10, TimeUnit.MINUTES).removalListener((RemovalNotification<ZoomParams, Map<EventType, Long>> rn) -> {
//LOGGER.log(Level.INFO, "evicting counts: {0}", rn.toString()); //LOGGER.log(Level.INFO, "evicting counts: {0}", rn.toString());
}).build(CacheLoader.from(eventDB::countEvents)); }).build(CacheLoader.from(eventDB::countEventsByType));
aggregateEventsCache = CacheBuilder.newBuilder().maximumSize(1000L).expireAfterAccess(10, TimeUnit.MINUTES).removalListener((RemovalNotification<ZoomParams, List<AggregateEvent>> rn) -> { aggregateEventsCache = CacheBuilder.newBuilder().maximumSize(1000L).expireAfterAccess(10, TimeUnit.MINUTES).removalListener((RemovalNotification<ZoomParams, List<AggregateEvent>> rn) -> {
//LOGGER.log(Level.INFO, "evicting aggregated events: {0}", rn.toString()); //LOGGER.log(Level.INFO, "evicting aggregated events: {0}", rn.toString());
}).build(CacheLoader.from(eventDB::getAggregatedEvents)); }).build(CacheLoader.from(eventDB::getAggregatedEvents));
maxCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMaxTime)); maxCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMaxTime));
minCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMinTime)); minCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMinTime));
this.modelInstance = new FilteredEventsModel(this, currentStateProperty); this.modelInstance = new FilteredEventsModel(this, currentStateProperty);
} }
/** @return min time (in seconds from unix epoch) */ /** @return min time (in seconds from unix epoch) */
@ -231,30 +236,35 @@ public class EventsRepository {
//TODO: can we avoid this with a state listener? does it amount to the same thing? //TODO: can we avoid this with a state listener? does it amount to the same thing?
//post population operation to execute //post population operation to execute
private final Runnable r; private final Runnable postPopulationOperation;
private final SleuthkitCase skCase;
private final TagsManager tagsManager;
public DBPopulationWorker(Runnable r) { public DBPopulationWorker(Runnable postPopulationOperation) {
progressDialog = new ProgressWindow(null, true, this); progressDialog = new ProgressWindow(null, true, this);
progressDialog.setVisible(true); progressDialog.setVisible(true);
this.r = r;
skCase = autoCase.getSleuthkitCase();
tagsManager = autoCase.getServices().getTagsManager();
this.postPopulationOperation = postPopulationOperation;
} }
@Override @Override
@NbBundle.Messages({"progressWindow.msg.populateMacEventsFiles=populating mac events for files:",
"progressWindow.msg.reinit_db=(re)initializing events database",
"progressWindow.msg.commitingDb=committing events db"})
protected Void doInBackground() throws Exception { protected Void doInBackground() throws Exception {
process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, -1, NbBundle.getMessage(this.getClass(), process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, -1, progressWindow_msg_reinit_db(), "")));
"EventsRepository.progressWindow.msg.reinit_db"), "")));
//reset database //reset database
//TODO: can we do more incremental updates? -jm //TODO: can we do more incremental updates? -jm
eventDB.dropEventsTable(); eventDB.reInitializeDB();
eventDB.initializeDB();
//grab ids of all files //grab ids of all files
SleuthkitCase skCase = autoCase.getSleuthkitCase();
List<Long> files = skCase.findAllFileIdsWhere("name != '.' AND name != '..'"); List<Long> files = skCase.findAllFileIdsWhere("name != '.' AND name != '..'");
final int numFiles = files.size(); final int numFiles = files.size();
process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, numFiles, NbBundle.getMessage(this.getClass(), process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, numFiles, progressWindow_msg_populateMacEventsFiles(), "")));
"EventsRepository.progressWindow.msg.populateMacEventsFiles"), "")));
//insert file events into db //insert file events into db
int i = 1; int i = 1;
@ -266,7 +276,9 @@ public class EventsRepository {
try { try {
AbstractFile f = skCase.getAbstractFileById(fID); AbstractFile f = skCase.getAbstractFileById(fID);
if (f != null) { if (f == null) {
LOGGER.log(Level.WARNING, "Failed to get data for file : {0}", fID); // NON-NLS
} else {
//TODO: This is broken for logical files? fix -jm //TODO: This is broken for logical files? fix -jm
//TODO: logical files don't necessarily have valid timestamps, so ... -jm //TODO: logical files don't necessarily have valid timestamps, so ... -jm
final String uniquePath = f.getUniquePath(); final String uniquePath = f.getUniquePath();
@ -279,26 +291,24 @@ public class EventsRepository {
final TskData.FileKnown known = f.getKnown(); final TskData.FileKnown known = f.getKnown();
boolean hashHit = f.getArtifactsCount(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT) > 0; boolean hashHit = f.getArtifactsCount(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT) > 0;
Set<String> hashSets = hashHit ? HashHitUtils.getHashSetNamesForFile(skCase, f.getId()) : Collections.emptySet(); Set<String> hashSets = hashHit ? HashHitUtils.getHashSetNamesForFile(skCase, f.getId()) : Collections.emptySet();
boolean tagged = !tagsManager.getContentTagsByContent(f).isEmpty();
//insert it into the db if time is > 0 => time is legitimate (drops logical files) //insert it into the db if time is > 0 => time is legitimate (drops logical files)
if (f.getAtime() > 0) { if (f.getAtime() > 0) {
eventDB.insertEvent(f.getAtime(), FileSystemTypes.FILE_ACCESSED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, trans); eventDB.insertEvent(f.getAtime(), FileSystemTypes.FILE_ACCESSED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tagged, trans);
} }
if (f.getMtime() > 0) { if (f.getMtime() > 0) {
eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, trans); eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tagged, trans);
} }
if (f.getCtime() > 0) { if (f.getCtime() > 0) {
eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, trans); eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tagged, trans);
} }
if (f.getCrtime() > 0) { if (f.getCrtime() > 0) {
eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, trans); eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tagged, trans);
} }
process(Arrays.asList(new ProgressWindow.ProgressUpdate(i, numFiles, process(Arrays.asList(new ProgressWindow.ProgressUpdate(i, numFiles,
NbBundle.getMessage(this.getClass(), progressWindow_msg_populateMacEventsFiles(), f.getName())));
"EventsRepository.progressWindow.msg.populateMacEventsFiles2"), f.getName())));
} else {
LOGGER.log(Level.WARNING, "failed to look up data for file : {0}", fID); // NON-NLS
} }
} catch (TskCoreException tskCoreException) { } catch (TskCoreException tskCoreException) {
LOGGER.log(Level.WARNING, "failed to insert mac event for file : " + fID, tskCoreException); // NON-NLS LOGGER.log(Level.WARNING, "failed to insert mac event for file : " + fID, tskCoreException); // NON-NLS
@ -315,12 +325,11 @@ public class EventsRepository {
} }
//skip file_system events, they are already handled above. //skip file_system events, they are already handled above.
if (type instanceof ArtifactEventType) { if (type instanceof ArtifactEventType) {
populateEventType((ArtifactEventType) type, trans, skCase); populateEventType((ArtifactEventType) type, trans);
} }
} }
process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, -1, NbBundle.getMessage(this.getClass(), process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, -1, progressWindow_msg_commitingDb(), "")));
"EventsRepository.progressWindow.msg.commitingDb"), "")));
if (isCancelled()) { if (isCancelled()) {
eventDB.rollBackTransaction(trans); eventDB.rollBackTransaction(trans);
} else { } else {
@ -346,6 +355,8 @@ public class EventsRepository {
} }
@Override @Override
@NbBundle.Messages("msgdlg.problem.text=There was a problem populating the timeline."
+ " Not all events may be present or accurate. See the log for details.")
protected void done() { protected void done() {
super.done(); super.done();
try { try {
@ -356,14 +367,12 @@ public class EventsRepository {
LOGGER.log(Level.INFO, "Database population was cancelled by the user. Not all events may be present or accurate. See the log for details.", ex); // NON-NLS LOGGER.log(Level.INFO, "Database population was cancelled by the user. Not all events may be present or accurate. See the log for details.", ex); // NON-NLS
} catch (InterruptedException | ExecutionException ex) { } catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.WARNING, "Exception while populating database.", ex); // NON-NLS LOGGER.log(Level.WARNING, "Exception while populating database.", ex); // NON-NLS
JOptionPane.showMessageDialog(null, NbBundle.getMessage(this.getClass(), JOptionPane.showMessageDialog(null, msgdlg_problem_text());
"EventsRepository.msgdlg.problem.text"));
} catch (Exception ex) { } catch (Exception ex) {
LOGGER.log(Level.WARNING, "Unexpected exception while populating database.", ex); // NON-NLS LOGGER.log(Level.WARNING, "Unexpected exception while populating database.", ex); // NON-NLS
JOptionPane.showMessageDialog(null, NbBundle.getMessage(this.getClass(), JOptionPane.showMessageDialog(null, msgdlg_problem_text());
"EventsRepository.msgdlg.problem.text"));
} }
r.run(); //execute post db population operation postPopulationOperation.run(); //execute post db population operation
} }
/** /**
@ -373,16 +382,15 @@ public class EventsRepository {
* @param trans the db transaction to use * @param trans the db transaction to use
* @param skCase a reference to the sleuthkit case * @param skCase a reference to the sleuthkit case
*/ */
private void populateEventType(final ArtifactEventType type, EventDB.EventTransaction trans, SleuthkitCase skCase) { @NbBundle.Messages({"# {0} - event type ", "progressWindow.populatingXevents=populating {0} events"})
private void populateEventType(final ArtifactEventType type, EventDB.EventTransaction trans) {
try { try {
//get all the blackboard artifacts corresponding to the given event sub_type //get all the blackboard artifacts corresponding to the given event sub_type
final ArrayList<BlackboardArtifact> blackboardArtifacts = skCase.getBlackboardArtifacts(type.getArtifactType()); final ArrayList<BlackboardArtifact> blackboardArtifacts = skCase.getBlackboardArtifacts(type.getArtifactType());
final int numArtifacts = blackboardArtifacts.size(); final int numArtifacts = blackboardArtifacts.size();
process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, numArtifacts, process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, numArtifacts,
NbBundle.getMessage(this.getClass(), progressWindow_populatingXevents(type.toString()), "")));
"EventsRepository.progressWindow.populatingXevents",
type.toString()), "")));
int i = 0; int i = 0;
for (final BlackboardArtifact bbart : blackboardArtifacts) { for (final BlackboardArtifact bbart : blackboardArtifacts) {
@ -395,7 +403,11 @@ public class EventsRepository {
AbstractFile f = skCase.getAbstractFileById(bbart.getObjectID()); AbstractFile f = skCase.getAbstractFileById(bbart.getObjectID());
boolean hashHit = f.getArtifactsCount(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT) > 0; boolean hashHit = f.getArtifactsCount(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT) > 0;
Set<String> hashSets = hashHit ? HashHitUtils.getHashSetNamesForFile(skCase, f.getId()) : Collections.emptySet(); Set<String> hashSets = hashHit ? HashHitUtils.getHashSetNamesForFile(skCase, f.getId()) : Collections.emptySet();
eventDB.insertEvent(eventDescription.getTime(), type, datasourceID, bbart.getObjectID(), bbart.getArtifactID(), eventDescription.getFullDescription(), eventDescription.getMedDescription(), eventDescription.getShortDescription(), null, hashSets, trans);
boolean tagged = tagsManager.getContentTagsByContent(f).isEmpty() == false;
tagged |= tagsManager.getBlackboardArtifactTagsByArtifact(bbart).isEmpty() == false;
eventDB.insertEvent(eventDescription.getTime(), type, datasourceID, bbart.getObjectID(), bbart.getArtifactID(), eventDescription.getFullDescription(), eventDescription.getMedDescription(), eventDescription.getShortDescription(), null, hashSets, tagged, trans);
} }
i++; i++;

View File

@ -22,7 +22,6 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -73,9 +72,13 @@ import org.sleuthkit.autopsy.timeline.filters.TextFilter;
import org.sleuthkit.autopsy.timeline.filters.TypeFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardArtifactTag;
import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.BlackboardAttribute;
import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
/** Represents an {@link AggregateEvent} in a {@link EventDetailChart}. */ /** Represents an {@link AggregateEvent} in a {@link EventDetailChart}. */
@ -84,6 +87,7 @@ public class AggregateEventNode extends StackPane {
private static final Image HASH_PIN = new Image(AggregateEventNode.class.getResourceAsStream("/org/sleuthkit/autopsy/images/hashset_hits.png")); private static final Image HASH_PIN = new Image(AggregateEventNode.class.getResourceAsStream("/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 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 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); private static final CornerRadii CORNER_RADII = new CornerRadii(3);
@ -145,7 +149,7 @@ public class AggregateEventNode extends StackPane {
private DescriptionVisibility descrVis; private DescriptionVisibility descrVis;
private final SleuthkitCase sleuthkitCase; private final SleuthkitCase sleuthkitCase;
private final FilteredEventsModel eventsModel; private final FilteredEventsModel eventsModel;
private Map<String, Long> hashSetCounts = null;
private Tooltip tooltip; private Tooltip tooltip;
public AggregateEventNode(final AggregateEvent event, AggregateEventNode parentEventNode, EventDetailChart chart) { public AggregateEventNode(final AggregateEvent event, AggregateEventNode parentEventNode, EventDetailChart chart) {
@ -157,10 +161,14 @@ public class AggregateEventNode extends StackPane {
eventsModel = chart.getController().getEventsModel(); eventsModel = chart.getController().getEventsModel();
final Region region = new Region(); final Region region = new Region();
HBox.setHgrow(region, Priority.ALWAYS); HBox.setHgrow(region, Priority.ALWAYS);
ImageView imageView = new ImageView(HASH_PIN); ImageView hashIV = new ImageView(HASH_PIN);
final HBox hBox = new HBox(descrLabel, countLabel, region, imageView, minusButton, plusButton); ImageView tagIV = new ImageView(TAG);
final HBox hBox = new HBox(descrLabel, countLabel, region, hashIV, tagIV, minusButton, plusButton);
if (event.getEventIDsWithHashHits().isEmpty()) { if (event.getEventIDsWithHashHits().isEmpty()) {
hBox.getChildren().remove(imageView); hBox.getChildren().remove(hashIV);
}
if (event.getEventIDsWithTags().isEmpty()) {
hBox.getChildren().remove(tagIV);
} }
hBox.setPrefWidth(USE_COMPUTED_SIZE); hBox.setPrefWidth(USE_COMPUTED_SIZE);
hBox.setMinWidth(USE_PREF_SIZE); hBox.setMinWidth(USE_PREF_SIZE);
@ -252,12 +260,11 @@ public class AggregateEventNode extends StackPane {
} }
private void installTooltip() { private void installTooltip() {
//TODO: all this work should probably go on a background thread...
if (tooltip == null) { if (tooltip == null) {
String collect = "";
HashMap<String, Long> hashSetCounts = new HashMap<>();
if (!event.getEventIDsWithHashHits().isEmpty()) { if (!event.getEventIDsWithHashHits().isEmpty()) {
if (Objects.isNull(hashSetCounts)) {
hashSetCounts = new HashMap<>();
try { try {
for (TimeLineEvent tle : eventsModel.getEventsById(event.getEventIDsWithHashHits())) { for (TimeLineEvent tle : eventsModel.getEventsById(event.getEventIDsWithHashHits())) {
ArrayList<BlackboardArtifact> blackboardArtifacts = sleuthkitCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, tle.getFileID()); ArrayList<BlackboardArtifact> blackboardArtifacts = sleuthkitCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, tle.getFileID());
@ -265,7 +272,7 @@ public class AggregateEventNode extends StackPane {
for (BlackboardAttribute attr : artf.getAttributes()) { for (BlackboardAttribute attr : artf.getAttributes()) {
if (attr.getAttributeTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()) { if (attr.getAttributeTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()) {
hashSetCounts.merge(attr.getValueString(), 1L, Long::sum); hashSetCounts.merge(attr.getValueString(), 1L, Long::sum);
}; }
} }
} }
} }
@ -274,17 +281,49 @@ public class AggregateEventNode extends StackPane {
} }
} }
collect = hashSetCounts.entrySet().stream() Map<Long, TagName> tags = new HashMap<>();
if (!event.getEventIDsWithTags().isEmpty()) {
try {
for (TimeLineEvent tle : eventsModel.getEventsById(event.getEventIDsWithTags())) {
AbstractFile abstractFileById = sleuthkitCase.getAbstractFileById(tle.getFileID());
List<ContentTag> contentTagsByContent = sleuthkitCase.getContentTagsByContent(abstractFileById);
for (ContentTag tag : contentTagsByContent) {
tags.putIfAbsent(tag.getId(), tag.getName());
}
Long artifactID = tle.getArtifactID();
if (artifactID != 0) {
BlackboardArtifact blackboardArtifact = sleuthkitCase.getBlackboardArtifact(artifactID);
List<BlackboardArtifactTag> blackboardArtifactTagsByArtifact = sleuthkitCase.getBlackboardArtifactTagsByArtifact(blackboardArtifact);
for (BlackboardArtifactTag tag : blackboardArtifactTagsByArtifact) {
tags.putIfAbsent(tag.getId(), tag.getName());
}
}
}
} catch (TskCoreException ex) {
Logger.getLogger(AggregateEventNode.class.getName()).log(Level.SEVERE, "Error getting tag info for event.", ex);
}
}
Map<String, Long> tagCounts = tags.values().stream()
.collect(Collectors.toMap(TagName::getDisplayName, anything -> 1L, Long::sum));
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()) .map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
.collect(Collectors.joining("\n")); .collect(Collectors.joining("\n"));
}
tooltip = new Tooltip( tooltip = new Tooltip(
NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text", NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text",
getEvent().getEventIDs().size(), getEvent().getType(), getEvent().getDescription(), getEvent().getEventIDs().size(), getEvent().getType(), getEvent().getDescription(),
getEvent().getSpan().getStart().toString(TimeLineController.getZonedFormatter()), getEvent().getSpan().getStart().toString(TimeLineController.getZonedFormatter()),
getEvent().getSpan().getEnd().toString(TimeLineController.getZonedFormatter())) getEvent().getSpan().getEnd().toString(TimeLineController.getZonedFormatter()))
+ (collect.isEmpty() ? "" : "\n\nHash Set Hits\n" + collect)); + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString)
+ (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString)
);
Tooltip.install(AggregateEventNode.this, tooltip); Tooltip.install(AggregateEventNode.this, tooltip);
} }
} }