/*
* Autopsy Forensic Browser
*
* Copyright 2013-16 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.datamodel.grouping;
import com.google.common.eventbus.Subscribe;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static java.util.Objects.nonNull;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import static javafx.concurrent.Worker.State.CANCELLED;
import static javafx.concurrent.Worker.State.FAILED;
import static javafx.concurrent.Worker.State.READY;
import static javafx.concurrent.Worker.State.RUNNING;
import static javafx.concurrent.Worker.State.SCHEDULED;
import static javafx.concurrent.Worker.State.SUCCEEDED;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.swing.SortOrder;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.netbeans.api.progress.ProgressHandle;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.coreutils.LoggedTask;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.datamodel.Category;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Provides an abstraction layer on top of {@link DrawableDB} ( and to some
* extent {@link SleuthkitCase} ) to facilitate creation, retrieval, updating,
* and sorting of {@link DrawableGroup}s.
*/
public class GroupManager {
private static final Logger LOGGER = Logger.getLogger(GroupManager.class.getName());
private DrawableDB db;
private final ImageGalleryController controller;
/**
* map from {@link GroupKey}s to {@link DrawableGroup}s. All groups (even
* not fully analyzed or not visible groups could be in this map
*/
@GuardedBy("this")
private final Map<GroupKey<?>, DrawableGroup> groupMap = new HashMap<>();
/**
* list of all analyzed groups
*/
@ThreadConfined(type = ThreadType.JFX)
private final ObservableList<DrawableGroup> analyzedGroups = FXCollections.observableArrayList();
private final ObservableList<DrawableGroup> unmodifiableAnalyzedGroups = FXCollections.unmodifiableObservableList(analyzedGroups);
/**
* list of unseen groups
*/
@ThreadConfined(type = ThreadType.JFX)
private final ObservableList<DrawableGroup> unSeenGroups = FXCollections.observableArrayList();
private final ObservableList<DrawableGroup> unmodifiableUnSeenGroups = FXCollections.unmodifiableObservableList(unSeenGroups);
private ReGroupTask<?> groupByTask;
/*
* --- current grouping/sorting attributes ---
*/
private volatile GroupSortBy sortBy = GroupSortBy.NONE;
private volatile DrawableAttribute<?> groupBy = DrawableAttribute.PATH;
private volatile SortOrder sortOrder = SortOrder.ASCENDING;
private final ReadOnlyObjectWrapper< Comparator<DrawableGroup>> sortByProp = new ReadOnlyObjectWrapper<>(sortBy);
private final ReadOnlyObjectWrapper< DrawableAttribute<?>> groupByProp = new ReadOnlyObjectWrapper<>(groupBy);
private final ReadOnlyObjectWrapper<SortOrder> sortOrderProp = new ReadOnlyObjectWrapper<>(sortOrder);
private final ReadOnlyDoubleWrapper regroupProgress = new ReadOnlyDoubleWrapper();
public void setDB(DrawableDB db) {
this.db = db;
regroup(groupBy, sortBy, sortOrder, Boolean.TRUE);
}
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ObservableList<DrawableGroup> getAnalyzedGroups() {
return unmodifiableAnalyzedGroups;
}
@ThreadConfined(type = ThreadType.JFX)
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ObservableList<DrawableGroup> getUnSeenGroups() {
return unmodifiableUnSeenGroups;
}
/**
* construct a group manager hooked up to the given db and controller
*
* @param db
* @param controller
*/
public GroupManager(ImageGalleryController controller) {
this.controller = controller;
}
/**
* using the current groupBy set for this manager, find groupkeys for all
* the groups the given file is a part of
*
* @param file
*
* @returna a set of {@link GroupKey}s representing the group(s) the given
* file is a part of
*/
@SuppressWarnings({"rawtypes", "unchecked"})
synchronized public Set<GroupKey<?>> getGroupKeysForFile(DrawableFile file) {
Set<GroupKey<?>> resultSet = new HashSet<>();
for (Comparable<?> val : groupBy.getValue(file)) {
if (groupBy == DrawableAttribute.TAGS) {
if (CategoryManager.isNotCategoryTagName((TagName) val)) {
resultSet.add(new GroupKey(groupBy, val));
}
} else {
resultSet.add(new GroupKey(groupBy, val));
}
}
return resultSet;
}
/**
* using the current groupBy set for this manager, find groupkeys for all
* the groups the given file is a part of
*
* @return a a set of {@link GroupKey}s representing the group(s) the given
* file is a part of
*/
synchronized public Set<GroupKey<?>> getGroupKeysForFileID(Long fileID) {
try {
if (nonNull(db)) {
DrawableFile file = db.getFileFromID(fileID);
return getGroupKeysForFile(file);
} else {
Logger.getLogger(GroupManager.class.getName()).log(Level.WARNING, "Failed to load file with id: {0} from database. There is no database assigned.", fileID); //NON-NLS
}
} catch (TskCoreException ex) {
Logger.getLogger(GroupManager.class.getName()).log(Level.SEVERE, "failed to load file with id: " + fileID + " from database", ex); //NON-NLS
}
return Collections.emptySet();
}
/**
* @param groupKey
*
* @return return the DrawableGroup (if it exists) for the given GroupKey,
* or null if no group exists for that key.
*/
@Nullable
public DrawableGroup getGroupForKey(@Nonnull GroupKey<?> groupKey) {
synchronized (groupMap) {
return groupMap.get(groupKey);
}
}
synchronized public void clear() {
if (groupByTask != null) {
groupByTask.cancel(true);
}
sortBy = GroupSortBy.GROUP_BY_VALUE;
groupBy = DrawableAttribute.PATH;
sortOrder = SortOrder.ASCENDING;
Platform.runLater(() -> {
unSeenGroups.forEach(controller.getCategoryManager()::unregisterListener);
unSeenGroups.clear();
analyzedGroups.forEach(controller.getCategoryManager()::unregisterListener);
analyzedGroups.clear();
});
synchronized (groupMap) {
groupMap.values().forEach(controller.getCategoryManager()::unregisterListener);
groupMap.clear();
}
db = null;
}
public boolean isRegrouping() {
if (groupByTask == null) {
return false;
}
switch (groupByTask.getState()) {
case READY:
case RUNNING:
case SCHEDULED:
return true;
case CANCELLED:
case FAILED:
case SUCCEEDED:
default:
return false;
}
}
/**
* 'mark' the given group as seen. This removes it from the queue of groups
* to review, and is persisted in the drawable db.
*
* @param group the {@link DrawableGroup} to mark as seen
*/
@ThreadConfined(type = ThreadType.JFX)
public void markGroupSeen(DrawableGroup group, boolean seen) {
if (nonNull(db)) {
db.markGroupSeen(group.getGroupKey(), seen);
group.setSeen(seen);
if (seen) {
unSeenGroups.removeAll(group);
} else if (unSeenGroups.contains(group) == false) {
unSeenGroups.add(group);
}
FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy));
}
}
/**
* remove the given file from the group with the given key. If the group
* doesn't exist or doesn't already contain this file, this method is a
* no-op
*
* @param groupKey the value of groupKey
* @param fileID the value of file
*/
public synchronized DrawableGroup removeFromGroup(GroupKey<?> groupKey, final Long fileID) {
//get grouping this file would be in
final DrawableGroup group = getGroupForKey(groupKey);
if (group != null) {
Platform.runLater(() -> {
group.removeFile(fileID);
});
// If we're grouping by category, we don't want to remove empty groups.
if (groupKey.getAttribute() != DrawableAttribute.CATEGORY) {
if (group.getFileIDs().isEmpty()) {
Platform.runLater(() -> {
if (analyzedGroups.contains(group)) {
analyzedGroups.remove(group);
FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy));
}
if (unSeenGroups.contains(group)) {
unSeenGroups.remove(group);
FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy));
}
});
}
} else { //group == null
// It may be that this was the last unanalyzed file in the group, so test
// whether the group is now fully analyzed.
popuplateIfAnalyzed(groupKey, null);
}
}
return group;
}
/**
* find the distinct values for the given column (DrawableAttribute)
*
* These values represent the groups of files.
*
* @param groupBy
*
* @return
*/
@SuppressWarnings({"unchecked"})
public <A extends Comparable<A>> List<A> findValuesForAttribute(DrawableAttribute<A> groupBy) {
List<A> values = Collections.emptyList();
try {
switch (groupBy.attrName) {
//these cases get special treatment
case CATEGORY:
values = (List<A>) Arrays.asList(Category.values());
break;
case TAGS:
values = (List<A>) controller.getTagsManager().getTagNamesInUse().stream()
.filter(CategoryManager::isNotCategoryTagName)
.collect(Collectors.toList());
break;
case ANALYZED:
values = (List<A>) Arrays.asList(false, true);
break;
case HASHSET:
if (nonNull(db)) {
TreeSet<A> names = new TreeSet<>((Collection<? extends A>) db.getHashSetNames());
values = new ArrayList<>(names);
}
break;
case MIME_TYPE:
if (nonNull(db)) {
HashSet<String> types = new HashSet<>();
try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery("select group_concat(obj_id), mime_type from tsk_files group by mime_type "); //NON-NLS
ResultSet resultSet = executeQuery.getResultSet();) {
while (resultSet.next()) {
final String mimeType = resultSet.getString("mime_type"); //NON-NLS
String objIds = resultSet.getString("group_concat(obj_id)"); //NON-NLS
Pattern.compile(",").splitAsStream(objIds)
.map(Long::valueOf)
.filter(db::isInDB)
.findAny().ifPresent(obj_id -> types.add(mimeType));
}
} catch (SQLException | TskCoreException ex) {
Exceptions.printStackTrace(ex);
}
values = new ArrayList<>((Collection<? extends A>) types);
}
break;
default:
//otherwise do straight db query
if (nonNull(db)) {
values = db.findValuesForAttribute(groupBy, sortBy, sortOrder);
}
}
return values;
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "TSK error getting list of type {0}", groupBy.getDisplayName()); //NON-NLS
return Collections.emptyList();
}
}
public Set<Long> getFileIDsInGroup(GroupKey<?> groupKey) throws TskCoreException {
Set<Long> fileIDsToReturn = Collections.emptySet();
switch (groupKey.getAttribute().attrName) {
//these cases get special treatment
case CATEGORY:
fileIDsToReturn = getFileIDsWithCategory((Category) groupKey.getValue());
break;
case TAGS:
fileIDsToReturn = getFileIDsWithTag((TagName) groupKey.getValue());
break;
case MIME_TYPE:
fileIDsToReturn = getFileIDsWithMimeType((String) groupKey.getValue());
break;
// case HASHSET: //comment out this case to use db functionality for hashsets
// return getFileIDsWithHashSetName((String) groupKey.getValue());
default:
//straight db query
if (nonNull(db)) {
fileIDsToReturn = db.getFileIDsInGroup(groupKey);
}
}
return fileIDsToReturn;
}
// @@@ This was kind of slow in the profiler. Maybe we should cache it.
// Unless the list of file IDs is necessary, use countFilesWithCategory() to get the counts.
public Set<Long> getFileIDsWithCategory(Category category) throws TskCoreException {
Set<Long> fileIDsToReturn = Collections.emptySet();
if (nonNull(db)) {
try {
final DrawableTagsManager tagsManager = controller.getTagsManager();
if (category == Category.ZERO) {
List< TagName> tns = Stream.of(Category.ONE, Category.TWO, Category.THREE, Category.FOUR, Category.FIVE)
.map(tagsManager::getTagName)
.collect(Collectors.toList());
Set<Long> files = new HashSet<>();
for (TagName tn : tns) {
if (tn != null) {
List<ContentTag> contentTags = tagsManager.getContentTagsByTagName(tn);
files.addAll(contentTags.stream()
.filter(ct -> ct.getContent() instanceof AbstractFile)
.filter(ct -> db.isInDB(ct.getContent().getId()))
.map(ct -> ct.getContent().getId())
.collect(Collectors.toSet()));
}
}
fileIDsToReturn = db.findAllFileIdsWhere("obj_id NOT IN (" + StringUtils.join(files, ',') + ")"); //NON-NLS
} else {
List<ContentTag> contentTags = tagsManager.getContentTagsByTagName(tagsManager.getTagName(category));
fileIDsToReturn = contentTags.stream()
.filter(ct -> ct.getContent() instanceof AbstractFile)
.filter(ct -> db.isInDB(ct.getContent().getId()))
.map(ct -> ct.getContent().getId())
.collect(Collectors.toSet());
}
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "TSK error getting files in Category:" + category.getDisplayName(), ex); //NON-NLS
throw ex;
}
}
return fileIDsToReturn;
}
public Set<Long> getFileIDsWithTag(TagName tagName) throws TskCoreException {
try {
Set<Long> files = new HashSet<>();
List<ContentTag> contentTags = controller.getTagsManager().getContentTagsByTagName(tagName);
for (ContentTag ct : contentTags) {
if (ct.getContent() instanceof AbstractFile && nonNull(db) && db.isInDB(ct.getContent().getId())) {
files.add(ct.getContent().getId());
}
}
return files;
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "TSK error getting files with Tag:" + tagName.getDisplayName(), ex); //NON-NLS
throw ex;
}
}
public Comparator<DrawableGroup> getSortBy() {
return sortBy;
}
void setSortBy(GroupSortBy sortBy) {
this.sortBy = sortBy;
Platform.runLater(() -> sortByProp.set(sortBy));
}
public ReadOnlyObjectProperty< Comparator<DrawableGroup>> getSortByProperty() {
return sortByProp.getReadOnlyProperty();
}
public DrawableAttribute<?> getGroupBy() {
return groupBy;
}
void setGroupBy(DrawableAttribute<?> groupBy) {
this.groupBy = groupBy;
Platform.runLater(() -> groupByProp.set(groupBy));
}
public ReadOnlyObjectProperty<DrawableAttribute<?>> getGroupByProperty() {
return groupByProp.getReadOnlyProperty();
}
public SortOrder getSortOrder() {
return sortOrder;
}
void setSortOrder(SortOrder sortOrder) {
this.sortOrder = sortOrder;
Platform.runLater(() -> sortOrderProp.set(sortOrder));
}
public ReadOnlyObjectProperty<SortOrder> getSortOrderProperty() {
return sortOrderProp.getReadOnlyProperty();
}
/**
* regroup all files in the database using given {@link DrawableAttribute}
* see {@link ReGroupTask} for more details.
*
* @param groupBy
* @param sortBy
* @param sortOrder
* @param force true to force a full db query regroup
*/
public synchronized <A extends Comparable<A>> void regroup(final DrawableAttribute<A> groupBy, final GroupSortBy sortBy, final SortOrder sortOrder, Boolean force) {
if (!Case.isCaseOpen()) {
return;
}
//only re-query the db if the group by attribute changed or it is forced
if (groupBy != getGroupBy() || force == true) {
setGroupBy(groupBy);
setSortBy(sortBy);
setSortOrder(sortOrder);
if (groupByTask != null) {
groupByTask.cancel(true);
}
groupByTask = new ReGroupTask<>(groupBy, sortBy, sortOrder);
Platform.runLater(() -> {
regroupProgress.bind(groupByTask.progressProperty());
});
regroupExecutor.submit(groupByTask);
} else {
// resort the list of groups
setSortBy(sortBy);
setSortOrder(sortOrder);
Platform.runLater(() -> {
FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy));
FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy));
});
}
}
/**
* an executor to submit async ui related background tasks to.
*/
final ExecutorService regroupExecutor = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder().namingPattern("ui task -%d").build()); //NON-NLS
public ReadOnlyDoubleProperty regroupProgress() {
return regroupProgress.getReadOnlyProperty();
}
@Subscribe
public void handleTagAdded(ContentTagAddedEvent evt) {
GroupKey<?> newGroupKey = null;
final long fileID = evt.getAddedTag().getContent().getId();
if (groupBy == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(evt.getAddedTag().getName())) {
newGroupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(evt.getAddedTag().getName()));
for (GroupKey<?> oldGroupKey : groupMap.keySet()) {
if (oldGroupKey.equals(newGroupKey) == false) {
removeFromGroup(oldGroupKey, fileID);
}
}
} else if (groupBy == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(evt.getAddedTag().getName())) {
newGroupKey = new GroupKey<>(DrawableAttribute.TAGS, evt.getAddedTag().getName());
}
if (newGroupKey != null) {
DrawableGroup g = getGroupForKey(newGroupKey);
addFileToGroup(g, newGroupKey, fileID);
}
}
@SuppressWarnings("AssignmentToMethodParameter")
private void addFileToGroup(DrawableGroup g, final GroupKey<?> groupKey, final long fileID) {
if (g == null) {
//if there wasn't already a group check if there should be one now
g = popuplateIfAnalyzed(groupKey, null);
}
DrawableGroup group = g;
if (group != null) {
//if there is aleady a group that was previously deemed fully analyzed, then add this newly analyzed file to it.
Platform.runLater(() -> {
group.addFile(fileID);
});
}
}
@Subscribe
public void handleTagDeleted(ContentTagDeletedEvent evt) {
GroupKey<?> groupKey = null;
final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo();
final TagName tagName = deletedTagInfo.getName();
if (groupBy == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(tagName)) {
groupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(tagName));
} else if (groupBy == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(tagName)) {
groupKey = new GroupKey<>(DrawableAttribute.TAGS, tagName);
}
if (groupKey != null) {
final long fileID = deletedTagInfo.getContentID();
DrawableGroup g = removeFromGroup(groupKey, fileID);
}
}
@Subscribe
synchronized public void handleFileRemoved(Collection<Long> removedFileIDs) {
for (final long fileId : removedFileIDs) {
//get grouping(s) this file would be in
Set<GroupKey<?>> groupsForFile = getGroupKeysForFileID(fileId);
for (GroupKey<?> gk : groupsForFile) {
removeFromGroup(gk, fileId);
}
}
}
/**
* handle {@link FileUpdateEvent} sent from Db when files are
* inserted/updated
*
* @param evt
*/
@Subscribe
synchronized public void handleFileUpdate(Collection<Long> updatedFileIDs) {
/**
* TODO: is there a way to optimize this to avoid quering to db so much.
* the problem is that as a new files are analyzed they might be in new
* groups( if we are grouping by say make or model) -jm
*/
for (long fileId : updatedFileIDs) {
controller.getHashSetManager().invalidateHashSetsForFile(fileId);
//get grouping(s) this file would be in
Set<GroupKey<?>> groupsForFile = getGroupKeysForFileID(fileId);
for (GroupKey<?> gk : groupsForFile) {
DrawableGroup g = getGroupForKey(gk);
addFileToGroup(g, gk, fileId);
}
}
//we fire this event for all files so that the category counts get updated during initial db population
controller.getCategoryManager().fireChange(updatedFileIDs, null);
}
private DrawableGroup popuplateIfAnalyzed(GroupKey<?> groupKey, ReGroupTask<?> task) {
if (Objects.nonNull(task) && (task.isCancelled())) {
/*
* if this method call is part of a ReGroupTask and that task is
* cancelled, no-op
*
* this allows us to stop if a regroup task has been cancelled (e.g.
* the user picked a different group by attribute, while the current
* task was still running)
*/
} else // no task or un-cancelled task
{
if (nonNull(db) && ((groupKey.getAttribute() != DrawableAttribute.PATH) || db.isGroupAnalyzed(groupKey))) {
/*
* for attributes other than path we can't be sure a group is
* fully analyzed because we don't know all the files that will
* be a part of that group,. just show them no matter what.
*/
try {
Set<Long> fileIDs = getFileIDsInGroup(groupKey);
if (Objects.nonNull(fileIDs)) {
DrawableGroup group;
final boolean groupSeen = db.isGroupSeen(groupKey);
synchronized (groupMap) {
if (groupMap.containsKey(groupKey)) {
group = groupMap.get(groupKey);
group.setFiles(ObjectUtils.defaultIfNull(fileIDs, Collections.emptySet()));
} else {
group = new DrawableGroup(groupKey, fileIDs, groupSeen);
controller.getCategoryManager().registerListener(group);
group.seenProperty().addListener((o, oldSeen, newSeen) -> {
Platform.runLater(() -> markGroupSeen(group, newSeen));
});
groupMap.put(groupKey, group);
}
}
Platform.runLater(() -> {
if (analyzedGroups.contains(group) == false) {
analyzedGroups.add(group);
if (Objects.isNull(task)) {
FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy));
}
}
markGroupSeen(group, groupSeen);
});
return group;
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "failed to get files for group: " + groupKey.getAttribute().attrName.toString() + " = " + groupKey.getValue(), ex); //NON-NLS
}
}
}
return null;
}
public Set<Long> getFileIDsWithMimeType(String mimeType) throws TskCoreException {
HashSet<Long> hashSet = new HashSet<>();
String query = (null == mimeType)
? "SELECT obj_id FROM tsk_files WHERE mime_type IS NULL" //NON-NLS
: "SELECT obj_id FROM tsk_files WHERE mime_type = '" + mimeType + "'"; //NON-NLS
try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery(query);
ResultSet resultSet = executeQuery.getResultSet();) {
while (resultSet.next()) {
final long fileID = resultSet.getLong("obj_id"); //NON-NLS
if (nonNull(db) && db.isInDB(fileID)) {
hashSet.add(fileID);
}
}
return hashSet;
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
throw new TskCoreException("Failed to get file ids with mime type " + mimeType, ex);
}
}
/**
* Task to query database for files in sorted groups and build
* {@link Groupings} for them
*/
@SuppressWarnings({"unchecked", "rawtypes"})
@NbBundle.Messages({"# {0} - groupBy attribute Name",
"# {1} - sortBy name",
"# {2} - sort Order",
"ReGroupTask.displayTitle=regrouping files by {0} sorted by {1} in {2} order",
"# {0} - groupBy attribute Name",
"# {1} - atribute value",
"ReGroupTask.progressUpdate=regrouping files by {0} : {1}"})
private class ReGroupTask<A extends Comparable<A>> extends LoggedTask<Void> {
private ProgressHandle groupProgress;
private final DrawableAttribute<A> groupBy;
private final GroupSortBy sortBy;
private final SortOrder sortOrder;
ReGroupTask(DrawableAttribute<A> groupBy, GroupSortBy sortBy, SortOrder sortOrder) {
super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), true);
this.groupBy = groupBy;
this.sortBy = sortBy;
this.sortOrder = sortOrder;
}
@Override
public boolean isCancelled() {
return super.isCancelled() || groupBy != getGroupBy() || sortBy != getSortBy() || sortOrder != getSortOrder();
}
@Override
protected Void call() throws Exception {
if (isCancelled()) {
return null;
}
groupProgress = ProgressHandle.createHandle(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), this);
Platform.runLater(() -> {
analyzedGroups.clear();
unSeenGroups.clear();
});
// Get the list of group keys
final List<A> vals = findValuesForAttribute(groupBy);
groupProgress.start(vals.size());
int p = 0;
// For each key value, partially create the group and add it to the list.
for (final A val : vals) {
if (isCancelled()) {
return null;//abort
}
p++;
updateMessage(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val));
updateProgress(p, vals.size());
groupProgress.progress(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val), p);
popuplateIfAnalyzed(new GroupKey<>(groupBy, val), this);
}
Platform.runLater(() -> FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy)));
updateProgress(1, 1);
return null;
}
@Override
protected void done() {
super.done();
if (groupProgress != null) {
groupProgress.finish();
groupProgress = null;
}
}
}
private static <T> Comparator<T> applySortOrder(final SortOrder sortOrder, Comparator<T> comparator) {
switch (sortOrder) {
case ASCENDING:
return comparator;
case DESCENDING:
return comparator.reversed();
case UNSORTED:
default:
return new GroupSortBy.AllEqualComparator<>();
}
}
}