APriestman 24bc7c14f7 Fixed bug where the incorrect category name was being displayed when grouping by category.
For speed, now stores hash set hits in memory.
Fixed bug where hash hit count would never update after a group was created, even if files were added/removed
Changed category zero count to be bases on other counts and not stored separately.
2015-05-22 09:35:50 -04:00

408 lines
16 KiB
Java

/*
* Autopsy Forensic Browser
*
* Copyright 2013-14 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.imagegallery.gui;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javax.swing.Action;
import javax.swing.SwingUtilities;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.actions.Presenter;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.corecomponentinterfaces.ContextMenuActionsProvider;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType;
import org.sleuthkit.autopsy.datamodel.FileNode;
import org.sleuthkit.autopsy.directorytree.DirectoryTreeTopComponent;
import org.sleuthkit.autopsy.directorytree.ExternalViewerAction;
import org.sleuthkit.autopsy.directorytree.ExtractAction;
import org.sleuthkit.autopsy.directorytree.NewWindowViewAction;
import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel;
import org.sleuthkit.autopsy.imagegallery.FileUpdateEvent;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent;
import org.sleuthkit.autopsy.imagegallery.TagUtils;
import org.sleuthkit.autopsy.imagegallery.actions.AddDrawableTagAction;
import org.sleuthkit.autopsy.imagegallery.actions.CategorizeAction;
import org.sleuthkit.autopsy.imagegallery.actions.SwingMenuItemAdapter;
import org.sleuthkit.autopsy.imagegallery.datamodel.Category;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.grouping.GroupKey;
import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskCoreException;
/**
* An abstract base class for {@link DrawableTile} and {@link SlideShowView},
* since they share a similar node tree and many behaviors, other implementers
* of {@link DrawableView}s should implement the interface directly
*
*/
public abstract class SingleDrawableViewBase extends AnchorPane implements DrawableView {
private static final Logger LOGGER = Logger.getLogger(SingleDrawableViewBase.class.getName());
private static final Border UNSELECTED_ORDER = new Border(new BorderStroke(Color.GRAY, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3)));
private static final Border SELECTED_BORDER = new Border(new BorderStroke(Color.BLUE, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3)));
//TODO: should this stuff be done in CSS? -jm
protected static final Image videoIcon = new Image("org/sleuthkit/autopsy/imagegallery/images/video-file.png");
protected static final Image hashHitIcon = new Image("org/sleuthkit/autopsy/imagegallery/images/hashset_hits.png");
protected static final Image followUpIcon = new Image("org/sleuthkit/autopsy/imagegallery/images/flag_red.png");
protected static final Image followUpGray = new Image("org/sleuthkit/autopsy/imagegallery/images/flag_gray.png");
protected static final FileIDSelectionModel globalSelectionModel = FileIDSelectionModel.getInstance();
/**
* displays the icon representing video files
*/
@FXML
protected ImageView fileTypeImageView;
/**
* displays the icon representing hash hits
*/
@FXML
protected ImageView hashHitImageView;
/**
* displays the icon representing follow up tag
*/
@FXML
protected ImageView followUpImageView;
@FXML
protected ToggleButton followUpToggle;
/**
* the label that shows the name of the represented file
*/
@FXML
protected Label nameLabel;
@FXML
protected BorderPane imageBorder;
static private ContextMenu contextMenu;
protected DrawableFile<?> file;
protected Long fileID;
/**
* the groupPane this {@link SingleDrawableViewBase} is embedded in
*/
protected GroupPane groupPane;
protected SingleDrawableViewBase() {
globalSelectionModel.getSelected().addListener((Observable observable) -> {
updateSelectionState();
});
//set up mouse listener
//TODO: split this between DrawableTile and SingleDrawableViewBase
addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent t) {
switch (t.getButton()) {
case PRIMARY:
if (t.getClickCount() == 1) {
if (t.isControlDown()) {
globalSelectionModel.toggleSelection(fileID);
} else {
groupPane.makeSelection(t.isShiftDown(), fileID);
}
} else if (t.getClickCount() > 1) {
groupPane.activateSlideShowViewer(fileID);
}
break;
case SECONDARY:
if (t.getClickCount() == 1) {
if (globalSelectionModel.isSelected(fileID) == false) {
groupPane.makeSelection(false, fileID);
}
}
if (contextMenu != null) {
contextMenu.hide();
}
final ContextMenu groupContextMenu = groupPane.getContextMenu();
if (groupContextMenu != null) {
groupContextMenu.hide();
}
contextMenu = buildContextMenu();
contextMenu.show(SingleDrawableViewBase.this, t.getScreenX(), t.getScreenY());
break;
}
t.consume();
}
private ContextMenu buildContextMenu() {
final ArrayList<MenuItem> menuItems = new ArrayList<>();
menuItems.add(CategorizeAction.getPopupMenu());
menuItems.add(AddDrawableTagAction.getInstance().getPopupMenu());
final MenuItem extractMenuItem = new MenuItem("Extract File(s)");
extractMenuItem.setOnAction((ActionEvent t) -> {
SwingUtilities.invokeLater(() -> {
TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID);
ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null));
});
});
menuItems.add(extractMenuItem);
MenuItem contentViewer = new MenuItem("Show Content Viewer");
contentViewer.setOnAction((ActionEvent t) -> {
SwingUtilities.invokeLater(() -> {
new NewWindowViewAction("Show Content Viewer", new FileNode(getFile().getAbstractFile())).actionPerformed(null);
});
});
menuItems.add(contentViewer);
MenuItem externalViewer = new MenuItem("Open in External Viewer");
final ExternalViewerAction externalViewerAction = new ExternalViewerAction("Open in External Viewer", new FileNode(getFile().getAbstractFile()));
externalViewer.setDisable(externalViewerAction.isEnabled() == false);
externalViewer.setOnAction((ActionEvent t) -> {
SwingUtilities.invokeLater(() -> {
externalViewerAction.actionPerformed(null);
});
});
menuItems.add(externalViewer);
Collection<? extends ContextMenuActionsProvider> menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class);
for (ContextMenuActionsProvider provider : menuProviders) {
for (final Action act : provider.getActions()) {
if (act instanceof Presenter.Popup) {
Presenter.Popup aact = (Presenter.Popup) act;
menuItems.add(SwingMenuItemAdapter.create(aact.getPopupPresenter()));
}
}
}
ContextMenu contextMenu = new ContextMenu(menuItems.toArray(new MenuItem[]{}));
contextMenu.setAutoHide(true);
return contextMenu;
}
});
}
@ThreadConfined(type = ThreadType.UI)
protected abstract void clearContent();
protected abstract void disposeContent();
protected abstract Runnable getContentUpdateRunnable();
protected abstract String getLabelText();
protected void initialize() {
followUpToggle.setOnAction((ActionEvent t) -> {
if (followUpToggle.isSelected() == true) {
globalSelectionModel.clearAndSelect(fileID);
try {
AddDrawableTagAction.getInstance().addTag(TagUtils.getFollowUpTagName(), "");
} catch (TskCoreException ex) {
Exceptions.printStackTrace(ex);
}
} else {
//TODO: convert this to an action!
final ImageGalleryController controller = ImageGalleryController.getDefault();
try {
// remove file from old category group
controller.getGroupManager().removeFromGroup(new GroupKey<TagName>(DrawableAttribute.TAGS, TagUtils.getFollowUpTagName()), fileID);
List<ContentTag> contentTagsByContent = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByContent(getFile());
for (ContentTag ct : contentTagsByContent) {
if (ct.getName().getDisplayName().equals(TagUtils.getFollowUpTagName().getDisplayName())) {
Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(ct);
SwingUtilities.invokeLater(() -> DirectoryTreeTopComponent.findInstance().refreshContentTreeSafe());
}
}
controller.getGroupManager().handleFileUpdate(FileUpdateEvent.newUpdateEvent(Collections.singleton(fileID), DrawableAttribute.TAGS));
} catch (TskCoreException ex) {
Exceptions.printStackTrace(ex);
}
}
});
}
@Override
public DrawableFile<?> getFile() {
if (fileID != null) {
if (file == null || file.getId() != fileID) {
try {
file = ImageGalleryController.getDefault().getFileFromId(fileID);
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "failed to get DrawableFile for obj_id" + fileID, ex);
return null;
}
}
} else {
return null;
}
return file;
}
protected boolean hasFollowUp() throws TskCoreException {
String followUpTagName = TagUtils.getFollowUpTagName().getDisplayName();
Collection<TagName> tagNames = DrawableAttribute.TAGS.getValue(getFile());
return tagNames.stream().anyMatch((tn) -> tn.getDisplayName().equals(followUpTagName));
}
protected void updateUI(final boolean isVideo, final boolean hasHashSetHits, final String text) {
if (isVideo) {
fileTypeImageView.setImage(videoIcon);
} else {
fileTypeImageView.setImage(null);
}
if (hasHashSetHits) {
hashHitImageView.setImage(hashHitIcon);
} else {
hashHitImageView.setImage(null);
}
nameLabel.setText(text);
nameLabel.setTooltip(new Tooltip(text));
}
@Override
public Long getFileID() {
return fileID;
}
@Override
public void handleTagsChanged(Collection<Long> ids) {
if (fileID != null && ids.contains(fileID)) {
updateFollowUpIcon();
}
}
protected void updateFollowUpIcon() {
if (file != null) {
try {
boolean hasFollowUp = hasFollowUp();
Platform.runLater(() -> {
followUpImageView.setImage(hasFollowUp ? followUpIcon : followUpGray);
followUpToggle.setSelected(hasFollowUp);
});
} catch (TskCoreException ex) {
Exceptions.printStackTrace(ex);
}
}
}
@Override
public void setFile(final Long fileID) {
if (Objects.equals(fileID, this.fileID) == false) {
this.fileID = fileID;
disposeContent();
if (this.fileID == null || Case.isCaseOpen() == false) {
Category.unregisterListener(this);
TagUtils.unregisterListener(this);
file = null;
Platform.runLater(() -> {
clearContent();
});
} else {
Category.registerListener(this);
TagUtils.registerListener(this);
getFile();
updateSelectionState();
updateCategoryBorder();
updateFollowUpIcon();
final String text = getLabelText();
final boolean isVideo = file.isVideo();
final boolean hasHashSetHits = hasHashHit();
Platform.runLater(() -> {
updateUI(isVideo, hasHashSetHits, text);
});
Platform.runLater(getContentUpdateRunnable());
}
}
}
private void updateSelectionState() {
final boolean selected = globalSelectionModel.isSelected(fileID);
Platform.runLater(() -> {
SingleDrawableViewBase.this.setBorder(selected ? SELECTED_BORDER : UNSELECTED_ORDER);
});
}
@Override
public Region getBorderable() {
return imageBorder;
}
@Override
public void handleCategoryChanged(Collection<Long> ids) {
if (ids.contains(fileID)) {
updateCategoryBorder();
}
}
}