package org.fenixedu.bennu.io.domain;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import javax.activation.MimetypesFileTypeMap;
import org.apache.tika.Tika;
import org.fenixedu.bennu.core.domain.User;
import org.fenixedu.commons.StringNormalizer;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pt.ist.fenixframework.Atomic;
import com.google.common.base.Strings;
import com.google.common.hash.Funnels;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
/**
*
* @author Shezad Anavarali Date: Jul 15, 2009
* @author Sérgio Silva (sergio.silva@tecnico.ulisboa.pt)
*
*/
public abstract class GenericFile extends GenericFile_Base {
private static final Logger logger = LoggerFactory.getLogger(GenericFile.class);
/**
* Used to detect file content type. {@link Tika#detect(byte[], String)} is thread-safe.
*/
private static final Tika tika = new Tika();
protected GenericFile() {
super();
setFileSupport(FileSupport.getInstance());
setCreationDate(new DateTime());
}
protected void init(String displayName, String filename, byte[] content) {
if (content == null) {
throw new NullPointerException("Content byte[] is null");
}
setDisplayName(displayName);
setFilename(filename);
setContent(content);
}
/**
* Initializes this file with the contents of the provided {@link File}.
*
* @param displayName
* The pretty name for this file
* @param filename
* The low-level filename for this file
* @param file
* The file from which the contents of the newly created file are based upon
* @throws IOException
* If an error occurs while reading the input file or storing it in the underlying storage
*/
protected void init(String displayName, String filename, File file) throws IOException {
if (file == null) {
throw new NullPointerException("Content is null");
}
setDisplayName(displayName);
setFilename(filename);
setContent(file, filename);
}
public abstract boolean isAccessible(User user);
public boolean isPrivate() {
return !isAccessible(null);
}
@Override
public DateTime getCreationDate() {
//FIXME: remove when the framework enables read-only slots
return super.getCreationDate();
}
@Override
public Long getSize() {
//FIXME: remove when the framework enables read-only slots
return super.getSize();
}
@Override
public String getContentType() {
//FIXME: remove when the framework enables read-only slots
return super.getContentType();
}
@Override
public String getContentKey() {
//FIXME: remove when the framework enables read-only slots
return super.getContentKey();
}
/**
* Returns the checksum for this file, using the algorithm specified by {@link #getChecksumAlgorithm()}.
*
* Due to performance concerns, this value is computed lazily, and may be cached in the domain. To ensure the value is cached,
* you should invoke the {@link #ensureChecksum()} method before any operation that manipulates a large number of files.
*
* @return
* The checksum of this file, never {@code null}
*/
@Override
public String getChecksum() {
if (super.getChecksum() == null) {
return computeChecksum();
} else {
return super.getChecksum();
}
}
/**
* Returns the algorithm used to compute the checksum of this file.
*
* @return
* The algorithm used to compute the checksum of this file, never {@code null}
*/
@Override
public String getChecksumAlgorithm() {
if (super.getChecksumAlgorithm() == null) {
return DEFAULT_CHECKSUM_ALGORITHM;
} else {
return super.getChecksumAlgorithm();
}
}
/**
* Ensures that this file's checksum is stored in the database, thus increasing the performance of {@link #getChecksum()}.
*/
public void ensureChecksum() {
if (super.getChecksum() == null) {
setChecksum(computeChecksum());
setChecksumAlgorithm(DEFAULT_CHECKSUM_ALGORITHM);
}
}
// Always ensure these two are synchronized
private static final String DEFAULT_CHECKSUM_ALGORITHM = "murmur3_128";
private static final HashFunction DEFAULT_HASH_FUNCTION = Hashing.murmur3_128();
private String computeChecksum() {
Hasher hasher = DEFAULT_HASH_FUNCTION.newHasher();
try (InputStream stream = getStream(); OutputStream out = Funnels.asOutputStream(hasher)) {
ByteStreams.copy(stream, out);
return hasher.hash().toString();
} catch (IOException e) {
throw new RuntimeException("Cannot compute checksum for " + getExternalId(), e);
}
}
@Override
public void setFilename(String filename) {
final String nicerFilename = filename.substring(filename.lastIndexOf('/') + 1);
final String normalizedFilename = StringNormalizer.normalizePreservingCapitalizedLetters(nicerFilename);
super.setFilename(normalizedFilename);
if (getContentKey() != null) {
//no point in calculating the content type before content is set
setContentType(detectContentType(getContent(), normalizedFilename));
}
}
private void setContent(File file, String filename) throws IOException {
long size = file.length();
setSize(Long.valueOf(size));
final FileStorage fileStorage = getFileStorage();
final String uniqueIdentification = fileStorage.store(this, file);
setStorage(fileStorage);
if (Strings.isNullOrEmpty(uniqueIdentification)) {
throw new RuntimeException();
}
setContentKey(uniqueIdentification);
setContentType(detectContentType(file, filename));
}
private void setContent(byte[] content) {
long size = (content == null) ? 0 : content.length;
setSize(Long.valueOf(size));
final FileStorage fileStorage = getFileStorage();
final String uniqueIdentification = fileStorage.store(this, content);
setStorage(fileStorage);
if (Strings.isNullOrEmpty(uniqueIdentification) && content != null) {
throw new RuntimeException();
}
setContentKey(uniqueIdentification);
if (content != null) {
setContentType(detectContentType(content, getFilename()));
}
}
public byte[] getContent() {
return getStorage().read(this);
}
public InputStream getStream() {
return getStorage().readAsInputStream(this);
}
public static void convertFileStorages(final FileStorage fileStorageToUpdate) {
if (fileStorageToUpdate != null) {
try {
for (final GenericFile genericFile : FileSupport.getInstance().getFileSet()) {
if (fileStorageToUpdate == genericFile.getFileStorage() && fileStorageToUpdate != genericFile.getStorage()) {
genericFile.updateFileStorage();
}
}
logger.debug("FILE Conversion: DONE SUCESSFULLY!");
} catch (Throwable e) {
logger.debug("FILE Conversion: ABORTED!!!");
e.printStackTrace();
}
}
}
@Atomic
private void updateFileStorage() {
setContent(getContent());
}
protected FileStorage getFileStorage() {
final FileStorage fileStorage = FileStorageConfiguration.readFileStorageByFileType(getClass().getName());
if (fileStorage == null) {
return FileSupport.getInstance().getDefaultStorage();
}
return fileStorage;
}
/**
* Guessing file content type with {@link javax.activation.MimetypesFileTypeMap} is not enough.
*
* @param filename
* The name of the file to evaluate
* @return file content type
* The detected file type, from the given file name
* @deprecated content detection is done automatically, no need for this method
*/
@Deprecated
protected String guessContentType(final String filename) {
return new MimetypesFileTypeMap().getContentType(filename);
}
/**
* Detect content type based on file content "magic" bytes. Fallback to filename extension if file content is inconclusive.
*
* @param content
* The content of the file, for magic byte analysis
* @param filename
* The name of the file to evaluate
*
* @return the detected mime-type. application/octet-stream returned when detection was not successful.
*
* @see Tika
*/
protected String detectContentType(byte[] content, String filename) {
return tika.detect(content, filename);
}
/**
* Detect content type based on file content "magic" bytes. Fallback to filename extension if file content is inconclusive.
*
* @return the detected mime-type. application/octet-stream returned when detection was not successful.
*
* @see Tika
*/
private static final String detectContentType(File file, String filename) throws IOException {
try (InputStream stream = new FileInputStream(file)) {
return tika.detect(stream, filename);
}
}
public void delete() {
setContent(null);
setStorage(null);
setFileSupport(null);
deleteDomainObject();
}
@SuppressWarnings("unchecked")
public static <T extends GenericFile> List<T> getFiles(final Class<T> clazz) {
final List<T> files = new ArrayList<>();
for (final GenericFile file : FileSupport.getInstance().getFileSet()) {
if (file.getClass().equals(clazz)) {
files.add((T) file);
}
}
return files;
}
}