/*
* 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.collect.Iterables;
import com.google.common.eventbus.Subscribe;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
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;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
/**
* Represents a set of image/video files in a group. The UI listens to changes
* to the group membership and updates itself accordingly.
*/
public class DrawableGroup implements Comparable<DrawableGroup> {
private static final Logger LOGGER = Logger.getLogger(DrawableGroup.class.getName());
public static String getBlankGroupName() {
return "unknown";
}
private final GroupKey<?> groupKey;
private final ObservableList<Long> fileIDs = FXCollections.observableArrayList();
private final ObservableList<Long> unmodifiableFileIDS = FXCollections.unmodifiableObservableList(fileIDs);
//cache the number of files in this groups with hashset hits
private final ReadOnlyLongWrapper hashSetHitsCount = new ReadOnlyLongWrapper(-1);
//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<Long> filesInGroup, boolean seen) {
this.groupKey = groupKey;
this.fileIDs.setAll(filesInGroup);
fileIDs.addListener((ListChangeListener.Change<? extends Long> listchange) -> {
boolean seenChanged = false;
while (false == seenChanged && listchange.next()) {
seenChanged |= listchange.wasAdded();
}
invalidateProperties(seenChanged);
});
this.seen.set(seen);
}
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public synchronized ObservableList<Long> getFileIDs() {
return unmodifiableFileIDS;
}
public GroupKey<?> getGroupKey() {
return groupKey;
}
public DrawableAttribute<?> getGroupByAttribute() {
return groupKey.getAttribute();
}
public Object getGroupByValue() {
return groupKey.getValue();
}
public String getGroupByValueDislpayName() {
return groupKey.getValueDisplayName();
}
public synchronized int getSize() {
return fileIDs.size();
}
public IntegerBinding sizeProperty() {
return Bindings.size(fileIDs);
}
public double getHashHitDensity() {
getHashSetHitsCount(); //initialize hashSetHitsCount
return hashDensity.get();
}
public DoubleBinding hashHitDensityProperty() {
getHashSetHitsCount(); //initialize hashSetHitsCount
return hashDensity;
}
/**
* @return the number of files in this group that have hash set hits
*/
public synchronized long getHashSetHitsCount() {
if (hashSetHitsCount.get() < 0) {
try {
hashSetHitsCount.set(fileIDs.stream()
.map(fileID -> ImageGalleryController.getDefault().getHashSetManager().isInAnyHashSet(fileID))
.filter(Boolean::booleanValue)
.count());
} catch (IllegalStateException | NullPointerException ex) {
LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS
}
}
return hashSetHitsCount.get();
}
public ReadOnlyLongProperty hashSetHitsCountProperty() {
getHashSetHitsCount(); //initialize hashSetHitsCount
return hashSetHitsCount.getReadOnlyProperty();
}
public final synchronized long getUncategorizedCount() {
if (uncatCount.get() < 0) {
try {
uncatCount.set(ImageGalleryController.getDefault().getDatabase().getUncategorizedCount(fileIDs));
} catch (IllegalStateException | NullPointerException ex) {
LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS
}
}
return uncatCount.get();
}
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<? extends Long> 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
public String toString() {
return "Grouping{ keyProp=" + groupKey + '}'; //NON-NLS
}
@Override
public int hashCode() {
int hash = 3;
hash = 53 * hash + Objects.hashCode(this.groupKey);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
return Objects.equals(this.groupKey,
((DrawableGroup) obj).groupKey);
}
// By default, sort by group key name
@Override
public int compareTo(DrawableGroup other) {
return this.groupKey.getValueDisplayName().compareTo(other.groupKey.getValueDisplayName());
}
}