/*
* This file is part of Fim - File Integrity Manager
*
* Copyright (C) 2017 Etienne Vrignaud
*
* Fim is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fim is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Fim. If not, see <http://www.gnu.org/licenses/>.
*/
package org.fim.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.base.Charsets;
import com.google.common.base.MoreObjects;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.rits.cloning.Cloner;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class FileState implements Hashable {
private static final Cloner CLONER = new Cloner();
private String fileName;
private long fileLength;
private FileTime fileTime;
private Modification modification;
private FileHash fileHash;
private Map<String, String> fileAttributes;
private FileState previousFileState;
private transient FileHash newFileHash; // Used by StateComparator to detect accurately duplicates
private transient FileHash originalFileHash;
private transient boolean toRemove;
public FileState() {
// Empty constructor for Jackson
}
public FileState(String fileName, long fileLength, FileTime fileTime, FileHash fileHash, List<Attribute> attributeList) {
if (fileName == null) {
throw new IllegalArgumentException("Invalid null fileName");
}
if (fileHash == null) {
throw new IllegalArgumentException("Invalid null hash");
}
setFileName(fileName);
setFileLength(fileLength);
setFileTime(fileTime);
setFileHash(fileHash);
setFileAttributesList(attributeList);
}
public FileState(String fileName, BasicFileAttributes attributes, FileHash fileHash, List<Attribute> attributeList) {
this(fileName, attributes.size(), new FileTime(attributes), fileHash, attributeList);
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
// Intern Strings to decrease memory usage
this.fileName = fileName.intern();
}
public long getFileLength() {
return fileLength;
}
public void setFileLength(long fileLength) {
this.fileLength = fileLength;
}
public FileTime getFileTime() {
return fileTime;
}
public void setFileTime(FileTime fileTime) {
this.fileTime = fileTime;
}
public Modification getModification() {
return modification;
}
public void setModification(Modification modification) {
this.modification = modification;
}
public FileHash getFileHash() {
return fileHash;
}
public void setFileHash(FileHash fileHash) {
this.fileHash = fileHash;
}
public Map<String, String> getFileAttributes() {
return fileAttributes;
}
private void setFileAttributesList(List<Attribute> attributeList) {
this.fileAttributes = toMap(attributeList);
}
public void setFileAttributes(Map<String, String> fileAttributes) {
this.fileAttributes = internAttributes(fileAttributes);
}
public FileState getPreviousFileState() {
return previousFileState;
}
public void setPreviousFileState(FileState previousFileState) {
this.previousFileState = previousFileState;
}
@JsonIgnore
public FileHash getNewFileHash() {
return newFileHash;
}
public void setNewFileHash(FileHash newFileHash) {
this.newFileHash = newFileHash;
}
public void resetNewHash() {
newFileHash = fileHash;
}
public void storeOriginalHash() {
originalFileHash = fileHash;
}
public void restoreOriginalHash() {
if (originalFileHash != null) {
fileHash = originalFileHash;
}
}
public boolean isToRemove() {
return toRemove;
}
public void setToRemove(boolean toRemove) {
this.toRemove = toRemove;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || !(other instanceof FileState)) {
return false;
}
FileState otherFileState = (FileState) other;
return Objects.equals(this.fileName, otherFileState.fileName)
&& Objects.equals(this.fileLength, otherFileState.fileLength)
&& Objects.equals(this.fileTime, otherFileState.fileTime)
&& Objects.equals(this.fileHash, otherFileState.fileHash)
&& Objects.equals(this.fileAttributes, otherFileState.fileAttributes);
}
/**
* @deprecated hashCode() should not be used, because there is a big risk of hash collision. Use longHashCode() instead.
* Those hash collision appears when you manage millions of FileStates.
*/
@Override
@Deprecated
public int hashCode() {
return Objects.hash(fileName, fileLength, fileTime, fileHash, fileAttributes);
}
/**
* Returns a long hash code value for the object.
* A long is used to avoid hashCode collisions when we have a huge number of FileStates.
*/
public long longHashCode() {
HashFunction hashFunction = Hashing.sha512();
Hasher hasher = hashFunction.newHasher(Constants._4_KB);
hashObject(hasher, true);
HashCode hash = hasher.hash();
return hash.asLong();
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("fileName", fileName)
.add("fileLength", fileLength)
.add("fileTime", fileTime)
.add("fileHash", fileHash)
.add("fileAttributes", fileAttributes)
.add("newFileHash", newFileHash)
.toString();
}
@Override
public void hashObject(Hasher hasher) {
hashObject(hasher, false);
}
public void hashObject(Hasher hasher, boolean millisecondsRemoved) {
hasher
.putString("FileState", Charsets.UTF_8)
.putChar(HASH_FIELD_SEPARATOR)
.putString(fileName, Charsets.UTF_8)
.putChar(HASH_FIELD_SEPARATOR)
.putLong(fileLength);
hasher.putChar(HASH_OBJECT_SEPARATOR);
fileTime.hashObject(hasher, millisecondsRemoved);
hasher.putChar(HASH_OBJECT_SEPARATOR);
fileHash.hashObject(hasher);
hasher.putChar(HASH_OBJECT_SEPARATOR);
if (fileAttributes != null) {
for (Map.Entry<String, String> entry : fileAttributes.entrySet()) {
hasher
.putString(entry.getKey(), Charsets.UTF_8)
.putChar(':')
.putChar(':')
.putString(entry.getValue(), Charsets.UTF_8);
hasher.putChar(HASH_OBJECT_SEPARATOR);
}
}
}
@Override
public FileState clone() {
return CLONER.deepClone(this);
}
private Map<String, String> toMap(List<Attribute> attrs) {
if (attrs == null) {
return null;
}
Map<String, String> map = new HashMap<>();
for (Attribute attr : attrs) {
// Intern Strings to decrease memory usage
map.put(attr.getName().intern(), attr.getValue().intern());
}
return map;
}
/**
* Intern Map content to decrease memory usage
*/
private Map<String, String> internAttributes(Map<String, String> attributes) {
if (attributes == null) {
return null;
}
Map<String, String> newAttributes = new HashMap<>();
for (Map.Entry<String, String> entry : attributes.entrySet()) {
newAttributes.put(entry.getKey().intern(), entry.getValue().intern());
}
return newAttributes;
}
public static class FileNameComparator implements Comparator<FileState> {
@Override
public int compare(FileState fs1, FileState fs2) {
return fs1.getFileName().compareTo(fs2.getFileName());
}
}
public static class HashComparator implements Comparator<FileState> {
@Override
public int compare(FileState fs1, FileState fs2) {
return fs1.getFileHash().compareTo(fs2.getFileHash());
}
}
}