diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml
index 0159ec9880..5dabff9f5d 100644
--- a/Core/nbproject/project.xml
+++ b/Core/nbproject/project.xml
@@ -327,6 +327,7 @@
org.sleuthkit.autopsy.events
org.sleuthkit.autopsy.filesearch
org.sleuthkit.autopsy.guiutils
+ org.sleuthkit.autopsy.healthmonitor
org.sleuthkit.autopsy.ingest
org.sleuthkit.autopsy.keywordsearchservice
org.sleuthkit.autopsy.menuactions
diff --git a/Core/src/org/sleuthkit/autopsy/communications/AbstractCVTAction.java b/Core/src/org/sleuthkit/autopsy/communications/AbstractCVTAction.java
new file mode 100644
index 0000000000..ef751d262c
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/communications/AbstractCVTAction.java
@@ -0,0 +1,65 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2018 Basis Technology Corp.
+ * Contact: carrier sleuthkit 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.communications;
+
+import java.util.Collection;
+import javax.swing.AbstractAction;
+import javax.swing.ImageIcon;
+import javax.swing.JMenuItem;
+import org.openide.util.Utilities;
+import org.openide.util.actions.Presenter;
+
+/**
+ * Base class for actions that act on the selected AccountDeviceInstanceKeys.
+ * getPopupPresenter() provides a JMenuItem that works (i.e., has an icon) in
+ * custom context menus and also in the Netbeans Explorer views.
+ */
+abstract class AbstractCVTAction extends AbstractAction implements Presenter.Popup {
+
+ /**
+ * Get the selected accounts that will be acted upon.
+ *
+ * @return The selected accounts
+ */
+ Collection extends AccountDeviceInstanceKey> getSelectedAccounts() {
+ return Utilities.actionsGlobalContext().lookupAll(AccountDeviceInstanceKey.class);
+ }
+
+ @Override
+ public JMenuItem getPopupPresenter() {
+ JMenuItem presenter = new JMenuItem(this);
+ presenter.setText(getActionDisplayName());
+ presenter.setIcon(getIcon());
+ return presenter;
+ }
+
+ /**
+ * The name/text of the action as displayed in a menu.
+ *
+ * @return The diaplay name of this action
+ */
+ abstract String getActionDisplayName();
+
+ /**
+ * The icon to use for this icon.
+ *
+ * @return An ImageIcon used to represent this action.
+ */
+ abstract ImageIcon getIcon();
+}
diff --git a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java
index 40cb15d27d..044cec322f 100644
--- a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java
+++ b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java
@@ -18,17 +18,13 @@
*/
package org.sleuthkit.autopsy.communications;
-import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collection;
-import javax.swing.AbstractAction;
import javax.swing.Action;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Sheet;
import org.openide.util.NbBundle;
-import org.openide.util.Utilities;
import org.openide.util.lookup.Lookups;
import org.sleuthkit.autopsy.datamodel.NodeProperty;
import org.sleuthkit.datamodel.Account;
@@ -106,54 +102,4 @@ final class AccountDeviceInstanceNode extends AbstractNode {
actions.add(ResetAndPinAccountsAction.getInstance());
return actions.toArray(new Action[actions.size()]);
}
-
- /**
- * Action that pins the selected AccountDeviceInstances to the
- * visualization.
- */
- static private class PinAccountsAction extends AbstractAction {
-
- private static final long serialVersionUID = 1L;
- private final static PinAccountsAction instance = new PinAccountsAction();
-
- private static PinAccountsAction getInstance() {
- return instance;
- }
-
- private PinAccountsAction() {
- super("Add Account(s) to Visualization");
- }
-
- @Override
- public void actionPerformed(ActionEvent e) {
- Collection extends AccountDeviceInstanceKey> lookupAll =
- Utilities.actionsGlobalContext().lookupAll(AccountDeviceInstanceKey.class);
- CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(lookupAll, false));
- }
- }
-
- /**
- * Action that pins the selected AccountDeviceInstances to the
- * visualization.
- */
- static private class ResetAndPinAccountsAction extends AbstractAction {
-
- private static final long serialVersionUID = 1L;
- private final static ResetAndPinAccountsAction instance = new ResetAndPinAccountsAction();
-
- private static ResetAndPinAccountsAction getInstance() {
- return instance;
- }
-
- private ResetAndPinAccountsAction() {
- super("Visualize Account(s)");
- }
-
- @Override
- public void actionPerformed(ActionEvent e) {
- Collection extends AccountDeviceInstanceKey> lookupAll =
- Utilities.actionsGlobalContext().lookupAll(AccountDeviceInstanceKey.class);
- CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(lookupAll, true));
- }
- }
}
diff --git a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties
index 559e9bba86..358971eaa8 100644
--- a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties
+++ b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties
@@ -20,9 +20,6 @@ CVTTopComponent.vizPanel.TabConstraints.tabTitle=Visualize
CVTTopComponent.accountsBrowser.TabConstraints.tabTitle_1=Browse
CVTTopComponent.browseVisualizeTabPane.AccessibleContext.accessibleName=Visualize
CVTTopComponent.vizPanel.TabConstraints.tabTitle_1=Visualize
-VisualizationPanel.jButton6.text=Hierarchy
-VisualizationPanel.jButton7.text=Circle
-VisualizationPanel.jButton8.text=Organic
VisualizationPanel.fitGraphButton.text=
VisualizationPanel.jTextArea1.text=Right-click an account in the Browse Accounts table, and select 'Visualize' to begin.
VisualizationPanel.jLabel1.text=Layouts:
@@ -36,11 +33,8 @@ VisualizationPanel.zoomInButton.toolTipText=Zoom in
VisualizationPanel.zoomInButton.text=
VisualizationPanel.zoomOutButton.toolTipText=Zoom out
VisualizationPanel.zoomOutButton.text=
-# 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.
VisualizationPanel.circleLayoutButton.text=Circle
VisualizationPanel.organicLayoutButton.text=Organic
VisualizationPanel.fastOrganicLayoutButton.text=Fast Organic
-VisualizationPanel.hierarchyLayoutButton.text=Hierarchy
+VisualizationPanel.hierarchyLayoutButton.text=Hierarchical
VisualizationPanel.clearVizButton.text_1=Clear Viz.
diff --git a/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java b/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java
index e0f945bb69..6a6a60d3da 100644
--- a/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java
+++ b/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java
@@ -21,7 +21,6 @@ package org.sleuthkit.autopsy.communications;
import com.google.common.collect.ImmutableSet;
import com.google.common.eventbus.EventBus;
import java.util.Collection;
-import java.util.Set;
import org.sleuthkit.datamodel.CommunicationsFilter;
/**
@@ -79,7 +78,7 @@ final class CVTEvents {
return accountDeviceInstances;
}
- public UnpinAccountsEvent(Set accountDeviceInstances) {
+ UnpinAccountsEvent(Collection extends AccountDeviceInstanceKey> accountDeviceInstances) {
this.accountDeviceInstances = ImmutableSet.copyOf(accountDeviceInstances);
}
}
diff --git a/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java b/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java
index f274ab00d7..716d00656c 100644
--- a/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java
+++ b/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java
@@ -25,7 +25,6 @@ import com.google.common.collect.MultimapBuilder;
import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxICell;
import com.mxgraph.util.mxConstants;
-import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxGraph;
import com.mxgraph.view.mxStylesheet;
import java.io.InputStream;
@@ -84,17 +83,19 @@ final class CommunicationsGraph extends mxGraph {
mxStylesheet.getDefaultEdgeStyle().put(mxConstants.STYLE_STARTARROW, mxConstants.NONE);
}
- /* Map from type specific account identifier to mxCell(vertex). */
+ /** Map from type specific account identifier to mxCell(vertex). */
private final Map nodeMap = new HashMap<>();
- /* Map from relationship source (Content) to mxCell (edge). */
+ /** Map from relationship source (Content) to mxCell (edge). */
private final Multimap edgeMap = MultimapBuilder.hashKeys().hashSetValues().build();
private final LockedVertexModel lockedVertexModel;
private final PinnedAccountModel pinnedAccountModel;
- CommunicationsGraph() {
+ CommunicationsGraph(PinnedAccountModel pinnedAccountModel, LockedVertexModel lockedVertexModel) {
super(mxStylesheet);
+ this.pinnedAccountModel = pinnedAccountModel;
+ this.lockedVertexModel = lockedVertexModel;
//set fixed properties of graph.
setAutoSizeCells(true);
setCellsCloneable(false);
@@ -113,21 +114,6 @@ final class CommunicationsGraph extends mxGraph {
setKeepEdgesInBackground(true);
setResetEdgesOnMove(true);
setHtmlLabels(true);
-
- lockedVertexModel = new LockedVertexModel();
- lockedVertexModel.registerhandler((LockedVertexModel.VertexLockEvent event) -> {
- if (event.isVertexLocked()) {
- getView().clear(event.getVertex(), true, true);
- getView().validate();
- } else {
- final mxCellState state = getView().getState(event.getVertex(), true);
- getView().updateLabel(state);
- getView().updateLabelBounds(state);
- getView().updateBoundingBox(state);
- }
- });
-
- pinnedAccountModel = new PinnedAccountModel(this);
}
/**
@@ -255,20 +241,20 @@ final class CommunicationsGraph extends mxGraph {
*/
private class RebuildWorker extends SwingWorker {
- private final ProgressIndicator progress;
+ private final ProgressIndicator progressIndicator;
private final CommunicationsManager commsManager;
private final CommunicationsFilter currentFilter;
RebuildWorker(ProgressIndicator progress, CommunicationsManager commsManager, CommunicationsFilter currentFilter) {
- this.progress = progress;
+ this.progressIndicator = progress;
this.currentFilter = currentFilter;
this.commsManager = commsManager;
}
@Override
- protected Void doInBackground() throws Exception {
- progress.start("Loading accounts");
+ protected Void doInBackground() {
+ progressIndicator.start("Loading accounts");
int progressCounter = 0;
try {
/**
@@ -279,18 +265,18 @@ final class CommunicationsGraph extends mxGraph {
if (isCancelled()) {
break;
}
+ //get accounts related to pinned account
final List relatedAccountDeviceInstances
= commsManager.getRelatedAccountDeviceInstances(adiKey.getAccountDeviceInstance(), currentFilter);
relatedAccounts.put(adiKey.getAccountDeviceInstance(), adiKey);
getOrCreateVertex(adiKey);
- //get accounts related to pinned account
for (final AccountDeviceInstance relatedADI : relatedAccountDeviceInstances) {
final long adiRelationshipsCount = commsManager.getRelationshipSourcesCount(relatedADI, currentFilter);
final AccountDeviceInstanceKey relatedADIKey = new AccountDeviceInstanceKey(relatedADI, currentFilter, adiRelationshipsCount);
relatedAccounts.put(relatedADI, relatedADIKey); //store related accounts
}
- progress.progress(++progressCounter);
+ progressIndicator.progress(++progressCounter);
}
Set accounts = relatedAccounts.keySet();
@@ -298,9 +284,9 @@ final class CommunicationsGraph extends mxGraph {
Map relationshipCounts = commsManager.getRelationshipCountsPairwise(accounts, currentFilter);
int total = relationshipCounts.size();
- int k = 0;
- String progressText = "";
- progress.switchToDeterminate("", 0, total);
+ int progress = 0;
+ String progressText = "";
+ progressIndicator.switchToDeterminate("", 0, total);
for (Map.Entry entry : relationshipCounts.entrySet()) {
Long count = entry.getValue();
AccountPair relationshipKey = entry.getKey();
@@ -308,15 +294,14 @@ final class CommunicationsGraph extends mxGraph {
AccountDeviceInstanceKey account2 = relatedAccounts.get(relationshipKey.getSecond());
if (pinnedAccountModel.isAccountPinned(account1)
- || pinnedAccountModel.isAccountPinned(account2)) {
+ || pinnedAccountModel.isAccountPinned(account2)) {
mxCell addEdge = addOrUpdateEdge(count, account1, account2);
progressText = addEdge.getId();
}
- progress.progress(progressText, k++);
+ progressIndicator.progress(progressText, progress++);
}
} catch (TskCoreException tskCoreException) {
logger.log(Level.SEVERE, "Error", tskCoreException);
- } finally {
}
return null;
@@ -332,7 +317,7 @@ final class CommunicationsGraph extends mxGraph {
} catch (CancellationException ex) {
logger.log(Level.INFO, "Graph visualization cancelled");
} finally {
- progress.finish();
+ progressIndicator.finish();
}
}
}
diff --git a/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java b/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java
index f26e9515b3..7d8b8c083d 100644
--- a/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java
+++ b/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java
@@ -18,20 +18,19 @@
*/
package org.sleuthkit.autopsy.communications;
+import com.google.common.collect.ImmutableSet;
import com.google.common.eventbus.EventBus;
import com.mxgraph.model.mxCell;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
-class LockedVertexModel {
-
- void registerhandler(EventHandler handler) {
- eventBus.register(handler);
- }
-
- void unregisterhandler(EventHandler handler) {
- eventBus.unregister(handler);
- }
+/**
+ * Model of which vertices in a graph are locked ( not moveable by layout
+ * algorithms).
+ *
+ */
+final class LockedVertexModel {
private final EventBus eventBus = new EventBus();
@@ -42,30 +41,34 @@ class LockedVertexModel {
*/
private final Set lockedVertices = new HashSet<>();
- LockedVertexModel() {
+ void registerhandler(Object handler) {
+ eventBus.register(handler);
+ }
+
+ void unregisterhandler(Object handler) {
+ eventBus.unregister(handler);
}
/**
- * Lock the given vertex so that applying a layout algorithm doesn't move
- * it. The user can still manually position the vertex.
+ * Lock the given vertices so that applying a layout algorithm doesn't move
+ * them. The user can still manually position the vertices.
*
* @param vertex The vertex to lock.
*/
- void lockVertex(mxCell vertex) {
- lockedVertices.add(vertex);
- eventBus.post(new VertexLockEvent(vertex, true));
-
+ void lock(Collection vertices) {
+ lockedVertices.addAll(vertices);
+ eventBus.post(new VertexLockEvent(true, vertices));
}
/**
- * Lock the given vertex so that applying a layout algorithm can move it.
+ * Unlock the given vertices so that applying a layout algorithm can move
+ * them.
*
* @param vertex The vertex to unlock.
*/
- void unlockVertex(mxCell vertex) {
- lockedVertices.remove(vertex);
- eventBus.post(new VertexLockEvent(vertex, false));
-
+ void unlock(Collection vertices) {
+ lockedVertices.removeAll(vertices);
+ eventBus.post(new VertexLockEvent(false, vertices));
}
boolean isVertexLocked(mxCell vertex) {
@@ -77,21 +80,36 @@ class LockedVertexModel {
lockedVertices.clear();
}
- static class VertexLockEvent {
+ boolean isEmpty() {
+ return lockedVertices.isEmpty();
+ }
- private final mxCell vertex;
+ /**
+ * Event that represents a change in the locked state of one or more
+ * vertices.
+ */
+ final static class VertexLockEvent {
- public mxCell getVertex() {
- return vertex;
+ private final boolean locked;
+ private final Set vertices;
+
+ /**
+ * @return The vertices whose locked state has changed.
+ */
+ public Set getVertices() {
+ return vertices;
}
- public boolean isVertexLocked() {
+ /**
+ * @return True if the vertices are locked, False if the vertices are
+ * unlocked.
+ */
+ public boolean isLocked() {
return locked;
}
- private final boolean locked;
- VertexLockEvent(mxCell vertex, boolean locked) {
- this.vertex = vertex;
+ VertexLockEvent(boolean locked, Collection< mxCell> vertices) {
+ this.vertices = ImmutableSet.copyOf(vertices);
this.locked = locked;
}
}
diff --git a/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java b/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java
index 68309f5195..7b90357993 100644
--- a/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java
+++ b/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java
@@ -35,7 +35,6 @@ import org.sleuthkit.autopsy.corecomponents.DataResultPanel;
import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable;
import org.sleuthkit.autopsy.corecomponents.TableFilterNode;
import org.sleuthkit.autopsy.directorytree.DataResultFilterNode;
-import org.sleuthkit.datamodel.AccountDeviceInstance;
/**
* The right hand side of the CVT. Has a DataResultPanel to show a listing of
@@ -151,10 +150,10 @@ public final class MessageBrowser extends JPanel implements ExplorerManager.Prov
//Use lookup here?
final AccountDeviceInstanceNode adiNode = (AccountDeviceInstanceNode) selectedNodes[0];
- final Set accountDeviceInstances = new HashSet<>();
+ final Set accountDeviceInstances = new HashSet<>();
for (final Node n : selectedNodes) {
//Use lookup here?
- accountDeviceInstances.add(((AccountDeviceInstanceNode) n).getAccountDeviceInstance());
+ accountDeviceInstances.add(((AccountDeviceInstanceNode) n).getAccountDeviceInstanceKey());
}
return SelectionNode.createFromAccounts(accountDeviceInstances, adiNode.getFilter(), adiNode.getCommsManager());
}
diff --git a/Core/src/org/sleuthkit/autopsy/communications/PinAccountsAction.java b/Core/src/org/sleuthkit/autopsy/communications/PinAccountsAction.java
new file mode 100644
index 0000000000..52136d1b1a
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/communications/PinAccountsAction.java
@@ -0,0 +1,59 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2018 Basis Technology Corp.
+ * Contact: carrier sleuthkit 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.communications;
+
+import java.awt.event.ActionEvent;
+import javax.swing.ImageIcon;
+import org.openide.util.ImageUtilities;
+import org.openide.util.NbBundle;
+
+/**
+ * Action that pins the AccountDevicesIntanceKeys in the ActionsGlobalContext to
+ * the visualizaion
+ */
+@NbBundle.Messages({"PinAccountsAction.pluralText=Add Selected Accounts to Visualization",
+ "PinAccountsAction.singularText=Add Selected Account to Visualization"})
+final class PinAccountsAction extends AbstractCVTAction {
+
+ static private final ImageIcon ICON = ImageUtilities.loadImageIcon(
+ "/org/sleuthkit/autopsy/communications/images/marker--plus.png", false);
+ private static final String SINGULAR_TEXT = Bundle.PinAccountsAction_singularText();
+ private static final String PLURAL_TEXT = Bundle.PinAccountsAction_pluralText();
+
+ private static final PinAccountsAction instance = new PinAccountsAction();
+
+ static PinAccountsAction getInstance() {
+ return instance;
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(getSelectedAccounts(), false));
+ }
+
+ @Override
+ protected String getActionDisplayName() {
+ return getSelectedAccounts().size() > 1 ? PLURAL_TEXT : SINGULAR_TEXT;
+ }
+
+ @Override
+ ImageIcon getIcon() {
+ return ICON;
+ }
+}
diff --git a/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java b/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java
index d5f7a7cc03..f7fb7d1232 100644
--- a/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java
+++ b/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java
@@ -19,22 +19,31 @@
package org.sleuthkit.autopsy.communications;
import com.google.common.collect.ImmutableSet;
+import com.google.common.eventbus.EventBus;
import java.util.HashSet;
import java.util.Set;
+/**
+ * Model of what accounts are pinned to a visualization.
+ */
class PinnedAccountModel {
/**
- * Set of AccountDeviceInstanceKeys that are 'Pinned' to this graph. Pinned
+ * Set of AccountDeviceInstanceKeys that are 'Pinned' to the graph. Pinned
* accounts are shown regardless of filters, and accounts that are related
* to pinned accounts and pass the filters are show. Pinning accounts is the
* primary way to populate the graph.
*/
private final Set pinnedAccountDevices = new HashSet<>();
- private final CommunicationsGraph graph;
- PinnedAccountModel(CommunicationsGraph graph) {
- this.graph = graph;
+ private final EventBus eventBus = new EventBus();
+
+ void registerhandler(Object handler) {
+ eventBus.register(handler);
+ }
+
+ void unregisterhandler(Object handler) {
+ eventBus.unregister(handler);
}
boolean isAccountPinned(AccountDeviceInstanceKey account) {
diff --git a/Core/src/org/sleuthkit/autopsy/communications/ResetAndPinAccountsAction.java b/Core/src/org/sleuthkit/autopsy/communications/ResetAndPinAccountsAction.java
new file mode 100644
index 0000000000..385ac3348b
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/communications/ResetAndPinAccountsAction.java
@@ -0,0 +1,59 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2018 Basis Technology Corp.
+ * Contact: carrier sleuthkit 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.communications;
+
+import java.awt.event.ActionEvent;
+import javax.swing.ImageIcon;
+import org.openide.util.ImageUtilities;
+import org.openide.util.NbBundle;
+
+/**
+ * Action that clears any pinned accounts and pins the AcountDevicesInstanceKeys
+ * in the ActionsGlobalContext to the visualization.
+ */
+@NbBundle.Messages(value = {"ResetAndPinAccountsAction.singularText=Visualize Only Selected Account",
+ "ResetAndPinAccountsAction.pluralText=Visualize Only Selected Accounts"})
+final class ResetAndPinAccountsAction extends AbstractCVTAction {
+
+ private static final ImageIcon ICON = ImageUtilities.loadImageIcon(
+ "/org/sleuthkit/autopsy/communications/images/marker--pin.png", false);
+ private static final String SINGULAR_TEXT = Bundle.ResetAndPinAccountsAction_singularText();
+ private static final String PLURAL_TEXT = Bundle.ResetAndPinAccountsAction_pluralText();
+
+ private static final ResetAndPinAccountsAction instance = new ResetAndPinAccountsAction();
+
+ static ResetAndPinAccountsAction getInstance() {
+ return instance;
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(getSelectedAccounts(), true));
+ }
+
+ @Override
+ protected String getActionDisplayName() {
+ return getSelectedAccounts().size() > 1 ? PLURAL_TEXT : SINGULAR_TEXT;
+ }
+
+ @Override
+ ImageIcon getIcon() {
+ return ICON;
+ }
+}
diff --git a/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java b/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java
index 40739b49cc..1618bb9ecc 100644
--- a/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java
+++ b/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java
@@ -24,10 +24,13 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
+import java.util.stream.Collectors;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.ChildFactory;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
+import org.openide.util.Lookup;
+import org.openide.util.lookup.Lookups;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.datamodel.AccountDeviceInstance;
import org.sleuthkit.datamodel.BlackboardArtifact;
@@ -44,23 +47,27 @@ import org.sleuthkit.datamodel.TskCoreException;
*/
final class SelectionNode extends AbstractNode {
- private SelectionNode(Children children) {
- super(children);
+ private SelectionNode(Children children, Lookup lookup) {
+ super(children, lookup);
}
static SelectionNode createFromAccountsAndRelationships(
Set edgeRelationshipArtifacts,
- Set accountDeviceInstances,
+ Set accountDeviceInstanceKeys,
CommunicationsFilter filter,
CommunicationsManager commsManager) {
+ Set accountDeviceInstances = accountDeviceInstanceKeys.stream()
+ .map(AccountDeviceInstanceKey::getAccountDeviceInstance)
+ .collect(Collectors.toSet());
+
SelectionNode node = new SelectionNode(Children.create(
new RelationshipChildren(
edgeRelationshipArtifacts,
accountDeviceInstances,
commsManager,
filter),
- true));
+ true), Lookups.fixed(accountDeviceInstanceKeys.toArray()));
//This is not good for internationalization!!!
String name = "";
@@ -82,7 +89,7 @@ final class SelectionNode extends AbstractNode {
}
static SelectionNode createFromAccounts(
- Set accountDeviceInstances,
+ Set accountDeviceInstances,
CommunicationsFilter filter,
CommunicationsManager commsManager) {
@@ -122,9 +129,9 @@ final class SelectionNode extends AbstractNode {
}
@Override
- protected Node createNodeForKey(Content t) {
- if (t instanceof BlackboardArtifact) {
- return new RelationshipNode((BlackboardArtifact) t);
+ protected Node createNodeForKey(Content content) {
+ if (content instanceof BlackboardArtifact) {
+ return new RelationshipNode((BlackboardArtifact) content);
} else {
throw new UnsupportedOperationException("Cannot create a RelationshipNode for non BlackboardArtifact content.");
}
diff --git a/Core/src/org/sleuthkit/autopsy/communications/UnpinAccountsAction.java b/Core/src/org/sleuthkit/autopsy/communications/UnpinAccountsAction.java
new file mode 100644
index 0000000000..ba0bbc545b
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/communications/UnpinAccountsAction.java
@@ -0,0 +1,59 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2018 Basis Technology Corp.
+ * Contact: carrier sleuthkit 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.communications;
+
+import java.awt.event.ActionEvent;
+import javax.swing.ImageIcon;
+import org.openide.util.ImageUtilities;
+import org.openide.util.NbBundle;
+
+/**
+ * Action that unpins the AcccountDeviceInstanceKeys in the ActionsGlobalContext
+ * form the visualization.
+ */
+@NbBundle.Messages({"UnpinAccountsAction.pluralText=Remove Selected Accounts",
+ "UnpinAccountsAction.singularText=Remove Selected Account"})
+final class UnpinAccountsAction extends AbstractCVTAction {
+
+ static final private ImageIcon ICON = ImageUtilities.loadImageIcon(
+ "/org/sleuthkit/autopsy/communications/images/marker--minus.png", false);
+ private static final String SINGULAR_TEXT = Bundle.UnpinAccountsAction_singularText();
+ private static final String PLURAL_TEXT = Bundle.UnpinAccountsAction_pluralText();
+
+ private static final UnpinAccountsAction instance = new UnpinAccountsAction();
+
+ static UnpinAccountsAction getInstance() {
+ return instance;
+ }
+
+ @Override
+ public void actionPerformed(final ActionEvent event) {
+ CVTEvents.getCVTEventBus().post(new CVTEvents.UnpinAccountsEvent(getSelectedAccounts()));
+ }
+
+ @Override
+ String getActionDisplayName() {
+ return getSelectedAccounts().size() > 1 ? PLURAL_TEXT : SINGULAR_TEXT;
+ }
+
+ @Override
+ ImageIcon getIcon() {
+ return ICON;
+ }
+}
diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form
index 9d5722443e..a847f700c7 100644
--- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form
+++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form
@@ -68,15 +68,15 @@
+
+
+
-
-
-
@@ -166,9 +166,6 @@
-
-
-
@@ -179,9 +176,6 @@
-
-
-
@@ -192,9 +186,6 @@
-
-
-
@@ -205,9 +196,6 @@
-
-
-
@@ -222,11 +210,11 @@
-
-
+
+
@@ -241,11 +229,11 @@
-
-
+
+
@@ -260,11 +248,11 @@
-
-
+
+
@@ -279,11 +267,11 @@
-
-
+
+
diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java
index cd75ab5185..4f6b53ae39 100644
--- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java
+++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java
@@ -28,7 +28,6 @@ import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxICell;
import com.mxgraph.swing.handler.mxRubberband;
import com.mxgraph.swing.mxGraphComponent;
-import com.mxgraph.swing.util.mxMorphing;
import com.mxgraph.util.mxEvent;
import com.mxgraph.util.mxEventObject;
import com.mxgraph.util.mxEventSource;
@@ -36,11 +35,14 @@ import com.mxgraph.util.mxPoint;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.util.mxUndoManager;
import com.mxgraph.util.mxUndoableEdit;
+import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxGraph;
+import com.mxgraph.view.mxGraphView;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
+import java.awt.Font;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@@ -51,12 +53,18 @@ import java.beans.PropertyChangeEvent;
import java.beans.PropertyVetoException;
import java.text.DecimalFormat;
import java.util.Arrays;
-import static java.util.Collections.singleton;
import java.util.EnumSet;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.Map;
import java.util.concurrent.Future;
+import java.util.function.BiConsumer;
import java.util.logging.Level;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.JButton;
@@ -79,12 +87,11 @@ import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.lookup.ProxyLookup;
import org.sleuthkit.autopsy.casemodule.Case;
-import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.progress.ModalDialogProgressIndicator;
-import org.sleuthkit.datamodel.AccountDeviceInstance;
import org.sleuthkit.datamodel.CommunicationsFilter;
import org.sleuthkit.datamodel.CommunicationsManager;
import org.sleuthkit.datamodel.Content;
@@ -100,23 +107,17 @@ import org.sleuthkit.datamodel.TskCoreException;
* CVTTopComponent when this tab is active allowing for context sensitive
* actions to work correctly.
*/
-@NbBundle.Messages("VisualizationPanel.cancelButton.text=Cancel")
final public class VisualizationPanel extends JPanel implements Lookup.Provider {
private static final long serialVersionUID = 1L;
private static final Logger logger = Logger.getLogger(VisualizationPanel.class.getName());
private static final String BASE_IMAGE_PATH = "/org/sleuthkit/autopsy/communications/images";
- static final private ImageIcon pinIcon
- = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--pin.png"));
- static final private ImageIcon addPinIcon
- = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--plus.png"));
- static final private ImageIcon unpinIcon
- = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--minus.png"));
static final private ImageIcon unlockIcon
= new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/lock_large_unlocked.png"));
static final private ImageIcon lockIcon
= new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/lock_large_locked.png"));
+ @NbBundle.Messages("VisualizationPanel.cancelButton.text=Cancel")
private static final String CANCEL = Bundle.VisualizationPanel_cancelButton_text();
private final ExplorerManager vizEM = new ExplorerManager();
@@ -131,27 +132,20 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
private final CommunicationsGraph graph;
private final mxUndoManager undoManager = new mxUndoManager();
- private final mxRubberband rubberband;
- private final mxFastOrganicLayout fastOrganicLayout;
- private final mxCircleLayout circleLayout;
- private final mxOrganicLayout organicLayout;
- private final mxHierarchicalLayout hierarchicalLayout;
+ private final mxRubberband rubberband; //NOPMD We keep a referenec as insurance to prevent garbage collection
@ThreadConfined(type = ThreadConfined.ThreadType.AWT)
private SwingWorker, ?> worker;
- private final PinnedAccountModel pinnedAccountModel;
- private final LockedVertexModel lockedVertexModel;
+ private final PinnedAccountModel pinnedAccountModel = new PinnedAccountModel();
+ private final LockedVertexModel lockedVertexModel = new LockedVertexModel();
+
+ private final Map layoutButtons = new HashMap<>();
+ private NamedGraphLayout currentLayout;
public VisualizationPanel() {
initComponents();
- graph = new CommunicationsGraph();
- pinnedAccountModel = graph.getPinnedAccountModel();
- lockedVertexModel = graph.getLockedVertexModel();
- fastOrganicLayout = new mxFastOrganicLayoutImpl(graph);
- circleLayout = new mxCircleLayoutImpl(graph);
- organicLayout = new mxOrganicLayoutImpl(graph);
- hierarchicalLayout = new mxHierarchicalLayoutImpl(graph);
+ graph = new CommunicationsGraph(pinnedAccountModel, lockedVertexModel);
graphComponent = new mxGraphComponent(graph);
graphComponent.setAutoExtend(true);
@@ -165,123 +159,90 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
graphComponent.setBackground(Color.WHITE);
borderLayoutPanel.add(graphComponent, BorderLayout.CENTER);
- //install rubber band selection handler
+ //install rubber band other handlers
rubberband = new mxRubberband(graphComponent);
+ lockedVertexModel.registerhandler(this);
+
final mxEventSource.mxIEventListener scaleListener = (Object sender, mxEventObject evt)
-> zoomLabel.setText(DecimalFormat.getPercentInstance().format(graph.getView().getScale()));
graph.getView().addListener(mxEvent.SCALE, scaleListener);
graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, scaleListener);
- graphComponent.getGraphControl().addMouseWheelListener(new MouseAdapter() {
- /**
- * Translate mouse wheel events into zooming.
- *
- * @param event The MouseWheelEvent
- */
- @Override
- public void mouseWheelMoved(final MouseWheelEvent event) {
- super.mouseWheelMoved(event);
- if (event.getPreciseWheelRotation() > 0) {
- graphComponent.zoomIn();
- } else if (event.getPreciseWheelRotation() < 0) {
- graphComponent.zoomOut();
- }
- }
- });
+ final GraphMouseListener graphMouseListener = new GraphMouseListener();
+ graphComponent.getGraphControl().addMouseWheelListener(graphMouseListener);
+ graphComponent.getGraphControl().addMouseListener(graphMouseListener);
- graphComponent.getGraphControl().addMouseListener(new MouseAdapter() {
- /**
- * Right click handler: show context menu.
- *
- * @param event The MouseEvent
- */
- @Override
- public void mouseClicked(final MouseEvent event) {
- super.mouseClicked(event);
- if (SwingUtilities.isRightMouseButton(event)) {
- final mxCell cellAt = (mxCell) graphComponent.getCellAt(event.getX(), event.getY());
- if (cellAt != null && cellAt.isVertex()) {
- final JPopupMenu jPopupMenu = new JPopupMenu();
- final AccountDeviceInstanceKey adiKey = (AccountDeviceInstanceKey) cellAt.getValue();
-
- if (lockedVertexModel.isVertexLocked(cellAt)) {
- jPopupMenu.add(new JMenuItem(new AbstractAction("UnLock " + cellAt.getId(), unlockIcon) {
- @Override
- public void actionPerformed(final ActionEvent event) {
- lockedVertexModel.unlockVertex(cellAt);
- }
- }));
- } else {
- jPopupMenu.add(new JMenuItem(new AbstractAction("Lock " + cellAt.getId(), lockIcon) {
- @Override
- public void actionPerformed(final ActionEvent event) {
- lockedVertexModel.lockVertex(cellAt);
- }
- }));
- }
- if (pinnedAccountModel.isAccountPinned(adiKey)) {
- jPopupMenu.add(new JMenuItem(new AbstractAction("Unpin " + cellAt.getId(), unpinIcon) {
- @Override
- public void actionPerformed(final ActionEvent event) {
- handleUnPinEvent(new CVTEvents.UnpinAccountsEvent(singleton((AccountDeviceInstanceKey) cellAt.getValue())));
- }
- }));
- } else {
- jPopupMenu.add(new JMenuItem(new AbstractAction("Pin " + cellAt.getId(), addPinIcon) {
- @Override
- public void actionPerformed(final ActionEvent event) {
- handlePinEvent(new CVTEvents.PinAccountsEvent(singleton((AccountDeviceInstanceKey) cellAt.getValue()), false));
- }
- }));
- jPopupMenu.add(new JMenuItem(new AbstractAction("Pin only " + cellAt.getId(), pinIcon) {
- @Override
- public void actionPerformed(final ActionEvent event) {
- handlePinEvent(new CVTEvents.PinAccountsEvent(singleton((AccountDeviceInstanceKey) cellAt.getValue()), true));
- }
- }));
- }
- jPopupMenu.show(graphComponent.getGraphControl(), event.getX(), event.getY());
- }
- }
- }
- });
final MessageBrowser messageBrowser = new MessageBrowser(vizEM, gacEM);
-
splitPane.setRightComponent(messageBrowser);
-
proxyLookup = new ProxyLookup(
- messageBrowser.getLookup(),
- ExplorerUtils.createLookup(vizEM, getActionMap()));
+ ExplorerUtils.createLookup(vizEM, getActionMap()),
+ messageBrowser.getLookup()
+ );
//feed selection to explorermanager
- graph.getSelectionModel().addListener(null, new SelectionListener());
+ graph.getSelectionModel().addListener(mxEvent.CHANGE, new SelectionListener());
final mxEventSource.mxIEventListener undoListener = (Object sender, mxEventObject evt)
-> undoManager.undoableEditHappened((mxUndoableEdit) evt.getProperty("edit"));
graph.getModel().addListener(mxEvent.UNDO, undoListener);
graph.getView().addListener(mxEvent.UNDO, undoListener);
+
+ FastOrganicLayoutImpl fastOrganicLayout = new FastOrganicLayoutImpl(graph);
+ CircleLayoutImpl circleLayout = new CircleLayoutImpl(graph);
+ OrganicLayoutImpl organicLayout = new OrganicLayoutImpl(graph);
+ organicLayout.setMaxIterations(10);
+ HierarchicalLayoutImpl hierarchyLayout = new HierarchicalLayoutImpl(graph);
+
+ //local method to configure layout buttons
+ BiConsumer configure = (layoutButton, layout) -> {
+ layoutButtons.put(layout, layoutButton);
+ layoutButton.addActionListener(event -> applyLayout(layout));
+ };
+ //configure layout buttons.
+ configure.accept(circleLayoutButton, circleLayout);
+ configure.accept(organicLayoutButton, organicLayout);
+ configure.accept(fastOrganicLayoutButton, fastOrganicLayout);
+ configure.accept(hierarchyLayoutButton, hierarchyLayout);
+
+ applyLayout(fastOrganicLayout);
}
+ /**
+ *
+ * @param layoutButton the value of layoutButton
+ * @param layout the value of layout
+ */
@Override
-
public Lookup getLookup() {
return proxyLookup;
}
@Subscribe
- void handleUnPinEvent(final CVTEvents.UnpinAccountsEvent pinEvent) {
+ void handle(LockedVertexModel.VertexLockEvent event) {
+ final Set vertices = event.getVertices();
+ mxGraphView view = graph.getView();
+ vertices.forEach(vertex -> {
+ final mxCellState state = view.getState(vertex, true);
+ view.updateLabel(state);
+ view.updateLabelBounds(state);
+ view.updateBoundingBox(state);
+ graphComponent.redraw(state);
+ });
+ }
+
+ @Subscribe
+ void handle(final CVTEvents.UnpinAccountsEvent pinEvent) {
graph.getModel().beginUpdate();
pinnedAccountModel.unpinAccount(pinEvent.getAccountDeviceInstances());
graph.clear();
rebuildGraph();
// Updates the display
graph.getModel().endUpdate();
-
}
@Subscribe
- void handlePinEvent(final CVTEvents.PinAccountsEvent pinEvent) {
+ void handle(final CVTEvents.PinAccountsEvent pinEvent) {
graph.getModel().beginUpdate();
if (pinEvent.isReplace()) {
graph.resetGraph();
@@ -290,19 +251,16 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
rebuildGraph();
// Updates the display
graph.getModel().endUpdate();
-
}
@Subscribe
- void handleFilterEvent(final CVTEvents.FilterChangeEvent filterChangeEvent) {
-
+ void handle(final CVTEvents.FilterChangeEvent filterChangeEvent) {
graph.getModel().beginUpdate();
graph.clear();
currentFilter = filterChangeEvent.getNewFilter();
rebuildGraph();
// Updates the display
graph.getModel().endUpdate();
-
}
@ThreadConfined(type = ThreadConfined.ThreadType.AWT)
@@ -327,14 +285,12 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
if (worker.isCancelled()) {
graph.resetGraph();
rebuildGraph();
- } else {
- morph(fastOrganicLayout);
}
+ applyLayout(currentLayout);
}
});
worker.execute();
-
}
}
@@ -351,7 +307,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
logger.log(Level.SEVERE, "Can't get CommunicationsManager when there is no case open.", ex);
}
- Case.addEventTypeSubscriber(EnumSet.of(CURRENT_CASE), evt -> {
+ Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), evt -> {
graph.getModel().beginUpdate();
try {
graph.resetGraph();
@@ -368,15 +324,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
logger.log(Level.SEVERE, "Error getting CommunicationsManager for the current case.", ex);
}
}
-
});
}
- @Override
- public void removeNotify() {
- super.removeNotify();
- }
-
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
@@ -413,11 +363,11 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
borderLayoutPanel.setLayout(new BorderLayout());
+ jTextArea1.setBackground(new Color(240, 240, 240));
jTextArea1.setColumns(20);
jTextArea1.setLineWrap(true);
jTextArea1.setRows(5);
jTextArea1.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.jTextArea1.text")); // NOI18N
- jTextArea1.setBackground(new Color(240, 240, 240));
GroupLayout placeHolderPanelLayout = new GroupLayout(placeHolderPanel);
placeHolderPanel.setLayout(placeHolderPanelLayout);
@@ -442,49 +392,29 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
hierarchyLayoutButton.setFocusable(false);
hierarchyLayoutButton.setHorizontalTextPosition(SwingConstants.CENTER);
hierarchyLayoutButton.setVerticalTextPosition(SwingConstants.BOTTOM);
- hierarchyLayoutButton.addActionListener(new ActionListener() {
- public void actionPerformed(ActionEvent evt) {
- hierarchyLayoutButtonActionPerformed(evt);
- }
- });
fastOrganicLayoutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fastOrganicLayoutButton.text")); // NOI18N
fastOrganicLayoutButton.setFocusable(false);
fastOrganicLayoutButton.setHorizontalTextPosition(SwingConstants.CENTER);
fastOrganicLayoutButton.setVerticalTextPosition(SwingConstants.BOTTOM);
- fastOrganicLayoutButton.addActionListener(new ActionListener() {
- public void actionPerformed(ActionEvent evt) {
- fastOrganicLayoutButtonActionPerformed(evt);
- }
- });
organicLayoutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.organicLayoutButton.text")); // NOI18N
organicLayoutButton.setFocusable(false);
organicLayoutButton.setHorizontalTextPosition(SwingConstants.CENTER);
organicLayoutButton.setVerticalTextPosition(SwingConstants.BOTTOM);
- organicLayoutButton.addActionListener(new ActionListener() {
- public void actionPerformed(ActionEvent evt) {
- organicLayoutButtonActionPerformed(evt);
- }
- });
circleLayoutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.circleLayoutButton.text")); // NOI18N
circleLayoutButton.setFocusable(false);
circleLayoutButton.setHorizontalTextPosition(SwingConstants.CENTER);
circleLayoutButton.setVerticalTextPosition(SwingConstants.BOTTOM);
- circleLayoutButton.addActionListener(new ActionListener() {
- public void actionPerformed(ActionEvent evt) {
- circleLayoutButtonActionPerformed(evt);
- }
- });
jSeparator1.setOrientation(SwingConstants.VERTICAL);
zoomOutButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-out-red.png"))); // NOI18N
zoomOutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomOutButton.text")); // NOI18N
+ zoomOutButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomOutButton.toolTipText")); // NOI18N
zoomOutButton.setFocusable(false);
zoomOutButton.setHorizontalTextPosition(SwingConstants.CENTER);
- zoomOutButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomOutButton.toolTipText")); // NOI18N
zoomOutButton.setVerticalTextPosition(SwingConstants.BOTTOM);
zoomOutButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
@@ -494,9 +424,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
zoomInButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-in-green.png"))); // NOI18N
zoomInButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomInButton.text")); // NOI18N
+ zoomInButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomInButton.toolTipText")); // NOI18N
zoomInButton.setFocusable(false);
zoomInButton.setHorizontalTextPosition(SwingConstants.CENTER);
- zoomInButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomInButton.toolTipText")); // NOI18N
zoomInButton.setVerticalTextPosition(SwingConstants.BOTTOM);
zoomInButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
@@ -506,9 +436,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
zoomActualButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-actual.png"))); // NOI18N
zoomActualButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomActualButton.text")); // NOI18N
+ zoomActualButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomActualButton.toolTipText")); // NOI18N
zoomActualButton.setFocusable(false);
zoomActualButton.setHorizontalTextPosition(SwingConstants.CENTER);
- zoomActualButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomActualButton.toolTipText")); // NOI18N
zoomActualButton.setVerticalTextPosition(SwingConstants.BOTTOM);
zoomActualButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
@@ -518,9 +448,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
fitZoomButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-fit.png"))); // NOI18N
fitZoomButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fitZoomButton.text")); // NOI18N
+ fitZoomButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fitZoomButton.toolTipText")); // NOI18N
fitZoomButton.setFocusable(false);
fitZoomButton.setHorizontalTextPosition(SwingConstants.CENTER);
- fitZoomButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fitZoomButton.toolTipText")); // NOI18N
fitZoomButton.setVerticalTextPosition(SwingConstants.BOTTOM);
fitZoomButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
@@ -620,21 +550,55 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
graphComponent.zoomOut();
}//GEN-LAST:event_zoomOutButtonActionPerformed
- private void circleLayoutButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_circleLayoutButtonActionPerformed
- morph(circleLayout);
- }//GEN-LAST:event_circleLayoutButtonActionPerformed
+ /**
+ * Apply the given layout. The given layout becomes the current layout. The
+ * layout is computed in the background.
+ *
+ * @param layout The layout to apply.
+ */
+ @NbBundle.Messages({"VisualizationPanel.computingLayout=Computing Layout",
+ "# {0} - layout name",
+ "VisualizationPanel.layoutFailWithLockedVertices.text={0} layout failed with locked vertices. Unlock some vertices or try a different layout.",
+ "# {0} - layout name",
+ "VisualizationPanel.layoutFail.text={0} layout failed. Try a different layout."})
+ private void applyLayout(NamedGraphLayout layout) {
+ currentLayout = layout;
+ layoutButtons.forEach((layoutKey, button)
+ -> button.setFont(button.getFont().deriveFont(layoutKey == layout ? Font.BOLD : Font.PLAIN)));
- private void organicLayoutButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_organicLayoutButtonActionPerformed
- applyOrganicLayout(10);
- }//GEN-LAST:event_organicLayoutButtonActionPerformed
+ ModalDialogProgressIndicator progressIndicator = new ModalDialogProgressIndicator(windowAncestor, Bundle.VisualizationPanel_computingLayout());
+ progressIndicator.start(Bundle.VisualizationPanel_computingLayout());
- private void fastOrganicLayoutButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_fastOrganicLayoutButtonActionPerformed
- morph(fastOrganicLayout);
- }//GEN-LAST:event_fastOrganicLayoutButtonActionPerformed
+ new SwingWorker() {
+ @Override
+ protected Void doInBackground() {
+ graph.getModel().beginUpdate();
+ try {
+ layout.execute(graph.getDefaultParent());
+ fitGraph();
+ } finally {
+ graph.getModel().endUpdate();
+ progressIndicator.finish();
+ }
+ return null;
+ }
- private void hierarchyLayoutButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_hierarchyLayoutButtonActionPerformed
- morph(hierarchicalLayout);
- }//GEN-LAST:event_hierarchyLayoutButtonActionPerformed
+ @Override
+ protected void done() {
+ try {
+ get();
+ } catch (InterruptedException | ExecutionException ex) {
+ logger.log(Level.WARNING, "CVT graph layout failed.", ex);
+ if (lockedVertexModel.isEmpty()) {
+ MessageNotifyUtil.Message.error(Bundle.VisualizationPanel_layoutFail_text(layout.getDisplayName()));
+ } else {
+ MessageNotifyUtil.Message.error(Bundle.VisualizationPanel_layoutFailWithLockedVertices_text(layout.getDisplayName()));
+ }
+ undoManager.undo();
+ }
+ }
+ }.execute();
+ }
private void clearVizButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_clearVizButtonActionPerformed
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
@@ -645,14 +609,8 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
// Updates the display
graph.getModel().endUpdate();
setCursor(Cursor.getDefaultCursor());
-
}//GEN-LAST:event_clearVizButtonActionPerformed
- private void applyOrganicLayout(int iterations) {
- organicLayout.setMaxIterations(iterations);
- morph(organicLayout);
- }
-
private void fitGraph() {
graphComponent.zoomTo(1, true);
mxPoint translate = graph.getView().getTranslate();
@@ -675,57 +633,11 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
final Dimension size = graphComponent.getSize();
final double widthFactor = size.getWidth() / boundsForCells.getWidth();
+ final double heightFactor = size.getHeight() / boundsForCells.getHeight();
- graphComponent.zoom(widthFactor);
-
+ graphComponent.zoom((heightFactor + widthFactor) / 2.0);
}
- private void morph(mxIGraphLayout layout) {
- // layout using morphing
- graph.getModel().beginUpdate();
-
- CancelationListener cancelationListener = new CancelationListener();
- ModalDialogProgressIndicator progress = new ModalDialogProgressIndicator(windowAncestor, "Computing layout", new String[]{CANCEL}, CANCEL, cancelationListener);
- SwingWorker morphWorker = new SwingWorker() {
- @Override
- protected Void doInBackground() {
- progress.start("Computing layout");
- layout.execute(graph.getDefaultParent());
- if (isCancelled()) {
- progress.finish();
- return null;
- }
- mxMorphing morph = new mxMorphing(graphComponent, 20, 1.2, 20) {
- @Override
- public void updateAnimation() {
- fireEvent(new mxEventObject(mxEvent.EXECUTE));
- super.updateAnimation(); //To change body of generated methods, choose Tools | Templates.
- }
-
- };
- morph.addListener(mxEvent.EXECUTE, (Object sender, mxEventObject evt) -> {
- if (isCancelled()) {
- morph.stopAnimation();
- }
- });
- morph.addListener(mxEvent.DONE, (Object sender, mxEventObject event) -> {
- graph.getModel().endUpdate();
- if (isCancelled()) {
- undoManager.undo();
- } else {
- fitGraph();
- }
- progress.finish();
- });
-
- morph.startAnimation();
- return null;
-
- }
- };
- cancelationListener.configure(morphWorker, progress);
- morphWorker.execute();
- }
// Variables declaration - do not modify//GEN-BEGIN:variables
private JPanel borderLayoutPanel;
@@ -749,6 +661,10 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
private JButton zoomOutButton;
// End of variables declaration//GEN-END:variables
+ /**
+ * Listens to graph selection model and updates ExplorerManager to reflect
+ * changes in selection.
+ */
final private class SelectionListener implements mxEventSource.mxIEventListener {
@SuppressWarnings("unchecked")
@@ -760,7 +676,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
if (selectionCells.length > 0) {
mxICell[] selectedCells = Arrays.asList(selectionCells).toArray(new mxCell[selectionCells.length]);
HashSet relationshipSources = new HashSet<>();
- HashSet adis = new HashSet<>();
+ HashSet adis = new HashSet<>();
for (mxICell cell : selectedCells) {
if (cell.isEdge()) {
mxICell source = (mxICell) graph.getModel().getTerminal(cell, true);
@@ -777,7 +693,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
logger.log(Level.SEVERE, " Error getting relationsips....", tskCoreException);
}
} else if (cell.isVertex()) {
- adis.add(((AccountDeviceInstanceKey) cell.getValue()).getAccountDeviceInstance());
+ adis.add((AccountDeviceInstanceKey) cell.getValue());
}
}
@@ -793,9 +709,20 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
}
}
- final private class mxFastOrganicLayoutImpl extends mxFastOrganicLayout {
+ /**
+ * Extend mxIGraphLayout with a getDisplayName method,
+ */
+ private interface NamedGraphLayout extends mxIGraphLayout {
- mxFastOrganicLayoutImpl(mxGraph graph) {
+ String getDisplayName();
+ }
+
+ /**
+ * Extension of mxFastOrganicLayout that ignores locked vertices.
+ */
+ final private class FastOrganicLayoutImpl extends mxFastOrganicLayout implements NamedGraphLayout {
+
+ FastOrganicLayoutImpl(mxGraph graph) {
super(graph);
}
@@ -806,18 +733,26 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
}
@Override
- public mxRectangle setVertexLocation(Object vertex, double x, double y) {
+ public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x, y are standard coordinate names
if (isVertexIgnored(vertex)) {
return getVertexBounds(vertex);
} else {
return super.setVertexLocation(vertex, x, y);
}
}
+
+ @Override
+ public String getDisplayName() {
+ return "Fast Organic";
+ }
}
- final private class mxCircleLayoutImpl extends mxCircleLayout {
+ /**
+ * Extension of mxCircleLayout that ignores locked vertices.
+ */
+ final private class CircleLayoutImpl extends mxCircleLayout implements NamedGraphLayout {
- mxCircleLayoutImpl(mxGraph graph) {
+ CircleLayoutImpl(mxGraph graph) {
super(graph);
setResetEdges(true);
}
@@ -829,18 +764,26 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
}
@Override
- public mxRectangle setVertexLocation(Object vertex, double x, double y) {
+ public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x, y are standard coordinate names
if (isVertexIgnored(vertex)) {
return getVertexBounds(vertex);
} else {
return super.setVertexLocation(vertex, x, y);
}
}
+
+ @Override
+ public String getDisplayName() {
+ return "Circle";
+ }
}
- final private class mxOrganicLayoutImpl extends mxOrganicLayout {
+ /**
+ * Extension of mxOrganicLayout that ignores locked vertices.
+ */
+ final private class OrganicLayoutImpl extends mxOrganicLayout implements NamedGraphLayout {
- mxOrganicLayoutImpl(mxGraph graph) {
+ OrganicLayoutImpl(mxGraph graph) {
super(graph);
setResetEdges(true);
}
@@ -852,18 +795,26 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
}
@Override
- public mxRectangle setVertexLocation(Object vertex, double x, double y) {
+ public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x, y are standard coordinate names
if (isVertexIgnored(vertex)) {
return getVertexBounds(vertex);
} else {
return super.setVertexLocation(vertex, x, y);
}
}
+
+ @Override
+ public String getDisplayName() {
+ return "Organic";
+ }
}
- final private class mxHierarchicalLayoutImpl extends mxHierarchicalLayout {
+ /**
+ * Extension of mxHierarchicalLayout that ignores locked vertices.
+ */
+ final private class HierarchicalLayoutImpl extends mxHierarchicalLayout implements NamedGraphLayout {
- mxHierarchicalLayoutImpl(mxGraph graph) {
+ HierarchicalLayoutImpl(mxGraph graph) {
super(graph);
}
@@ -874,15 +825,24 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
}
@Override
- public mxRectangle setVertexLocation(Object vertex, double x, double y) {
+ public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x, y are standard coordinate names
if (isVertexIgnored(vertex)) {
return getVertexBounds(vertex);
} else {
return super.setVertexLocation(vertex, x, y);
}
}
+
+ @Override
+ public String getDisplayName() {
+ return "Hierarchical";
+ }
}
+ /**
+ * Listener that closses the given ModalDialogProgressIndicator and cancels
+ * the future.
+ */
private class CancelationListener implements ActionListener {
private Future> cancellable;
@@ -899,6 +859,108 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider
cancellable.cancel(true);
progress.finish();
}
+ }
+ /**
+ * Mouse Adapter for the graphComponent. Handles wheel zooming and context
+ * menus.
+ */
+ private class GraphMouseListener extends MouseAdapter {
+
+ /**
+ * Translate mouse wheel events into zooming.
+ *
+ * @param event The MouseWheelEvent
+ */
+ @Override
+ public void mouseWheelMoved(final MouseWheelEvent event) {
+ super.mouseWheelMoved(event);
+ if (event.getPreciseWheelRotation() < 0) {
+ graphComponent.zoomIn();
+ } else if (event.getPreciseWheelRotation() > 0) {
+ graphComponent.zoomOut();
+ }
+ }
+
+ /**
+ * Right click handler: show context menu.
+ *
+ * @param event The MouseEvent
+ */
+ @Override
+ public void mouseClicked(final MouseEvent event) {
+ super.mouseClicked(event);
+ if (SwingUtilities.isRightMouseButton(event)) {
+ final mxCell cellAt = (mxCell) graphComponent.getCellAt(event.getX(), event.getY());
+ if (cellAt != null && cellAt.isVertex()) {
+ final JPopupMenu jPopupMenu = new JPopupMenu();
+ final AccountDeviceInstanceKey adiKey = (AccountDeviceInstanceKey) cellAt.getValue();
+
+ Set selectedVertices
+ = Stream.of(graph.getSelectionModel().getCells())
+ .map(mxCell.class::cast)
+ .filter(mxCell::isVertex)
+ .collect(Collectors.toSet());
+
+ if (lockedVertexModel.isVertexLocked(cellAt)) {
+ jPopupMenu.add(new JMenuItem(new UnlockAction(selectedVertices)));
+ } else {
+ jPopupMenu.add(new JMenuItem(new LockAction(selectedVertices)));
+ }
+ if (pinnedAccountModel.isAccountPinned(adiKey)) {
+ jPopupMenu.add(UnpinAccountsAction.getInstance().getPopupPresenter());
+ } else {
+ jPopupMenu.add(PinAccountsAction.getInstance().getPopupPresenter());
+ jPopupMenu.add(ResetAndPinAccountsAction.getInstance().getPopupPresenter());
+ }
+ jPopupMenu.show(graphComponent.getGraphControl(), event.getX(), event.getY());
+ }
+ }
+ }
+ }
+
+ /**
+ * Action that un-locks the selected vertices.
+ */
+ @NbBundle.Messages({
+ "VisualizationPanel.unlockAction.singularText=Unlock Selected Account",
+ "VisualizationPanel.unlockAction.pluralText=Unlock Selected Accounts",})
+ private final class UnlockAction extends AbstractAction {
+
+ private final Set selectedVertices;
+
+ UnlockAction(Set selectedVertices) {
+ super(selectedVertices.size() > 1 ? Bundle.VisualizationPanel_unlockAction_pluralText() : Bundle.VisualizationPanel_unlockAction_singularText(),
+ unlockIcon);
+ this.selectedVertices = selectedVertices;
+ }
+
+ @Override
+
+ public void actionPerformed(final ActionEvent event) {
+ lockedVertexModel.unlock(selectedVertices);
+ }
+ }
+
+ /**
+ * Action that locks the selected vertices.
+ */
+ @NbBundle.Messages({
+ "VisualizationPanel.lockAction.singularText=Lock Selected Account",
+ "VisualizationPanel.lockAction.pluralText=Lock Selected Accounts"})
+ private final class LockAction extends AbstractAction {
+
+ private final Set selectedVertices;
+
+ LockAction(Set selectedVertices) {
+ super(selectedVertices.size() > 1 ? Bundle.VisualizationPanel_lockAction_pluralText() : Bundle.VisualizationPanel_lockAction_singularText(),
+ lockIcon);
+ this.selectedVertices = selectedVertices;
+ }
+
+ @Override
+ public void actionPerformed(final ActionEvent event) {
+ lockedVertexModel.lock(selectedVertices);
+ }
}
}
diff --git a/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java b/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java
index 9b2afff6b4..d7ef8c4750 100644
--- a/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java
+++ b/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java
@@ -457,7 +457,8 @@ public final class CoordinationService {
CASES("cases"),
MANIFESTS("manifests"),
CONFIG("config"),
- CENTRAL_REPO("centralRepository");
+ CENTRAL_REPO("centralRepository"),
+ HEALTH_MONITOR("healthMonitor");
private final String displayName;
diff --git a/Core/src/org/sleuthkit/autopsy/core/Installer.java b/Core/src/org/sleuthkit/autopsy/core/Installer.java
index 83250f719d..63ac880171 100644
--- a/Core/src/org/sleuthkit/autopsy/core/Installer.java
+++ b/Core/src/org/sleuthkit/autopsy/core/Installer.java
@@ -216,6 +216,7 @@ public class Installer extends ModuleInstall {
packageInstallers.add(org.sleuthkit.autopsy.datamodel.Installer.getDefault());
packageInstallers.add(org.sleuthkit.autopsy.ingest.Installer.getDefault());
packageInstallers.add(org.sleuthkit.autopsy.centralrepository.eventlisteners.Installer.getDefault());
+ packageInstallers.add(org.sleuthkit.autopsy.healthmonitor.Installer.getDefault());
}
/**
diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java
index 3b6878ce23..a06d3eeeea 100644
--- a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java
+++ b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java
@@ -408,6 +408,7 @@ final public class Accounts implements AutopsyVisitableItem {
protected void addNotify() {
IngestManager.getInstance().addIngestJobEventListener(pcl);
IngestManager.getInstance().addIngestModuleEventListener(pcl);
+ Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl);
super.addNotify();
}
@@ -415,6 +416,7 @@ final public class Accounts implements AutopsyVisitableItem {
protected void removeNotify() {
IngestManager.getInstance().removeIngestJobEventListener(pcl);
IngestManager.getInstance().removeIngestModuleEventListener(pcl);
+ Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl);
super.removeNotify();
}
@@ -569,6 +571,7 @@ final public class Accounts implements AutopsyVisitableItem {
protected void addNotify() {
IngestManager.getInstance().addIngestJobEventListener(pcl);
IngestManager.getInstance().addIngestModuleEventListener(pcl);
+ Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl);
super.addNotify();
}
@@ -576,6 +579,7 @@ final public class Accounts implements AutopsyVisitableItem {
protected void removeNotify() {
IngestManager.getInstance().removeIngestJobEventListener(pcl);
IngestManager.getInstance().removeIngestModuleEventListener(pcl);
+ Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl);
super.removeNotify();
}
@@ -691,6 +695,7 @@ final public class Accounts implements AutopsyVisitableItem {
protected void addNotify() {
IngestManager.getInstance().addIngestJobEventListener(pcl);
IngestManager.getInstance().addIngestModuleEventListener(pcl);
+ Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl);
super.addNotify();
}
@@ -698,6 +703,7 @@ final public class Accounts implements AutopsyVisitableItem {
protected void removeNotify() {
IngestManager.getInstance().removeIngestJobEventListener(pcl);
IngestManager.getInstance().removeIngestModuleEventListener(pcl);
+ Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl);
super.removeNotify();
}
@@ -901,6 +907,7 @@ final public class Accounts implements AutopsyVisitableItem {
protected void addNotify() {
IngestManager.getInstance().addIngestJobEventListener(pcl);
IngestManager.getInstance().addIngestModuleEventListener(pcl);
+ Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl);
super.addNotify();
}
@@ -908,6 +915,7 @@ final public class Accounts implements AutopsyVisitableItem {
protected void removeNotify() {
IngestManager.getInstance().removeIngestJobEventListener(pcl);
IngestManager.getInstance().removeIngestModuleEventListener(pcl);
+ Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl);
super.removeNotify();
}
diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java
new file mode 100644
index 0000000000..35bc6b6bfa
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java
@@ -0,0 +1,785 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2018 Basis Technology Corp.
+ * Contact: carrier sleuthkit 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.healthmonitor;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import org.apache.commons.dbcp2.BasicDataSource;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
+import org.sleuthkit.autopsy.core.UserPreferences;
+import org.sleuthkit.autopsy.core.UserPreferencesException;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.coreutils.ModuleSettings;
+import org.sleuthkit.autopsy.coreutils.ThreadUtils;
+import org.sleuthkit.datamodel.CaseDbConnectionInfo;
+import org.sleuthkit.datamodel.CaseDbSchemaVersionNumber;
+
+/**
+ * Class for recording data on the health of the system.
+ *
+ * For timing data:
+ * Modules will call getTimingMetric() before the code to be timed to get a TimingMetric object
+ * Modules will call submitTimingMetric() with the obtained TimingMetric object to log it
+ */
+public final class EnterpriseHealthMonitor implements PropertyChangeListener {
+
+ private final static Logger logger = Logger.getLogger(EnterpriseHealthMonitor.class.getName());
+ private final static String DATABASE_NAME = "EnterpriseHealthMonitor";
+ private final static String MODULE_NAME = "EnterpriseHealthMonitor";
+ private final static String IS_ENABLED_KEY = "is_enabled";
+ private final static long DATABASE_WRITE_INTERVAL = 60; // Minutes
+ public static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION
+ = new CaseDbSchemaVersionNumber(1, 0);
+
+ private static final AtomicBoolean isEnabled = new AtomicBoolean(false);
+ private static EnterpriseHealthMonitor instance;
+
+ private final ExecutorService healthMonitorExecutor;
+ private static final String HEALTH_MONITOR_EVENT_THREAD_NAME = "Health-Monitor-Event-Listener-%d";
+
+ private ScheduledThreadPoolExecutor healthMonitorOutputTimer;
+ private final Map timingInfoMap;
+ private static final int CONN_POOL_SIZE = 10;
+ private BasicDataSource connectionPool = null;
+ private String hostName;
+
+ private EnterpriseHealthMonitor() throws HealthMonitorException {
+
+ // Create the map to collect timing metrics. The map will exist regardless
+ // of whether the monitor is enabled.
+ timingInfoMap = new HashMap<>();
+
+ // Set up the executor to handle case events
+ healthMonitorExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(HEALTH_MONITOR_EVENT_THREAD_NAME).build());
+
+ // Get the host name
+ try {
+ hostName = java.net.InetAddress.getLocalHost().getHostName();
+ } catch (java.net.UnknownHostException ex) {
+ // Continue on, but log the error and generate a UUID to use for this session
+ hostName = UUID.randomUUID().toString();
+ logger.log(Level.SEVERE, "Unable to look up host name - falling back to UUID " + hostName, ex);
+ }
+
+ // Read from module settings to determine if the module is enabled
+ if (ModuleSettings.settingExists(MODULE_NAME, IS_ENABLED_KEY)) {
+ if(ModuleSettings.getConfigSetting(MODULE_NAME, IS_ENABLED_KEY).equals("true")){
+ isEnabled.set(true);
+ try {
+ activateMonitor();
+ } catch (HealthMonitorException ex) {
+ // If we failed to activate it, then disable the monitor
+ logger.log(Level.SEVERE, "Health monitor activation failed - disabling health monitor");
+ setEnabled(false);
+ throw ex;
+ }
+ return;
+ }
+ }
+ isEnabled.set(false);
+ }
+
+ /**
+ * Get the instance of the EnterpriseHealthMonitor
+ * @return the instance
+ * @throws HealthMonitorException
+ */
+ synchronized static EnterpriseHealthMonitor getInstance() throws HealthMonitorException {
+ if (instance == null) {
+ instance = new EnterpriseHealthMonitor();
+ Case.addPropertyChangeListener(instance);
+ }
+ return instance;
+ }
+
+ /**
+ * Activate the health monitor.
+ * Creates/initialized the database (if needed), clears any existing metrics
+ * out of the maps, and sets up the timer for writing to the database.
+ * @throws HealthMonitorException
+ */
+ private synchronized void activateMonitor() throws HealthMonitorException {
+
+ logger.log(Level.INFO, "Activating Servies Health Monitor");
+
+ if (!UserPreferences.getIsMultiUserModeEnabled()) {
+ throw new HealthMonitorException("Multi user mode is not enabled - can not activate health monitor");
+ }
+
+ // Set up database (if needed)
+ try (CoordinationService.Lock lock = getExclusiveDbLock()) {
+ if(lock == null) {
+ throw new HealthMonitorException("Error getting database lock");
+ }
+
+ // Check if the database exists
+ if (! databaseExists()) {
+
+ // If not, create a new one
+ createDatabase();
+ }
+
+ if( ! databaseIsInitialized()) {
+ initializeDatabaseSchema();
+ }
+
+ } catch (CoordinationService.CoordinationServiceException ex) {
+ throw new HealthMonitorException("Error releasing database lock", ex);
+ }
+
+ // Clear out any old data
+ timingInfoMap.clear();
+
+ // Start the timer for database writes
+ startTimer();
+ }
+
+ /**
+ * Deactivate the health monitor.
+ * This should only be used when disabling the monitor, not when Autopsy is closing.
+ * Clears out any metrics that haven't been written, stops the database write timer,
+ * and shuts down the connection pool.
+ * @throws HealthMonitorException
+ */
+ private synchronized void deactivateMonitor() throws HealthMonitorException {
+
+ logger.log(Level.INFO, "Deactivating Servies Health Monitor");
+
+ // Clear out the collected data
+ timingInfoMap.clear();
+
+ // Stop the timer
+ stopTimer();
+
+ // Shut down the connection pool
+ shutdownConnections();
+ }
+
+ /**
+ * Start the ScheduledThreadPoolExecutor that will handle the database writes.
+ */
+ private synchronized void startTimer() {
+ // Make sure the previous executor (if it exists) has been stopped
+ stopTimer();
+
+ healthMonitorOutputTimer = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("health_monitor_timer").build());
+ healthMonitorOutputTimer.scheduleWithFixedDelay(new DatabaseWriteTask(), DATABASE_WRITE_INTERVAL, DATABASE_WRITE_INTERVAL, TimeUnit.MINUTES);
+ }
+
+ /**
+ * Stop the ScheduledThreadPoolExecutor to prevent further database writes.
+ */
+ private synchronized void stopTimer() {
+ if(healthMonitorOutputTimer != null) {
+ ThreadUtils.shutDownTaskExecutor(healthMonitorOutputTimer);
+ }
+ }
+
+ /**
+ * Called from the installer to set up the Health Monitor instance at startup.
+ * @throws HealthMonitorException
+ */
+ static synchronized void startUpIfEnabled() throws HealthMonitorException {
+ getInstance();
+ }
+
+ /**
+ * Enabled/disable the health monitor.
+ * @param enabled true to enable the monitor, false to disable it
+ * @throws HealthMonitorException
+ */
+ static synchronized void setEnabled(boolean enabled) throws HealthMonitorException {
+ if(enabled == isEnabled.get()) {
+ // The setting has not changed, so do nothing
+ return;
+ }
+
+ if(enabled) {
+ getInstance().activateMonitor();
+
+ // If activateMonitor fails, we won't update either of these
+ ModuleSettings.setConfigSetting(MODULE_NAME, IS_ENABLED_KEY, "true");
+ isEnabled.set(true);
+ } else {
+ ModuleSettings.setConfigSetting(MODULE_NAME, IS_ENABLED_KEY, "false");
+ isEnabled.set(false);
+ getInstance().deactivateMonitor();
+ }
+ }
+
+ /**
+ * Get a metric that will measure the time to execute a section of code.
+ * Call this before the section of code to be timed and then
+ * submit it afterward using submitTimingMetric().
+ * This method is safe to call regardless of whether the Enterprise Health
+ * Monitor is enabled.
+ * @param name A short but descriptive name describing the code being timed.
+ * This name will appear in the UI.
+ * @return The TimingMetric object
+ */
+ public static TimingMetric getTimingMetric(String name) {
+ if(isEnabled.get()) {
+ return new TimingMetric(name);
+ }
+ return null;
+ }
+
+ /**
+ * Submit the metric that was previously obtained through getTimingMetric().
+ * Call this immediately after the section of code being timed.
+ * This method is safe to call regardless of whether the Enterprise Health
+ * Monitor is enabled.
+ * @param metric The TimingMetric object obtained from getTimingMetric()
+ */
+ public static void submitTimingMetric(TimingMetric metric) {
+ if(isEnabled.get() && (metric != null)) {
+ metric.stopTiming();
+ try {
+ getInstance().addTimingMetric(metric);
+ } catch (HealthMonitorException ex) {
+ // We don't want calling methods to have to check for exceptions, so just log it
+ logger.log(Level.SEVERE, "Error adding timing metric", ex);
+ }
+ }
+ }
+
+ /**
+ * Add the timing metric data to the map.
+ * @param metric The metric to add. stopTiming() should already have been called.
+ */
+ private void addTimingMetric(TimingMetric metric) throws HealthMonitorException {
+
+ // Do as little as possible within the synchronized block to minimize
+ // blocking with multiple threads.
+ synchronized(this) {
+ // There's a small check-then-act situation here where isEnabled
+ // may have changed before reaching this code. This is fine -
+ // the map still exists and any extra data added after the monitor
+ // is disabled will be deleted if the monitor is re-enabled. This
+ // seems preferable to doing another check on isEnabled within
+ // the synchronized block.
+ if(timingInfoMap.containsKey(metric.getName())) {
+ timingInfoMap.get(metric.getName()).addMetric(metric);
+ } else {
+ timingInfoMap.put(metric.getName(), new TimingInfo(metric));
+ }
+ }
+ }
+
+ /**
+ * Write the collected metrics to the database.
+ * @throws HealthMonitorException
+ */
+ private void writeCurrentStateToDatabase() throws HealthMonitorException {
+
+ Map timingMapCopy;
+
+ // Do as little as possible within the synchronized block since it will
+ // block threads attempting to record metrics.
+ synchronized(this) {
+ if(! isEnabled.get()) {
+ return;
+ }
+
+ // Make a shallow copy of the timing map. The map should be small - one entry
+ // per metric name.
+ timingMapCopy = new HashMap<>(timingInfoMap);
+ timingInfoMap.clear();
+ }
+
+ // Check if there's anything to report (right now we only have the timing map)
+ if(timingMapCopy.keySet().isEmpty()) {
+ return;
+ }
+
+ logger.log(Level.INFO, "Writing health monitor metrics to database");
+
+ // Write to the database
+ try (CoordinationService.Lock lock = getSharedDbLock()) {
+ if(lock == null) {
+ throw new HealthMonitorException("Error getting database lock");
+ }
+
+ Connection conn = connect();
+ if(conn == null) {
+ throw new HealthMonitorException("Error getting database connection");
+ }
+
+ // Add timing metrics to the database
+ String addTimingInfoSql = "INSERT INTO timing_data (name, host, timestamp, count, average, max, min) VALUES (?, ?, ?, ?, ?, ?, ?)";
+ try (PreparedStatement statement = conn.prepareStatement(addTimingInfoSql)) {
+
+ for(String name:timingMapCopy.keySet()) {
+ TimingInfo info = timingMapCopy.get(name);
+
+ statement.setString(1, name);
+ statement.setString(2, hostName);
+ statement.setLong(3, System.currentTimeMillis());
+ statement.setLong(4, info.getCount());
+ statement.setLong(5, info.getAverage());
+ statement.setLong(6, info.getMax());
+ statement.setLong(7, info.getMin());
+
+ statement.execute();
+ }
+
+ } catch (SQLException ex) {
+ throw new HealthMonitorException("Error saving metric data to database", ex);
+ } finally {
+ try {
+ conn.close();
+ } catch (SQLException ex) {
+ logger.log(Level.SEVERE, "Error closing Connection.", ex);
+ }
+ }
+ } catch (CoordinationService.CoordinationServiceException ex) {
+ throw new HealthMonitorException("Error releasing database lock", ex);
+ }
+ }
+
+ /**
+ * Check whether the health monitor database exists.
+ * Does not check the schema.
+ * @return true if the database exists, false otherwise
+ * @throws HealthMonitorException
+ */
+ private boolean databaseExists() throws HealthMonitorException {
+ try {
+ // Use the same database settings as the case
+ CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
+ Class.forName("org.postgresql.Driver"); //NON-NLS
+ ResultSet rs = null;
+ try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS
+ Statement statement = connection.createStatement();) {
+ String createCommand = "SELECT 1 AS result FROM pg_database WHERE datname='" + DATABASE_NAME + "'";
+ rs = statement.executeQuery(createCommand);
+ if(rs.next()) {
+ logger.log(Level.INFO, "Existing Enterprise Health Monitor database found");
+ return true;
+ }
+ } finally {
+ if(rs != null) {
+ rs.close();
+ }
+ }
+ } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) {
+ throw new HealthMonitorException("Failed check for health monitor database", ex);
+ }
+ return false;
+ }
+
+ /**
+ * Create a new health monitor database.
+ * @throws HealthMonitorException
+ */
+ private void createDatabase() throws HealthMonitorException {
+ try {
+ // Use the same database settings as the case
+ CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
+ Class.forName("org.postgresql.Driver"); //NON-NLS
+ try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS
+ Statement statement = connection.createStatement();) {
+ String createCommand = "CREATE DATABASE \"" + DATABASE_NAME + "\" OWNER \"" + db.getUserName() + "\""; //NON-NLS
+ statement.execute(createCommand);
+ }
+ logger.log(Level.INFO, "Created new health monitor database " + DATABASE_NAME);
+ } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) {
+ throw new HealthMonitorException("Failed to delete health monitor database", ex);
+ }
+ }
+
+ /**
+ * Setup a connection pool for db connections.
+ * @throws HealthMonitorException
+ */
+ private void setupConnectionPool() throws HealthMonitorException {
+ try {
+ CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
+
+ connectionPool = new BasicDataSource();
+ connectionPool.setDriverClassName("org.postgresql.Driver");
+
+ StringBuilder connectionURL = new StringBuilder();
+ connectionURL.append("jdbc:postgresql://");
+ connectionURL.append(db.getHost());
+ connectionURL.append(":");
+ connectionURL.append(db.getPort());
+ connectionURL.append("/");
+ connectionURL.append(DATABASE_NAME);
+
+ connectionPool.setUrl(connectionURL.toString());
+ connectionPool.setUsername(db.getUserName());
+ connectionPool.setPassword(db.getPassword());
+
+ // tweak pool configuration
+ connectionPool.setInitialSize(3); // start with 3 connections
+ connectionPool.setMaxIdle(CONN_POOL_SIZE); // max of 10 idle connections
+ connectionPool.setValidationQuery("SELECT version()");
+ } catch (UserPreferencesException ex) {
+ throw new HealthMonitorException("Error loading database configuration", ex);
+ }
+ }
+
+ /**
+ * Shut down the connection pool
+ * @throws HealthMonitorException
+ */
+ private void shutdownConnections() throws HealthMonitorException {
+ try {
+ synchronized(this) {
+ if(connectionPool != null){
+ connectionPool.close();
+ connectionPool = null; // force it to be re-created on next connect()
+ }
+ }
+ } catch (SQLException ex) {
+ throw new HealthMonitorException("Failed to close existing database connections.", ex); // NON-NLS
+ }
+ }
+
+ /**
+ * Get a database connection.
+ * Sets up the connection pool if needed.
+ * @return The Connection object
+ * @throws HealthMonitorException
+ */
+ private Connection connect() throws HealthMonitorException {
+ synchronized (this) {
+ if (connectionPool == null) {
+ setupConnectionPool();
+ }
+ }
+
+ try {
+ return connectionPool.getConnection();
+ } catch (SQLException ex) {
+ throw new HealthMonitorException("Error getting connection from connection pool.", ex); // NON-NLS
+ }
+ }
+
+ /**
+ * Test whether the database schema has been initialized.
+ * We do this by looking for the version number.
+ * @return True if it has been initialized, false otherwise.
+ * @throws HealthMonitorException
+ */
+ private boolean databaseIsInitialized() throws HealthMonitorException {
+ Connection conn = connect();
+ if(conn == null) {
+ throw new HealthMonitorException("Error getting database connection");
+ }
+ ResultSet resultSet = null;
+
+ try (Statement statement = conn.createStatement()) {
+ resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_VERSION'");
+ return resultSet.next();
+ } catch (SQLException ex) {
+ // This likely just means that the db_info table does not exist
+ return false;
+ } finally {
+ if(resultSet != null) {
+ try {
+ resultSet.close();
+ } catch (SQLException ex) {
+ logger.log(Level.SEVERE, "Error closing result set", ex);
+ }
+ }
+ try {
+ conn.close();
+ } catch (SQLException ex) {
+ logger.log(Level.SEVERE, "Error closing Connection.", ex);
+ }
+ }
+ }
+
+ /**
+ * Get the current schema version
+ * @return the current schema version
+ * @throws HealthMonitorException
+ */
+ private CaseDbSchemaVersionNumber getVersion() throws HealthMonitorException {
+ Connection conn = connect();
+ if(conn == null) {
+ throw new HealthMonitorException("Error getting database connection");
+ }
+ ResultSet resultSet = null;
+
+ try (Statement statement = conn.createStatement()) {
+ int minorVersion = 0;
+ int majorVersion = 0;
+ resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_MINOR_VERSION'");
+ if (resultSet.next()) {
+ String minorVersionStr = resultSet.getString("value");
+ try {
+ minorVersion = Integer.parseInt(minorVersionStr);
+ } catch (NumberFormatException ex) {
+ throw new HealthMonitorException("Bad value for schema minor version (" + minorVersionStr + ") - database is corrupt");
+ }
+ }
+
+ resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_VERSION'");
+ if (resultSet.next()) {
+ String majorVersionStr = resultSet.getString("value");
+ try {
+ majorVersion = Integer.parseInt(majorVersionStr);
+ } catch (NumberFormatException ex) {
+ throw new HealthMonitorException("Bad value for schema version (" + majorVersionStr + ") - database is corrupt");
+ }
+ }
+
+ return new CaseDbSchemaVersionNumber(majorVersion, minorVersion);
+ } catch (SQLException ex) {
+ throw new HealthMonitorException("Error initializing database", ex);
+ } finally {
+ if(resultSet != null) {
+ try {
+ resultSet.close();
+ } catch (SQLException ex) {
+ logger.log(Level.SEVERE, "Error closing result set", ex);
+ }
+ }
+ try {
+ conn.close();
+ } catch (SQLException ex) {
+ logger.log(Level.SEVERE, "Error closing Connection.", ex);
+ }
+ }
+ }
+
+ /**
+ * Initialize the database.
+ * @throws HealthMonitorException
+ */
+ private void initializeDatabaseSchema() throws HealthMonitorException {
+ Connection conn = connect();
+ if(conn == null) {
+ throw new HealthMonitorException("Error getting database connection");
+ }
+
+ try (Statement statement = conn.createStatement()) {
+ conn.setAutoCommit(false);
+
+ String createTimingTable =
+ "CREATE TABLE IF NOT EXISTS timing_data (" +
+ "id SERIAL PRIMARY KEY," +
+ "name text NOT NULL," +
+ "host text NOT NULL," +
+ "timestamp bigint NOT NULL," +
+ "count bigint NOT NULL," +
+ "average bigint NOT NULL," +
+ "max bigint NOT NULL," +
+ "min bigint NOT NULL" +
+ ")";
+ statement.execute(createTimingTable);
+
+ String createDbInfoTable =
+ "CREATE TABLE IF NOT EXISTS db_info (" +
+ "id SERIAL PRIMARY KEY NOT NULL," +
+ "name text NOT NULL," +
+ "value text NOT NULL" +
+ ")";
+ statement.execute(createDbInfoTable);
+
+ statement.execute("INSERT INTO db_info (name, value) VALUES ('SCHEMA_VERSION', '" + CURRENT_DB_SCHEMA_VERSION.getMajor() + "')");
+ statement.execute("INSERT INTO db_info (name, value) VALUES ('SCHEMA_MINOR_VERSION', '" + CURRENT_DB_SCHEMA_VERSION.getMinor() + "')");
+
+ conn.commit();
+ } catch (SQLException ex) {
+ try {
+ conn.rollback();
+ } catch (SQLException ex2) {
+ logger.log(Level.SEVERE, "Rollback error");
+ }
+ throw new HealthMonitorException("Error initializing database", ex);
+ } finally {
+ try {
+ conn.close();
+ } catch (SQLException ex) {
+ logger.log(Level.SEVERE, "Error closing connection.", ex);
+ }
+ }
+ }
+
+ /**
+ * The task called by the ScheduledThreadPoolExecutor to handle
+ * the database writes.
+ */
+ static final class DatabaseWriteTask implements Runnable {
+
+ /**
+ * Write current metric data to the database
+ */
+ @Override
+ public void run() {
+ try {
+ getInstance().writeCurrentStateToDatabase();
+ } catch (HealthMonitorException ex) {
+ logger.log(Level.SEVERE, "Error writing current metrics to database", ex); //NON-NLS
+ }
+ }
+ }
+
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+
+ switch (Case.Events.valueOf(evt.getPropertyName())) {
+
+ case CURRENT_CASE:
+ if ((null == evt.getNewValue()) && (evt.getOldValue() instanceof Case)) {
+ // When a case is closed, write the current metrics to the database
+ healthMonitorExecutor.submit(new EnterpriseHealthMonitor.DatabaseWriteTask());
+ }
+ break;
+ }
+ }
+
+ /**
+ * Get an exclusive lock for the health monitor database.
+ * Acquire this before creating, initializing, or updating the database schema.
+ * @return The lock
+ * @throws HealthMonitorException
+ */
+ private CoordinationService.Lock getExclusiveDbLock() throws HealthMonitorException{
+ try {
+ CoordinationService.Lock lock = CoordinationService.getInstance().tryGetExclusiveLock(CoordinationService.CategoryNode.HEALTH_MONITOR, DATABASE_NAME, 5, TimeUnit.MINUTES);
+
+ if(lock != null){
+ return lock;
+ }
+ throw new HealthMonitorException("Error acquiring database lock");
+ } catch (InterruptedException | CoordinationService.CoordinationServiceException ex){
+ throw new HealthMonitorException("Error acquiring database lock", ex);
+ }
+ }
+
+ /**
+ * Get an shared lock for the health monitor database.
+ * Acquire this before database reads or writes.
+ * @return The lock
+ * @throws HealthMonitorException
+ */
+ private CoordinationService.Lock getSharedDbLock() throws HealthMonitorException{
+ try {
+ String databaseNodeName = DATABASE_NAME;
+ CoordinationService.Lock lock = CoordinationService.getInstance().tryGetSharedLock(CoordinationService.CategoryNode.HEALTH_MONITOR, databaseNodeName, 5, TimeUnit.MINUTES);
+
+ if(lock != null){
+ return lock;
+ }
+ throw new HealthMonitorException("Error acquiring database lock");
+ } catch (InterruptedException | CoordinationService.CoordinationServiceException ex){
+ throw new HealthMonitorException("Error acquiring database lock");
+ }
+ }
+
+ /**
+ * Internal class for collecting timing metrics.
+ * Instead of storing each TimingMetric, we only store the min and max
+ * seen and the number of metrics and total duration to compute the average
+ * later.
+ * One TimingInfo instance should be created per metric name, and
+ * additional timing metrics will be added to it.
+ */
+ private class TimingInfo {
+ private long count; // Number of metrics collected
+ private long sum; // Sum of the durations collected (nanoseconds)
+ private long max; // Maximum value found (nanoseconds)
+ private long min; // Minimum value found (nanoseconds)
+
+ TimingInfo(TimingMetric metric) throws HealthMonitorException {
+ count = 1;
+ sum = metric.getDuration();
+ max = metric.getDuration();
+ min = metric.getDuration();
+ }
+
+ /**
+ * Add a new TimingMetric to an existing TimingInfo object.
+ * This is called in a synchronized block for almost all new
+ * TimingMetric objects, so do as little processing here as possible.
+ * @param metric The new metric
+ * @throws HealthMonitorException Will be thrown if the metric hasn't been stopped
+ */
+ void addMetric(TimingMetric metric) throws HealthMonitorException {
+
+ // Keep track of needed info to calculate the average
+ count++;
+ sum += metric.getDuration();
+
+ // Check if this is the longest duration seen
+ if(max < metric.getDuration()) {
+ max = metric.getDuration();
+ }
+
+ // Check if this is the lowest duration seen
+ if(min > metric.getDuration()) {
+ min = metric.getDuration();
+ }
+ }
+
+ /**
+ * Get the average duration
+ * @return average duration (nanoseconds)
+ */
+ long getAverage() {
+ return sum / count;
+ }
+
+ /**
+ * Get the maximum duration
+ * @return maximum duration (nanoseconds)
+ */
+ long getMax() {
+ return max;
+ }
+
+ /**
+ * Get the minimum duration
+ * @return minimum duration (nanoseconds)
+ */
+ long getMin() {
+ return min;
+ }
+
+ /**
+ * Get the total number of metrics collected
+ * @return number of metrics collected
+ */
+ long getCount() {
+ return count;
+ }
+ }
+}
diff --git a/Core/src/org/sleuthkit/autopsy/communications/EventHandler.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorException.java
similarity index 64%
rename from Core/src/org/sleuthkit/autopsy/communications/EventHandler.java
rename to Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorException.java
index f1acedf1cf..6df918acaa 100644
--- a/Core/src/org/sleuthkit/autopsy/communications/EventHandler.java
+++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorException.java
@@ -16,13 +16,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-package org.sleuthkit.autopsy.communications;
+package org.sleuthkit.autopsy.healthmonitor;
/**
- *
+ * Exception used internally by the Services Health Monitor
*/
-public interface EventHandler {
+class HealthMonitorException extends Exception {
+ private static final long serialVersionUID = 1L;
- void handle(T event);
+ HealthMonitorException(String message) {
+ super(message);
+ }
+
+ HealthMonitorException(String message, Throwable cause) {
+ super(message, cause);
+ }
}
diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java
new file mode 100644
index 0000000000..61ea5a5244
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java
@@ -0,0 +1,53 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2018 Basis Technology Corp.
+ * Contact: carrier sleuthkit 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.healthmonitor;
+
+import java.util.logging.Level;
+import org.openide.modules.ModuleInstall;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+public class Installer extends ModuleInstall {
+
+ private static final Logger logger = Logger.getLogger(Installer.class.getName());
+ private static final long serialVersionUID = 1L;
+
+ private static Installer instance;
+
+ public synchronized static Installer getDefault() {
+ if (instance == null) {
+ instance = new Installer();
+ }
+ return instance;
+ }
+
+ private Installer() {
+ super();
+ }
+
+ @Override
+ public void restored() {
+
+ try {
+ EnterpriseHealthMonitor.startUpIfEnabled();
+ } catch (HealthMonitorException ex) {
+ logger.log(Level.SEVERE, "Error starting health services monitor", ex);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetric.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetric.java
new file mode 100644
index 0000000000..d94dfd7161
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetric.java
@@ -0,0 +1,65 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2018 Basis Technology Corp.
+ * Contact: carrier sleuthkit 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.healthmonitor;
+
+/**
+ * Used to calculate and report timing metrics.
+ */
+public class TimingMetric {
+
+ private final String name;
+ private final long startingTimestamp;
+ private Long duration;
+
+ TimingMetric(String name) {
+ this.name = name;
+ this.startingTimestamp = System.nanoTime();
+ this.duration = null;
+ }
+
+ /**
+ * Record how long the metric was running.
+ */
+ void stopTiming() {
+ long endingTimestamp = System.nanoTime();
+ this.duration = endingTimestamp - startingTimestamp;
+ }
+
+ /**
+ * Get the name of metric
+ * @return name
+ */
+ String getName() {
+ return name;
+ }
+
+ /**
+ * Get the duration of the metric. Will throw an exception if the
+ * metric has not been stopped.
+ * @return how long the metric was running (nanoseconds)
+ * @throws HealthMonitorException
+ */
+ long getDuration() throws HealthMonitorException {
+ if (duration != null) {
+ return duration;
+ } else {
+ throw new HealthMonitorException("getDuration() called before stopTiming()");
+ }
+ }
+}
diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java
index fd285ae1aa..50246f5255 100644
--- a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java
+++ b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java
@@ -136,7 +136,6 @@ class VolatilityProcessor {
break;
}
String pluginToRun = pluginsToRun.get(i);
- progressMonitor.setProgressText(Bundle.VolatilityProcessor_progressMessage_runningImageInfo(pluginToRun));
runVolatilityPlugin(pluginToRun);
progressMonitor.setProgress(i);
}
@@ -172,6 +171,8 @@ class VolatilityProcessor {
"VolatilityProcessor_exceptionMessage_errorIndexingOutput=Error indexing output for {0} plugin"
})
private void runVolatilityPlugin(String pluginToRun) throws VolatilityProcessorException {
+ progressMonitor.setProgressText("Running module " + pluginToRun);
+
List commandLine = new ArrayList<>();
commandLine.add("\"" + executableFile + "\""); //NON-NLS
File memoryImage = new File(memoryImagePath);
@@ -314,6 +315,8 @@ class VolatilityProcessor {
String filePath = volfile.getParent();
+ logger.log(Level.INFO, "Looking up file " + fileName + " at path " + filePath);
+
try {
List resolvedFiles;
if (filePath == null) {
@@ -333,12 +336,13 @@ class VolatilityProcessor {
}
fileName += ".%"; //NON-NLS
+ logger.log(Level.INFO, "Looking up file (extension wildcard) " + fileName + " at path " + filePath);
+
if (filePath == null) {
resolvedFiles = fileManager.findFiles(fileName); //NON-NLS
} else {
resolvedFiles = fileManager.findFiles(fileName, filePath); //NON-NLS
}
-
}
if (resolvedFiles.isEmpty()) {
@@ -387,6 +391,7 @@ class VolatilityProcessor {
* @param pluginOutputFile File that contains the output to parse.
*/
private void createArtifactsFromPluginOutput(String pluginName, File pluginOutputFile) throws VolatilityProcessorException {
+ progressMonitor.setProgressText("Parsing module " + pluginName);
Set fileSet = null;
switch (pluginName) {
case "dlllist": //NON-NLS
@@ -421,6 +426,7 @@ class VolatilityProcessor {
}
if (fileSet != null && !fileSet.isEmpty()) {
+ progressMonitor.setProgressText("Flagging files from module " + pluginName);
flagFiles(fileSet, pluginName);
}
}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java
index c7f4c07f6a..b9c4541c7b 100644
--- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java
@@ -27,6 +27,8 @@ import org.apache.solr.common.SolrInputDocument;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.ContentUtils;
+import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor;
+import org.sleuthkit.autopsy.healthmonitor.TimingMetric;
import org.sleuthkit.autopsy.ingest.IngestJobContext;
import org.sleuthkit.autopsy.keywordsearch.Chunker.Chunk;
import org.sleuthkit.datamodel.AbstractFile;
@@ -235,7 +237,9 @@ class Ingester {
try {
//TODO: consider timeout thread, or vary socket timeout based on size of indexed content
+ TimingMetric metric = EnterpriseHealthMonitor.getTimingMetric("Solr: Index chunk");
solrServer.addDocument(updateDoc);
+ EnterpriseHealthMonitor.submitTimingMetric(metric);
uncommitedIngests = true;
} catch (KeywordSearchModuleException | NoOpenCoreException ex) {
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java
index 416f2b394f..eea4d2914e 100644
--- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java
@@ -70,6 +70,8 @@ import org.sleuthkit.autopsy.core.UserPreferences;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ModuleSettings;
import org.sleuthkit.autopsy.coreutils.PlatformUtil;
+import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor;
+import org.sleuthkit.autopsy.healthmonitor.TimingMetric;
import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException;
import org.sleuthkit.datamodel.Content;
@@ -773,7 +775,9 @@ public class Server {
IndexingServerProperties properties = getMultiUserServerProperties(theCase.getCaseDirectory());
currentSolrServer = new HttpSolrServer("http://" + properties.getHost() + ":" + properties.getPort() + "/solr"); //NON-NLS
}
+ TimingMetric metric = EnterpriseHealthMonitor.getTimingMetric("Solr: Connectivity check");
connectToSolrServer(currentSolrServer);
+ EnterpriseHealthMonitor.submitTimingMetric(metric);
} catch (SolrServerException | IOException ex) {
throw new KeywordSearchModuleException(NbBundle.getMessage(Server.class, "Server.connect.exception.msg", ex.getLocalizedMessage()), ex);