package org.fenixedu.bennu.io.domain;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jvstm.PerTxBox;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.CharMatcher;
import com.google.common.io.Files;
/**
*
* @author Shezad Anavarali Date: Jul 16, 2009
*
*/
public class LocalFileSystemStorage extends LocalFileSystemStorage_Base {
private static final Logger logger = LoggerFactory.getLogger(LocalFileSystemStorage.class);
private transient PerTxBox<Map<String, FileWriteIntention>> fileIntentions;
private static class FileWriteIntention {
private final String path;
private final byte[] contents;
private final File file;
FileWriteIntention(String path, byte[] contents) {
this.path = path;
this.contents = contents;
this.file = null;
}
FileWriteIntention(String path, File file) {
this.path = path;
this.contents = null;
this.file = file;
}
public void write() throws IOException {
if (contents != null) {
Files.write(contents, new File(path));
} else {
java.nio.file.Files.move(file.toPath(), Paths.get(path));
}
}
public byte[] asByteArray() throws IOException {
if (contents != null) {
return contents;
} else {
return Files.toByteArray(file);
}
}
public InputStream asInputStream() throws IOException {
if (contents != null) {
return new ByteArrayInputStream(contents);
} else {
return new FileInputStream(file);
}
}
}
LocalFileSystemStorage(String name, String path, Integer treeDirectoriesNameLength) {
super();
setName(name);
setPath(path);
setTreeDirectoriesNameLength(treeDirectoriesNameLength);
}
@Override
public String getPath() {
//FIXME: remove when the framework enables read-only slots
return super.getPath();
}
@Override
public Integer getTreeDirectoriesNameLength() {
//FIXME: remove when the framework enables read-only slots
return super.getTreeDirectoriesNameLength();
}
@Override
public String store(GenericFile genericFile, File file) {
String uniqueIdentification =
genericFile.getContentKey() == null ? genericFile.getExternalId() : genericFile.getContentKey();
final String fullPath = getFullPath(uniqueIdentification);
ensureDirectoryExists(fullPath);
Map<String, FileWriteIntention> map = new HashMap<>(getPerTxBox().get());
map.put(uniqueIdentification, new FileWriteIntention(fullPath + uniqueIdentification, file));
getPerTxBox().put(map);
return uniqueIdentification;
}
@Override
public String store(GenericFile file, byte[] content) {
String uniqueIdentification = file.getContentKey() == null ? file.getExternalId() : file.getContentKey();
final String fullPath = getFullPath(uniqueIdentification);
if (content == null) {
new LocalFileToDelete(fullPath + uniqueIdentification);
} else {
ensureDirectoryExists(fullPath);
Map<String, FileWriteIntention> map = new HashMap<>(getPerTxBox().get());
map.put(uniqueIdentification, new FileWriteIntention(fullPath + uniqueIdentification, content));
getPerTxBox().put(map);
}
return uniqueIdentification;
}
private static void ensureDirectoryExists(String fullPath) {
File directory = new File(fullPath);
if (!directory.exists()) {
if (!directory.mkdirs()) {
throw new RuntimeException("Could not create directory " + directory.getName());
}
} else {
if (!directory.isDirectory()) {
throw new RuntimeException("Trying to create " + fullPath
+ " as a directory but, it already exists and it's not a directory");
}
}
}
private String getFullPath(final String uniqueIdentification) {
return getAbsolutePath() + transformIDInPath(uniqueIdentification) + File.separatorChar;
}
private static final Pattern BRACES_PATTERN = Pattern.compile("(\\{.+?\\})");
public String getAbsolutePath() {
String path = getPath();
if (path.contains("{") && path.contains("}")) {
// Compile regular expression
Matcher matcher = BRACES_PATTERN.matcher(path);
// Replace all occurrences of pattern in input
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String replaceStr = CharMatcher.anyOf("{}").trimFrom(matcher.group());
matcher.appendReplacement(result, System.getProperty(replaceStr));
}
matcher.appendTail(result);
path = result.toString();
}
if (!path.endsWith(File.separator)) {
path = path + File.separatorChar;
}
File dir = new File(path);
if (!dir.exists()) {
logger.debug("Filesystem storage {} directory does not exist, creating: {}", getName(), path);
if (!dir.mkdir()) {
throw new RuntimeException("Could not create base directory for " + this.getExternalId() + ": " + path);
}
}
return path;
}
private String transformIDInPath(final String uniqueIdentification) {
final Integer directoriesNameLength = getTreeDirectoriesNameLength();
final StringBuilder result = new StringBuilder();
char[] idArray = uniqueIdentification.toCharArray();
for (int i = 0; i < idArray.length; i++) {
if (i > 0 && i % directoriesNameLength == 0 && ((i + directoriesNameLength) < uniqueIdentification.length())) {
result.append(File.separatorChar);
} else if ((i + directoriesNameLength) >= uniqueIdentification.length()) {
break;
}
result.append(idArray[i]);
}
return result.toString();
}
@Override
public byte[] read(GenericFile file) {
String uniqueIdentification = file.getContentKey();
try {
Map<String, FileWriteIntention> map = getPerTxBox().get();
if (map.containsKey(uniqueIdentification)) {
return map.get(uniqueIdentification).asByteArray();
}
return Files.toByteArray(new File(getFullPath(uniqueIdentification) + uniqueIdentification));
} catch (IOException e) {
throw new RuntimeException("error.store.file", e);
}
}
@Override
public InputStream readAsInputStream(GenericFile file) {
String uniqueIdentification = file.getContentKey();
try {
Map<String, FileWriteIntention> map = getPerTxBox().get();
if (map.containsKey(uniqueIdentification)) {
return map.get(uniqueIdentification).asInputStream();
}
return new FileInputStream(new File(getFullPath(uniqueIdentification) + uniqueIdentification));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("file.not.found", e);
}
}
/*
* Attempt to use the 'sendfile' primitive to download the file.
*
* This feature may not be supported, or the file may not be stored in the
* filesystem, causing this not to work.
*
* However, when it works, it provides great benefits, as the file must not
* be read to the Java Heap, only to be written to a socket, thus greatly
* reducing memory consumption.
*/
@Override
protected boolean tryLowLevelDownload(GenericFile file, HttpServletRequest request, HttpServletResponse response, long start,
long end) {
if (supportsSendfile(request)) {
Optional<String> filePath = getSendfilePath(file.getContentKey());
if (filePath.isPresent()) {
handleSendfile(filePath.get(), request, response, start, end);
return true;
} else {
return false;
}
}
return false;
}
/*
* Sendfile is available, and the file is stored in the filesystem, so instruct
* the container to directly write the file.
*
* For now, we only support the Tomcat-specific 'sendfile' implementation.
* See: http://tomcat.apache.org/tomcat-7.0-doc/aio.html#Asynchronous_writes
*/
private static void handleSendfile(String filename, HttpServletRequest request, HttpServletResponse response, long start,
long end) {
response.setHeader("X-Bennu-Sendfile", "true");
request.setAttribute("org.apache.tomcat.sendfile.filename", filename);
request.setAttribute("org.apache.tomcat.sendfile.start", start);
request.setAttribute("org.apache.tomcat.sendfile.end", end + 1);
}
/*
* Checks if the container supports usage of the 'sendfile' primitive.
*
* For now, we only support the Tomcat-specific 'sendfile' implementation.
* See: http://tomcat.apache.org/tomcat-7.0-doc/aio.html#Asynchronous_writes
*/
private static boolean supportsSendfile(HttpServletRequest request) {
return Boolean.TRUE.equals(request.getAttribute("org.apache.tomcat.sendfile.support"));
}
/*
* Returns the absolute path for the given content key.
*
* It must first check if the file indeed exists, in order
* for the application to throw the proper exception.
*/
private Optional<String> getSendfilePath(String uniqueIdentification) {
String path = getFullPath(uniqueIdentification) + uniqueIdentification;
if (new File(path).exists()) {
return Optional.of(path);
} else {
return Optional.empty();
}
}
private synchronized PerTxBox<Map<String, FileWriteIntention>> getPerTxBox() {
if (fileIntentions == null) {
fileIntentions = new PerTxBox<Map<String, FileWriteIntention>>(Collections.emptyMap()) {
@Override
public void commit(Map<String, FileWriteIntention> map) {
for (FileWriteIntention i : map.values()) {
try {
i.write();
} catch (IOException e) {
throw new RuntimeException("error.store.file", e);
}
}
}
};
}
return fileIntentions;
}
}