From 626f97ff066511df2666c27e09019787e481b85c Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 27 Jan 2016 15:44:48 -0500 Subject: [PATCH] cleanup FileTypeUtils and ImageGalleryController, and DrawableFile, etc... --- .../autopsy/imagegallery/FileTypeUtils.java | 32 ++-- .../imagegallery/ImageGalleryController.java | 13 +- .../actions/CategorizeGroupAction.java | 2 +- .../imagegallery/actions/TagGroupAction.java | 2 +- .../datamodel/DrawableAttribute.java | 107 +++++++++---- .../imagegallery/datamodel/DrawableDB.java | 4 +- .../imagegallery/datamodel/DrawableFile.java | 12 +- .../datamodel/grouping/DrawableGroup.java | 146 ++++++++++-------- .../datamodel/grouping/GroupKey.java | 4 + .../datamodel/grouping/GroupManager.java | 16 +- .../datamodel/grouping/GroupSortBy.java | 8 +- .../gui/drawableviews/GroupPane.java | 32 ++-- .../gui/drawableviews/SlideShowView.java | 12 +- .../gui/navpanel/GroupListCell.java | 54 +++---- .../imagegallery/gui/navpanel/GroupTree.java | 10 +- .../gui/navpanel/GroupTreeCell.java | 6 +- .../imagegallery/images/mime_types.png | Bin 0 -> 1453 bytes 17 files changed, 251 insertions(+), 209 deletions(-) create mode 100644 ImageGallery/src/org/sleuthkit/autopsy/imagegallery/images/mime_types.png diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/FileTypeUtils.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/FileTypeUtils.java index bddfe6f1e7..4498021484 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/FileTypeUtils.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/FileTypeUtils.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015 Basis Technology Corp. + * Copyright 2015-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,7 +38,7 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Enum style singleton to provide utilities related to questions about a files - * type, and wheather it should be supported in Image Gallery. + * type, and whether it should be supported in Image Gallery. * * TODO: refactor this to remove code that duplicates * org.sleuthkit.autopsy.coreutils.ImageUtils @@ -74,6 +74,7 @@ public enum FileTypeUtils { * videoExtensions sets. */ private static final Set supportedExtensions; + /** * Lazily instantiated FileTypeDetector to use when the mimetype of a file * is needed @@ -119,6 +120,14 @@ public enum FileTypeUtils { videoMimeTypes.addAll(Arrays.asList("application/x-shockwave-flash")); supportedMimeTypes.addAll(videoMimeTypes); + + /* + * TODO: windows .cur cursor files get misidentified as + * application/x-123, so we claim to support application/x-123 so we + * don't miss them: ie this is a hack to cover another bug. when this is + * fixed, we should remove application/x-123 from the list of supported + * mime types. + */ supportedMimeTypes.addAll(Arrays.asList("application/x-123")); //add list of mimetypes ImageIO claims to support @@ -126,21 +135,13 @@ public enum FileTypeUtils { .map(String::toLowerCase) .collect(Collectors.toList())); - supportedMimeTypes.removeIf("application/octet-stream"::equals); //this is rearely usefull + supportedMimeTypes.removeIf("application/octet-stream"::equals); //this is rarely usefull } - /** - * - * @return - */ public static Set getAllSupportedMimeTypes() { return Collections.unmodifiableSet(supportedMimeTypes); } - /** - * - * @return - */ static Set getAllSupportedExtensions() { return Collections.unmodifiableSet(supportedExtensions); } @@ -167,12 +168,9 @@ public enum FileTypeUtils { */ public static boolean isDrawable(AbstractFile file) throws TskCoreException { return hasDrawableMimeType(file).orElseGet(() -> { - final boolean contains = FileTypeUtils.supportedExtensions.contains(file.getNameExtension()); - final boolean jpegFileHeader = ImageUtils.isJpegFileHeader(file); - final boolean pngFileHeader = ImageUtils.isPngFileHeader(file); - return contains - || jpegFileHeader - || pngFileHeader; + return FileTypeUtils.supportedExtensions.contains(file.getNameExtension().toLowerCase()) + || ImageUtils.isJpegFileHeader(file) + || ImageUtils.isPngFileHeader(file); }); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index 0b36024f2f..b749c9cdde 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -64,6 +64,7 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.coreutils.History; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.imagegallery.actions.UndoRedoManager; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; @@ -695,9 +696,8 @@ public final class ImageGalleryController implements Executor { * Task that runs when image gallery listening is (re) enabled. * * Grabs all files with supported image/video mime types or extensions, and - * adds them to the Drawable DB. Uses the presence of TSK_FILE_TYPE_SIG - * attributes as a approximation to 'analyzed'. - * + * adds them to the Drawable DB. Uses the presence of a mimetype as an + * approximation to 'analyzed'. */ static private class CopyAnalyzedFiles extends InnerTask { @@ -747,7 +747,7 @@ public final class ImageGalleryController implements Executor { int units = 0; for (final AbstractFile f : files) { if (isCancelled()) { - LOGGER.log(Level.WARNING, "task cancelled: not all contents may be transfered to database"); + LOGGER.log(Level.WARNING, "Task cancelled: not all contents may be transfered to drawable database."); progressHandle.finish(); break; } @@ -759,7 +759,7 @@ public final class ImageGalleryController implements Executor { } else { final Optional hasMimeType = FileTypeUtils.hasDrawableMimeType(f); if (hasMimeType.isPresent()) { - if (hasMimeType.get()) { // supported mimetype => analyzed + if (hasMimeType.get()) { //supported mimetype => analyzed taskDB.updateFile(DrawableFile.create(f, true, false), tr); } else { //unsupported mimtype => analyzed but shouldn't include taskDB.removeFile(f.getId(), tr); @@ -794,6 +794,7 @@ public final class ImageGalleryController implements Executor { } catch (TskCoreException ex) { progressHandle.progress("Stopping copy to drawable db task."); Logger.getLogger(CopyAnalyzedFiles.class.getName()).log(Level.WARNING, "Stopping copy to drawable db task. Failed to transfer all database contents: " + ex.getMessage()); + MessageNotifyUtil.Notify.warn("There was an error populating Image Gallery database.", ex.getMessage()); progressHandle.finish(); updateMessage(""); updateProgress(-1.0); @@ -810,7 +811,7 @@ public final class ImageGalleryController implements Executor { /** * task that does pre-ingest copy over of files from a new datasource (uses - * fs_obj_id to identify files from new datasource) * + * fs_obj_id to identify files from new datasources) * * TODO: create methods to simplify progress value/text updates to both * netbeans and ImageGallery progress/status diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java index ec5d1c1f0d..e0861b9870 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java @@ -33,7 +33,7 @@ public class CategorizeGroupAction extends Action { public CategorizeGroupAction(Category cat, ImageGalleryController controller) { super(cat.getDisplayName(), (javafx.event.ActionEvent actionEvent) -> { - Set fileIdSet = ImmutableSet.copyOf(controller.viewState().get().getGroup().fileIds()); + Set fileIdSet = ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs()); new CategorizeAction(controller).addTagsToFiles(controller.getTagsManager().getTagName(cat), "", fileIdSet); }); setGraphic(new ImageView(DrawableAttribute.CATEGORY.getIcon())); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java index 08a81f8199..dc7db4074c 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java @@ -33,7 +33,7 @@ public class TagGroupAction extends Action { public TagGroupAction(final TagName tagName, ImageGalleryController controller) { super(tagName.getDisplayName(), (javafx.event.ActionEvent actionEvent) -> { - Set fileIdSet = ImmutableSet.copyOf(controller.viewState().get().getGroup().fileIds()); + Set fileIdSet = ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs()); new AddDrawableTagAction(controller).addTagsToFiles(tagName, "", fileIdSet); }); setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon())); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableAttribute.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableAttribute.java index 7f72173426..eb3a7ed64d 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableAttribute.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableAttribute.java @@ -40,13 +40,22 @@ import org.sleuthkit.datamodel.TagName; public class DrawableAttribute> { public final static DrawableAttribute MD5_HASH = - new DrawableAttribute<>(AttributeName.MD5_HASH, "MD5 Hash", false, "icon-hashtag.png", f -> Collections.singleton(f.getMd5Hash())); + new DrawableAttribute<>(AttributeName.MD5_HASH, "MD5 Hash", + false, + "icon-hashtag.png", + f -> Collections.singleton(f.getMd5Hash())); public final static DrawableAttribute NAME = - new DrawableAttribute<>(AttributeName.NAME, "Name", true, "folder-rename.png", f -> Collections.singleton(f.getName())); + new DrawableAttribute<>(AttributeName.NAME, "Name", + true, + "folder-rename.png", + f -> Collections.singleton(f.getName())); public final static DrawableAttribute ANALYZED = - new DrawableAttribute<>(AttributeName.ANALYZED, "Analyzed", true, "", f -> Collections.singleton(f.isAnalyzed())); + new DrawableAttribute<>(AttributeName.ANALYZED, "Analyzed", + true, + "", + f -> Collections.singleton(f.isAnalyzed())); /** * since categories are really just tags in autopsy, they are not dealt with @@ -57,40 +66,76 @@ public class DrawableAttribute> { * advantage. move categories into DrawableDB? */ public final static DrawableAttribute CATEGORY = - new DrawableAttribute<>(AttributeName.CATEGORY, "Category", false, "category-icon.png", f -> Collections.singleton(f.getCategory())); + new DrawableAttribute<>(AttributeName.CATEGORY, "Category", + false, + "category-icon.png", + f -> Collections.singleton(f.getCategory())); public final static DrawableAttribute TAGS = - new DrawableAttribute<>(AttributeName.TAGS, "Tags", false, "tag_red.png", DrawableFile::getTagNames); + new DrawableAttribute<>(AttributeName.TAGS, "Tags", + false, + "tag_red.png", + DrawableFile::getTagNames); public final static DrawableAttribute PATH = - new DrawableAttribute<>(AttributeName.PATH, "Path", true, "folder_picture.png", f -> Collections.singleton(f.getDrawablePath())); + new DrawableAttribute<>(AttributeName.PATH, "Path", + true, + "folder_picture.png", + f -> Collections.singleton(f.getDrawablePath())); public final static DrawableAttribute CREATED_TIME = - new DrawableAttribute<>(AttributeName.CREATED_TIME, "Created Time", true, "clock--plus.png", f -> Collections.singleton(ContentUtils.getStringTime(f.getCrtime(), f))); + new DrawableAttribute<>(AttributeName.CREATED_TIME, "Created Time", + true, + "clock--plus.png", + f -> Collections.singleton(ContentUtils.getStringTime(f.getCrtime(), f))); public final static DrawableAttribute MODIFIED_TIME = - new DrawableAttribute<>(AttributeName.MODIFIED_TIME, "Modified Time", true, "clock--pencil.png", f -> Collections.singleton(ContentUtils.getStringTime(f.getMtime(), f))); + new DrawableAttribute<>(AttributeName.MODIFIED_TIME, "Modified Time", + true, + "clock--pencil.png", + f -> Collections.singleton(ContentUtils.getStringTime(f.getMtime(), f))); public final static DrawableAttribute MAKE = - new DrawableAttribute<>(AttributeName.MAKE, "Camera Make", true, "camera.png", f -> Collections.singleton(f.getMake())); + new DrawableAttribute<>(AttributeName.MAKE, "Camera Make", + true, + "camera.png", + f -> Collections.singleton(f.getMake())); public final static DrawableAttribute MODEL = - new DrawableAttribute<>(AttributeName.MODEL, "Camera Model", true, "camera.png", f -> Collections.singleton(f.getModel())); + new DrawableAttribute<>(AttributeName.MODEL, "Camera Model", + true, + "camera.png", + f -> Collections.singleton(f.getModel())); public final static DrawableAttribute HASHSET = - new DrawableAttribute<>(AttributeName.HASHSET, "Hashset", true, "hashset_hits.png", DrawableFile::getHashSetNamesUnchecked); + new DrawableAttribute<>(AttributeName.HASHSET, "Hashset", + true, + "hashset_hits.png", + DrawableFile::getHashSetNamesUnchecked); public final static DrawableAttribute OBJ_ID = - new DrawableAttribute<>(AttributeName.OBJ_ID, "Internal Object ID", true, "", f -> Collections.singleton(f.getId())); + new DrawableAttribute<>(AttributeName.OBJ_ID, "Internal Object ID", + true, + "", + f -> Collections.singleton(f.getId())); public final static DrawableAttribute WIDTH = - new DrawableAttribute<>(AttributeName.WIDTH, "Width", false, "arrow-resize.png", f -> Collections.singleton(f.getWidth())); + new DrawableAttribute<>(AttributeName.WIDTH, "Width", + false, + "arrow-resize.png", + f -> Collections.singleton(f.getWidth())); public final static DrawableAttribute HEIGHT = - new DrawableAttribute<>(AttributeName.HEIGHT, "Height", false, "arrow-resize-090.png", f -> Collections.singleton(f.getHeight())); + new DrawableAttribute<>(AttributeName.HEIGHT, "Height", + false, + "arrow-resize-090.png", + f -> Collections.singleton(f.getHeight())); public final static DrawableAttribute MIME_TYPE = - new DrawableAttribute<>(AttributeName.MIME_TYPE, "MIME type", false, " ", f -> Collections.singleton(f.getMIMEType())); + new DrawableAttribute<>(AttributeName.MIME_TYPE, "MIME type", + false, + "mime_types.png", + f -> Collections.singleton(f.getMIMEType())); final private static List< DrawableAttribute> groupables = Arrays.asList(PATH, HASHSET, CATEGORY, TAGS, MAKE, MODEL, MIME_TYPE); @@ -105,9 +150,10 @@ public class DrawableAttribute> { this.attrName = name; this.displayName = new ReadOnlyStringWrapper(displayName); this.isDBColumn = isDBColumn; - this.imageName = imageName; this.extractor = extractor; + this.imageName = imageName; } + private final String imageName; private Image icon; @@ -117,17 +163,26 @@ public class DrawableAttribute> { private final StringProperty displayName; - private final String imageName; - public Image getIcon() { - if (icon == null) { - if (StringUtils.isBlank(imageName) == false) { - this.icon = new Image("org/sleuthkit/autopsy/imagegallery/images/" + imageName, true); - } + /* + * There is some issue with loading this in the constructor which gets + * called at class load time, so instead we load them lazily the first + * time they are needed + */ + if (null == icon && StringUtils.isNotBlank(imageName)) { + this.icon = new Image("org/sleuthkit/autopsy/imagegallery/images/" + imageName, true); } return icon; } + /** + * TODO: override this to load per value icons form some attributes like + * mime-type and category + */ + public Image getIconForValue(T val) { + return getIcon(); + } + public static List> getGroupableAttrs() { return Collections.unmodifiableList(groupables); } @@ -144,6 +199,10 @@ public class DrawableAttribute> { return displayName.get(); } + public Collection getValue(DrawableFile f) { + return extractor.apply(f); + } + public static enum AttributeName { NAME, @@ -162,8 +221,4 @@ public class DrawableAttribute> { MD5_HASH, MIME_TYPE; } - - public Collection getValue(DrawableFile f) { - return extractor.apply(f); - } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index 2e5469a1b8..61ef672bc2 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -41,7 +41,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; -import javax.annotation.Nonnull; import java.util.stream.Collectors; import javax.annotation.concurrent.GuardedBy; import javax.swing.SortOrder; @@ -127,7 +126,7 @@ public final class DrawableDB { */ private final Map, PreparedStatement> groupStatementMap = new HashMap<>(); - private GroupManager groupManager; + private final GroupManager groupManager; private final Path dbPath; @@ -1200,6 +1199,7 @@ public final class DrawableDB { LOGGER.log(Level.SEVERE, "Failed to get content tags by tag name.", ex1); } return -1; + } /** diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java index fe352c71da..14fbf4265c 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java @@ -40,7 +40,6 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagegallery.FileTypeUtils; import org.sleuthkit.autopsy.imagegallery.ThumbnailCache; import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; -import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; @@ -79,8 +78,7 @@ public abstract class DrawableFile extends AbstractFile return create(Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(id), analyzed); } - SoftReference imageRef; -// SoftReference thumbref; + private SoftReference imageRef; private String drawablePath; @@ -327,14 +325,6 @@ public abstract class DrawableFile extends AbstractFile abstract Double getHeight(); - public String getMIMEType() { - try { - return new FileTypeDetector().getFileType(file); - } catch (FileTypeDetector.FileTypeDetectorInitException | TskCoreException ex) { - return null; - } - } - public String getDrawablePath() { if (drawablePath != null) { return drawablePath; diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java index 40467d9f3c..f3020830a9 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java @@ -23,13 +23,14 @@ import com.google.common.eventbus.Subscribe; import java.util.Objects; import java.util.Set; import java.util.logging.Level; -import javafx.beans.Observable; import javafx.beans.binding.Bindings; +import javafx.beans.binding.DoubleBinding; import javafx.beans.binding.IntegerBinding; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyLongWrapper; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; @@ -49,25 +50,37 @@ public class DrawableGroup implements Comparable { return "unknown"; } + private final GroupKey groupKey; private final ObservableList fileIDs = FXCollections.observableArrayList(); private final ObservableList unmodifiableFileIDS = FXCollections.unmodifiableObservableList(fileIDs); //cache the number of files in this groups with hashset hits private final ReadOnlyLongWrapper hashSetHitsCount = new ReadOnlyLongWrapper(-1); - - public ReadOnlyLongProperty hashSetHitsCountProperty() { - return hashSetHitsCount.getReadOnlyProperty(); - } - private final ReadOnlyBooleanWrapper seen = new ReadOnlyBooleanWrapper(false); + //cache the number ofuncategorized files in this group private final ReadOnlyLongWrapper uncatCount = new ReadOnlyLongWrapper(-1); + //cache the hash hit density for this group + private final DoubleBinding hashDensity = hashSetHitsCount.multiply(100d).divide(Bindings.size(fileIDs)); + //cache if this group has been seen + private final ReadOnlyBooleanWrapper seen = new ReadOnlyBooleanWrapper(false); + + DrawableGroup(GroupKey groupKey, Set filesInGroup, boolean seen) { + this.groupKey = groupKey; + this.fileIDs.setAll(filesInGroup); + fileIDs.addListener((ListChangeListener.Change listchange) -> { + boolean seenChanged = false; + while (false == seenChanged && listchange.next()) { + seenChanged |= listchange.wasAdded(); + } + invalidateProperties(seenChanged); + }); + this.seen.set(seen); + } @SuppressWarnings("ReturnOfCollectionOrArrayField") - synchronized public ObservableList fileIds() { + public synchronized ObservableList getFileIDs() { return unmodifiableFileIDS; } - final public GroupKey groupKey; - public GroupKey getGroupKey() { return groupKey; } @@ -84,18 +97,7 @@ public class DrawableGroup implements Comparable { return groupKey.getValueDisplayName(); } - DrawableGroup(GroupKey groupKey, Set filesInGroup, boolean seen) { - this.groupKey = groupKey; - this.fileIDs.setAll(filesInGroup); - fileIDs.addListener((Observable observable) -> { - hashSetHitsCount.set(-1); - DrawableGroup.this.seen.set(false); - }); - this.seen.set(seen); - getUncategorizedCount(); - } - - synchronized public int getSize() { + public synchronized int getSize() { return fileIDs.size(); } @@ -104,17 +106,19 @@ public class DrawableGroup implements Comparable { } public double getHashHitDensity() { - return hashSetHitsCountProperty().divide((double) getSize()).get(); + getHashSetHitsCount(); //initialize hashSetHitsCount + return hashDensity.get(); } - synchronized private void invalidateUncatCount() { - uncatCount.set(-1); + public DoubleBinding hashHitDensityProperty() { + getHashSetHitsCount(); //initialize hashSetHitsCount + return hashDensity; } /** * @return the number of files in this group that have hash set hits */ - synchronized public long getHashSetHitsCount() { + public synchronized long getHashSetHitsCount() { if (hashSetHitsCount.get() < 0) { try { hashSetHitsCount.set(fileIDs.stream() @@ -127,9 +131,15 @@ public class DrawableGroup implements Comparable { } return hashSetHitsCount.get(); + } - final synchronized public long getUncategorizedCount() { + public ReadOnlyLongProperty hashSetHitsCountProperty() { + getHashSetHitsCount(); //initialize hashSetHitsCount + return hashSetHitsCount.getReadOnlyProperty(); + } + + public final synchronized long getUncategorizedCount() { if (uncatCount.get() < 0) { try { uncatCount.set(ImageGalleryController.getDefault().getDatabase().getCategoryCount(Category.ZERO, fileIDs)); @@ -143,7 +153,51 @@ public class DrawableGroup implements Comparable { } public ReadOnlyLongProperty uncatCountProperty() { + getUncategorizedCount(); //initialize uncatCount return uncatCount.getReadOnlyProperty(); + + } + + void setSeen(boolean isSeen) { + this.seen.set(isSeen); + } + + public boolean isSeen() { + return seen.get(); + } + + public ReadOnlyBooleanWrapper seenProperty() { + return seen; + } + + @Subscribe + public synchronized void handleCatChange(CategoryManager.CategoryChangeEvent event) { + if (Iterables.any(event.getFileIDs(), fileIDs::contains)) { + uncatCount.set(-1); + } + } + + synchronized void addFile(Long f) { + if (fileIDs.contains(f) == false) { + fileIDs.add(f); + } + } + + synchronized void setFiles(Set newFileIds) { + fileIDs.removeIf(fileID -> newFileIds.contains(fileID) == false); + newFileIds.stream().forEach(this::addFile); + } + + synchronized void removeFile(Long f) { + fileIDs.removeAll(f); + } + + private void invalidateProperties(boolean seenChanged) { + if (seenChanged) { + seen.set(false); + } + uncatCount.set(-1); + hashSetHitsCount.set(-1); } @Override @@ -170,50 +224,10 @@ public class DrawableGroup implements Comparable { ((DrawableGroup) obj).groupKey); } - synchronized void addFile(Long f) { - invalidateUncatCount(); - if (fileIDs.contains(f) == false) { - fileIDs.add(f); - } - } - - synchronized void setFiles(Set newFileIds) { - fileIDs.removeIf((Long t) -> newFileIds.contains(t) == false); - - for (Long f : newFileIds) { - if (fileIDs.contains(f) == false) { - fileIDs.add(f); - seen.set(false); - } - } - } - - synchronized void removeFile(Long f) { - fileIDs.removeAll(f); - invalidateUncatCount(); - } - // By default, sort by group key name @Override public int compareTo(DrawableGroup other) { return this.groupKey.getValueDisplayName().compareTo(other.groupKey.getValueDisplayName()); } - void setSeen(boolean isSeen) { - this.seen.set(isSeen); - } - - public ReadOnlyBooleanWrapper seenProperty() { - return seen; - } - - public boolean isSeen() { - return seen.get(); - } - @Subscribe - synchronized public void handleCatChange(CategoryManager.CategoryChangeEvent event) { - if (Iterables.any(event.getFileIDs(), fileIDs::contains)) { - invalidateUncatCount(); - } - } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java index d50cc81998..79cbb1fba2 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.imagegallery.datamodel.grouping; import java.util.Map; import java.util.Objects; +import javafx.scene.image.Image; import javax.annotation.concurrent.Immutable; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.datamodel.TagName; @@ -88,4 +89,7 @@ public class GroupKey> implements Comparable return val.compareTo(o.val); } + public Image getIcon() { + return attr.getIconForValue(val); + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java index d97d41a3e3..200ef32a16 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-15 Basis Technology Corp. + * Copyright 2013-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,7 +35,6 @@ import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.function.Consumer; import java.util.logging.Level; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -295,7 +294,7 @@ public class GroupManager { // If we're grouping by category, we don't want to remove empty groups. if (groupKey.getAttribute() != DrawableAttribute.CATEGORY) { - if (group.fileIds().isEmpty()) { + if (group.getFileIDs().isEmpty()) { Platform.runLater(() -> { if (analyzedGroups.contains(group)) { analyzedGroups.remove(group); @@ -357,12 +356,7 @@ public class GroupManager { Pattern.compile(",").splitAsStream(objIds) .map(Long::valueOf) .filter(db::isInDB) - .findAny().ifPresent(new Consumer() { - - public void accept(Long obj_id) { - types.add(mimeType); - } - }); + .findAny().ifPresent(obj_id -> types.add(mimeType)); } } catch (SQLException | TskCoreException ex) { Exceptions.printStackTrace(ex); @@ -702,8 +696,8 @@ public class GroupManager { ? "SELECT obj_id FROM tsk_files WHERE mime_type IS NULL" : "SELECT obj_id FROM tsk_files WHERE mime_type = '" + mimeType + "'"; - try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery(query);) { - ResultSet resultSet = executeQuery.getResultSet(); + try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery(query); + ResultSet resultSet = executeQuery.getResultSet();) { while (resultSet.next()) { final long fileID = resultSet.getLong("obj_id"); if (db.isInDB(fileID)) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java index de2c806105..53f464dbb0 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-14 Basis Technology Corp. + * Copyright 2013-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,7 +59,7 @@ public enum GroupSortBy implements ComparatorProvider { GROUP_BY_VALUE("Group Name", true, "folder-rename.png") { @Override public Comparator getGrpComparator(final SortOrder sortOrder) { - return applySortOrder(sortOrder, Comparator.comparing(t -> t.groupKey.getValueDisplayName())); + return applySortOrder(sortOrder, Comparator.comparing(t -> t.getGroupByValueDislpayName())); } @Override @@ -71,10 +71,6 @@ public enum GroupSortBy implements ComparatorProvider { * don't sort the groups just use what ever order they come in (ingest * order) */ - /** - * don't sort the groups just use what ever order they come in (ingest - * order) - */ NONE("None", false, "prohibition.png") { @Override public Comparator getGrpComparator(SortOrder sortOrder) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java index c82f625763..a31001fd2f 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java @@ -254,7 +254,7 @@ public class GroupPane extends BorderPane { private final InvalidationListener filesSyncListener = (observable) -> { final String header = getHeaderString(); - final List fileIds = getGroup().fileIds(); + final List fileIds = getGroup().getFileIDs(); Platform.runLater(() -> { slideShowToggle.setDisable(fileIds.isEmpty()); gridView.getItems().setAll(fileIds); @@ -289,8 +289,8 @@ public class GroupPane extends BorderPane { } //assign last selected file or if none first file in group - if (slideShowFileID == null || getGroup().fileIds().contains(slideShowFileID) == false) { - slideShowPane.setFile(getGroup().fileIds().get(0)); + if (slideShowFileID == null || getGroup().getFileIDs().contains(slideShowFileID) == false) { + slideShowPane.setFile(getGroup().getFileIDs().get(0)); } else { slideShowPane.setFile(slideShowFileID); } @@ -324,7 +324,7 @@ public class GroupPane extends BorderPane { } private void selectAllFiles() { - selectionModel.clearAndSelectAll(getGroup().fileIds()); + selectionModel.clearAndSelectAll(getGroup().getFileIDs()); } /** @@ -676,7 +676,7 @@ public class GroupPane extends BorderPane { if (isNull(viewState) || isNull(viewState.getGroup())) { if (nonNull(getGroup())) { - getGroup().fileIds().removeListener(filesSyncListener); + getGroup().getFileIDs().removeListener(filesSyncListener); } this.grouping.set(null); @@ -695,16 +695,16 @@ public class GroupPane extends BorderPane { } else { if (getGroup() != viewState.getGroup()) { if (nonNull(getGroup())) { - getGroup().fileIds().removeListener(filesSyncListener); + getGroup().getFileIDs().removeListener(filesSyncListener); } this.grouping.set(viewState.getGroup()); - getGroup().fileIds().addListener(filesSyncListener); + getGroup().getFileIDs().addListener(filesSyncListener); final String header = getHeaderString(); Platform.runLater(() -> { - gridView.getItems().setAll(getGroup().fileIds()); + gridView.getItems().setAll(getGroup().getFileIDs()); slideShowToggle.setDisable(gridView.getItems().isEmpty()); groupLabel.setText(header); resetScrollBar(); @@ -737,10 +737,10 @@ public class GroupPane extends BorderPane { if (shiftDown) { //TODO: do more hear to implement slicker multiselect - int endIndex = grouping.get().fileIds().indexOf(newFileID); - int startIndex = IntStream.of(grouping.get().fileIds().size(), selectionAnchorIndex, endIndex).min().getAsInt(); + int endIndex = grouping.get().getFileIDs().indexOf(newFileID); + int startIndex = IntStream.of(grouping.get().getFileIDs().size(), selectionAnchorIndex, endIndex).min().getAsInt(); endIndex = IntStream.of(0, selectionAnchorIndex, endIndex).max().getAsInt(); - List subList = grouping.get().fileIds().subList(Math.max(0, startIndex), Math.min(endIndex, grouping.get().fileIds().size()) + 1); + List subList = grouping.get().getFileIDs().subList(Math.max(0, startIndex), Math.min(endIndex, grouping.get().getFileIDs().size()) + 1); selectionModel.clearAndSelectAll(subList.toArray(new Long[subList.size()])); selectionModel.select(newFileID); @@ -802,7 +802,7 @@ public class GroupPane extends BorderPane { switch (t.getCode()) { case SHIFT: if (selectionAnchorIndex == null) { - selectionAnchorIndex = grouping.get().fileIds().indexOf(selectionModel.lastSelectedProperty().get()); + selectionAnchorIndex = grouping.get().getFileIDs().indexOf(selectionModel.lastSelectedProperty().get()); } t.consume(); break; @@ -880,7 +880,7 @@ public class GroupPane extends BorderPane { Long lastSelectFileId = selectionModel.lastSelectedProperty().get(); int lastSelectedIndex = lastSelectFileId != null - ? grouping.get().fileIds().indexOf(lastSelectFileId) + ? grouping.get().getFileIDs().indexOf(lastSelectFileId) : Optional.ofNullable(selectionAnchorIndex).orElse(0); final int columns = Math.max((int) Math.floor((gridView.getWidth() - 18) / (gridView.getCellWidth() + gridView.getHorizontalCellSpacing() * 2)), 1); @@ -889,15 +889,15 @@ public class GroupPane extends BorderPane { // implement proper keyboard based multiselect int indexOfToBeSelectedTile = lastSelectedIndex + tileIndexMap.get(t.getCode()); - final int size = grouping.get().fileIds().size(); + final int size = grouping.get().getFileIDs().size(); if (0 > indexOfToBeSelectedTile) { //don't select past begining of group } else if (0 <= indexOfToBeSelectedTile && indexOfToBeSelectedTile < size) { //normal selection within group - makeSelection(t.isShiftDown(), grouping.get().fileIds().get(indexOfToBeSelectedTile)); + makeSelection(t.isShiftDown(), grouping.get().getFileIDs().get(indexOfToBeSelectedTile)); } else if (indexOfToBeSelectedTile <= size - 1 + columns - (size % columns)) { //selection last item if selection is empty space at end of group - makeSelection(t.isShiftDown(), grouping.get().fileIds().get(size - 1)); + makeSelection(t.isShiftDown(), grouping.get().getFileIDs().get(size - 1)); } else { //don't select past end of group } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java index f7c0ec1751..59d747a655 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java @@ -118,7 +118,7 @@ public class SlideShowView extends DrawableTileBase { getGroupPane().grouping().addListener((Observable observable) -> { syncButtonVisibility(); if (getGroupPane().getGroup() != null) { - getGroupPane().getGroup().fileIds().addListener((Observable observable1) -> { + getGroupPane().getGroup().getFileIDs().addListener((Observable observable1) -> { syncButtonVisibility(); }); } @@ -128,7 +128,7 @@ public class SlideShowView extends DrawableTileBase { @ThreadConfined(type = ThreadType.ANY) private void syncButtonVisibility() { try { - final boolean hasMultipleFiles = getGroupPane().getGroup().fileIds().size() > 1; + final boolean hasMultipleFiles = getGroupPane().getGroup().getFileIDs().size() > 1; Platform.runLater(() -> { rightButton.setVisible(hasMultipleFiles); leftButton.setVisible(hasMultipleFiles); @@ -265,12 +265,12 @@ public class SlideShowView extends DrawableTileBase { @ThreadConfined(type = ThreadType.JFX) synchronized private void cycleSlideShowImage(int direction) { stopVideo(); - final int groupSize = getGroupPane().getGroup().fileIds().size(); + final int groupSize = getGroupPane().getGroup().getFileIDs().size(); final Integer nextIndex = getFileID().map(fileID -> { - final int currentIndex = getGroupPane().getGroup().fileIds().indexOf(fileID); + final int currentIndex = getGroupPane().getGroup().getFileIDs().indexOf(fileID); return (currentIndex + direction + groupSize) % groupSize; }).orElse(0); - setFile(getGroupPane().getGroup().fileIds().get(nextIndex)); + setFile(getGroupPane().getGroup().getFileIDs().get(nextIndex)); } @@ -279,7 +279,7 @@ public class SlideShowView extends DrawableTileBase { * of y" */ private String getSupplementalText() { - final ObservableList fileIds = getGroupPane().getGroup().fileIds(); + final ObservableList fileIds = getGroupPane().getGroup().getFileIDs(); return getFileID().map(fileID -> " ( " + (fileIds.indexOf(fileID) + 1) + " of " + fileIds.size() + " in group )") .orElse(""); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupListCell.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupListCell.java index 8a56db05b3..6951d67997 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupListCell.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupListCell.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015 Basis Technology Corp. + * Copyright 2015-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -62,9 +62,7 @@ class GroupListCell extends ListCell { */ private final InvalidationListener seenListener = (Observable o) -> { final String style = getSeenStyleClass(); - Platform.runLater(() -> { - setStyle(style); - }); + Platform.runLater(() -> setStyle(style)); }; private final ReadOnlyObjectProperty> sortOrder; @@ -77,24 +75,19 @@ class GroupListCell extends ListCell { //since end of path is probably more interesting put ellipsis at front setTextOverrun(OverrunStyle.LEADING_ELLIPSIS); - Platform.runLater(() -> { - prefWidthProperty().bind(getListView().widthProperty().subtract(15)); - }); - + Platform.runLater(() -> prefWidthProperty().bind(getListView().widthProperty().subtract(15))); } - /** - * {@inheritDoc } - */ @Override protected synchronized void updateItem(final DrawableGroup group, boolean empty) { //if there was a previous group, remove the listeners Optional.ofNullable(getItem()) .ifPresent(oldGroup -> { sortOrder.removeListener(fileCountListener); - oldGroup.fileIds().removeListener(fileCountListener); + oldGroup.getFileIDs().removeListener(fileCountListener); oldGroup.seenProperty().removeListener(seenListener); - oldGroup.uncatCountProperty().removeListener(fileCountListener); + oldGroup.uncatCountProperty().removeListener(fileCountListener); + oldGroup.hashSetHitsCountProperty().removeListener(fileCountListener); }); super.updateItem(group, empty); @@ -107,35 +100,33 @@ class GroupListCell extends ListCell { setStyle(""); }); } else { + final String text = getGroupName() + getCountsText(); + String style; + Image icon; if (isNull(group)) { - final String text = getGroupName(); //"dummy" group in file system tree <=> a folder with no drawables - Platform.runLater(() -> { - setTooltip(new Tooltip(text)); - setText(text); - setGraphic(new ImageView(EMPTY_FOLDER_ICON)); - setStyle(""); - }); - + icon = EMPTY_FOLDER_ICON; + style = ""; } else { //if number of files in this group changes (eg a file is recategorized), update counts via listener - group.fileIds().addListener(fileCountListener); + group.getFileIDs().addListener(fileCountListener); group.uncatCountProperty().addListener(fileCountListener); + group.hashSetHitsCountProperty().addListener(fileCountListener); sortOrder.addListener(fileCountListener); //if the seen state of this group changes update its style group.seenProperty().addListener(seenListener); //and use icon corresponding to group type - final Image icon = group.groupKey.getAttribute().getIcon(); - final String text = getGroupName() + getCountsText(); - final String style = getSeenStyleClass(); - Platform.runLater(() -> { - setTooltip(new Tooltip(text)); - setGraphic(new ImageView(icon)); - setText(text); - setStyle(style); - }); + icon = group.getGroupKey().getIcon(); + style = getSeenStyleClass(); } + + Platform.runLater(() -> { + setTooltip(new Tooltip(text)); + setGraphic(new ImageView(icon)); + setText(text); + setStyle(style); + }); } } @@ -166,7 +157,6 @@ class GroupListCell extends ListCell { */ @Nonnull private String getCountsText() { - return Optional.ofNullable(getItem()) .map(group -> " (" + (sortOrder.get() == GroupComparators.ALPHABETICAL diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTree.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTree.java index f2d6fc0e95..212a7adc24 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTree.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTree.java @@ -137,13 +137,13 @@ final public class GroupTree extends NavPanel> { } private static List groupingToPath(DrawableGroup g) { - String path = g.groupKey.getValueDisplayName(); - if (g.groupKey.getAttribute() != DrawableAttribute.PATH) { - String stripStart = StringUtils.strip(path, "/"); - return Arrays.asList(stripStart); - } else { + String path = g.getGroupByValueDislpayName(); + if (g.getGroupByAttribute() == DrawableAttribute.PATH) { String[] cleanPathTokens = StringUtils.stripStart(path, "/").split("/"); return Arrays.asList(cleanPathTokens); + } else { + String stripStart = StringUtils.strip(path, "/"); + return Arrays.asList(stripStart); } } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeCell.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeCell.java index d061477b9f..917aa35084 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeCell.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupTreeCell.java @@ -96,7 +96,7 @@ class GroupTreeCell extends TreeCell { .map(GroupTreeNode::getGroup) .ifPresent(group -> { sortOrder.addListener(fileCountListener); - group.fileIds().removeListener(fileCountListener); + group.getFileIDs().removeListener(fileCountListener); group.hashSetHitsCountProperty().removeListener(fileCountListener); group.seenProperty().removeListener(seenListener); group.uncatCountProperty().removeListener(fileCountListener); @@ -124,7 +124,7 @@ class GroupTreeCell extends TreeCell { } else { //if number of files in this group changes (eg a file is recategorized), update counts via listener - treeNode.getGroup().fileIds().addListener(fileCountListener); + treeNode.getGroup().getFileIDs().addListener(fileCountListener); treeNode.getGroup().uncatCountProperty().addListener(fileCountListener); treeNode.getGroup().hashSetHitsCountProperty().addListener(fileCountListener); sortOrder.addListener(fileCountListener); @@ -132,7 +132,7 @@ class GroupTreeCell extends TreeCell { treeNode.getGroup().seenProperty().addListener(seenListener); //and use icon corresponding to group type - final Image icon = treeNode.getGroup().groupKey.getAttribute().getIcon(); + final Image icon = treeNode.getGroup().getGroupKey().getIcon(); final String text = getGroupName() + getCountsText(); final String style = getSeenStyleClass(); Platform.runLater(() -> { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/images/mime_types.png b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/images/mime_types.png new file mode 100644 index 0000000000000000000000000000000000000000..63e0b2a9c17bec677d5db77375dd60aa36fdd1e2 GIT binary patch literal 1453 zcmaJ>drT8|96!Z1D6*hV4Kl{{L^NP~ceMq2YiIhPvx9c5)KD`8j`n~P@8R8H3q}$M zFmO!J!3>*#h#F16m^e3rh+!a3+?Zw~A``WX1E!LyI7~JlyCUHJ7`x=|_sHk-`My56 zie&5i!HZTc0stV`yvb-2*AV|3C=tIA6EAUb2^A6?LJE^5xN(jEbS@^70L>JhP1p$B zmA~^cp#gvZh_pKdhb0MfGL#(mW8@x+71;ow+2~<$XD%UtnM5{8Ya!pc3lK=Uv{0(b zf>>Askwb1O;E0q0tKC_U>r}g-jqiaP4<-swgn)w{Y6s0@9xXJbi-~*xH4K4M5Fu9! zy-LbqNd^rJM}R6hB6A|KC>X1hqe?`rRIdjW2#Ua{0!HFwD1xctFa!msA4rVGxw0^u z(KH=PJZYgEL0~Z$cDvnj_Xat`Wy7dit%eZ=tWd~AgpALp1>7T}`PDNDMuK;8BrA{% z4f+-FOeRmzLSm+`N}$+TS(=}2lh`oWgR?LyNBkwt04rmnip)u>wN!Hn78M% z1Z*RCCXaK9gUed&55-~zj=%+mvop+&nJy;hFapEpFf3?DQOQv-m84yao1em4ESQ<* z1)O#gW}_Ao733u8!t`o&tTJAY#>MJYD4L+qsq~6?lSvnw5UVsP^>H&?Bje1Y2wIro zx?Xb?b8`Jwpja`ok>JQ(gv-P+6gX8fM$Vaw&NQdqG}kqEF36l*SWE`?5B6V!p1Bg; zoO?XW`?3Y+p~%4>aZpI*x-&E1vC^l#~ZvxYqO>NV`>?Jb0|R zY7KsSuxEI!q<``WccInyu}7(;p08iAByd5=m!X;$ZAJH2{sc06bp7oM&kYxKb{0Od zKW5vv9x5qA>T}KvYzANSpKWjUuG?bT-%)m>Aav3mwDmxxfL$CPAHTHU+KQ{|77s** zJT05lG@KJP%^ z8>h|;rk+H+zO%NvKiXOg%fFS^7lSQc?~$^1akFkwMQ%`gcUv>lOd=z#e}jrcVr^ko%OQ+LmM5C{N1 Xxqat=G|aQc{{uBASdFLj8O8qqUfm?h literal 0 HcmV?d00001