NIHVIVO-160 These belong in the 'src' directory, not in the 'test' directory.

This commit is contained in:
jeb228 2010-05-21 17:35:11 +00:00
parent 197aa2f4f9
commit 734067f22a
9 changed files with 551 additions and 315 deletions

View file

@ -0,0 +1,38 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.filestorage;
import java.io.IOException;
/**
* Indicates that a file already exists with the specified ID, but with a
* different filename from the one specified.
*/
public class FileAlreadyExistsException extends IOException {
private final String id;
private final String existingFilename;
private final String requestedFilename;
public FileAlreadyExistsException(String id, String existingFilename,
String requestedFilename) {
super("File with a different name already exists at this ID: '" + id
+ "', requested filename: '" + requestedFilename
+ "', existing filename: '" + existingFilename + "'");
this.id = id;
this.existingFilename = existingFilename;
this.requestedFilename = requestedFilename;
}
public String getId() {
return id;
}
public String getExistingFilename() {
return existingFilename;
}
public String getRequestedFilename() {
return requestedFilename;
}
}

View file

@ -0,0 +1,63 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.filestorage;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import edu.cornell.mannlib.vitro.webapp.ConfigurationProperties;
/**
* The interface for the File Storage system.
*/
public interface FileStorage {
/**
* The default implementation will use this key to ask
* {@link ConfigurationProperties} for the file storage base directory.
*/
String PROPERTY_FILE_STORAGE_BASE_DIR = "upload.directory";
/**
* The default implementation will use this key to ask
* {@link ConfigurationProperties} for the default URI namespace.
*/
String PROPERTY_DEFAULT_NAMESPACE = "Vitro.defaultNamespace";
/**
* Store the bytes from this stream as a file with the specified ID and
* filename. If the file already exists, it is over-written.
*
* @throws FileAlreadyExistsException
* if a file already exists with this ID but with a different
* filename.
*
*/
void createFile(String id, String filename, InputStream bytes)
throws FileAlreadyExistsException, IOException;
/**
* If a file exists with this ID, get its name.
*
* @return The name of the file (un-encoded) if it exists, or
* <code>null</code> if it does not.
*/
String getFilename(String id) throws IOException;
/**
* Get the contents of the file with this ID and this filename.
*
* @throws FileNotFoundException
* if there is no file that matches this ID and filename.
*/
byte[] getfile(String id, String filename) throws FileNotFoundException,
IOException;
/**
* If a file exists with this ID, it will be deleted, regardless of the file
* name. If no such file exists, no action is taken, no exception is thrown.
*
* @return <code>true<code> if a file existed, <code>false</code> otherwise.
*/
boolean deleteFile(String id) throws IOException;
}

View file

@ -0,0 +1,56 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.filestorage;
import java.io.IOException;
/**
* Create an instance of {@link FileStorage} -- either the default
* implementation, or one specified by a system property.
*/
public class FileStorageFactory {
/**
* If this system property is set, it will be taken as the name of the
* implementing class.
*/
public static final String PROPERTY_IMPLEMETATION_CLASSNAME = FileStorage.class
.getName();
/**
* <p>
* Get an instance of {@link FileStorage}. By default, this will be an
* instance of {@link FileStorageImpl}.
* </p>
* <p>
* If the System Property named by
* {#SYSTEM_PROPERTY_IMPLEMETATION_CLASSNAME} is set, it must contain the
* name of the implementation class, which must be a sub-class of
* {@link FileStorage}, and must have a public, no-argument constructor.
* </p>
*/
public static FileStorage getFileStorage() throws IOException {
String className = System.getProperty(PROPERTY_IMPLEMETATION_CLASSNAME);
if (className == null) {
return new FileStorageImpl();
}
try {
Class<?> clazz = Class.forName(className);
Object instance = clazz.newInstance();
return FileStorage.class.cast(instance);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(
"Can't create a FileStorage instance", e);
} catch (ClassCastException e) {
throw new IllegalArgumentException(
"Can't create a FileStorage instance", e);
} catch (InstantiationException e) {
throw new IllegalArgumentException(
"Can't create a FileStorage instance", e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(
"Can't create a FileStorage instance", e);
}
}
}

View file

@ -0,0 +1,60 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.filestorage;
import java.io.File;
/**
* TODO
*/
public class FileStorageHelper {
/**
* @param id
* @return
*/
public static String id2Path(String id) {
// TODO Auto-generated method stub
throw new RuntimeException("FileStorageHelper.id2Path() not implemented.");
}
/**
* @param filename
* @return
*/
public static String encodeName(String filename) {
// TODO Auto-generated method stub
throw new RuntimeException("FileStorageHelper.encodeName() not implemented.");
}
/**
* @param rootDir
* @param id
* @param filename
* @return
*/
public static File getFullPath(File rootDir, String id, String filename) {
// TODO Auto-generated method stub
throw new RuntimeException("FileStorageHelper.getFullPath() not implemented.");
}
/**
* @param rootDir
* @param id
* @return
*/
public static File getPathToIdDirectory(File rootDir, String id) {
// TODO Auto-generated method stub
throw new RuntimeException("FileStorageHelper.getPathToIdDirectory() not implemented.");
}
/**
* @param name
* @return
*/
public static String decodeName(String name) {
// TODO Auto-generated method stub
throw new RuntimeException("FileStorageHelper.decodeName() not implemented.");
}
}

View file

@ -0,0 +1,359 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.filestorage;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Map.Entry;
import edu.cornell.mannlib.vitro.webapp.ConfigurationProperties;
/**
* The default implementation of {@link FileStorage}.
*/
public class FileStorageImpl implements FileStorage {
private final File baseDir;
private final File rootDir;
private final File namespaceFile;
private final Map<Character, String> namespacesMap;
// ----------------------------------------------------------------------
// Constructors and helper methods.
// ----------------------------------------------------------------------
/**
* Use the configuration properties to create an instance.
*
* @throws IllegalArgumentException
* if the configuration property for the base directory is
* missing, or if it doesn't point to an existing, writeable
* directory.
* @throws IllegalArgumentException
* if the configuration property for the default namespace is
* missing, or if it isn't in the expected form.
*/
FileStorageImpl() throws IOException {
this(figureBaseDir(), figureFileNamespace());
}
/**
* Use the arguments to create an instance. If the base directory is empty,
* initialize it. Otherwise, check that it was initialized to the same
* namespaces.
*
* @throws IllegalArgumentException
* if the configuration property doesn't point to an existing,
* writeable directory.
*/
FileStorageImpl(File baseDir, Collection<String> namespaces)
throws IOException {
checkBaseDirValid(baseDir);
checkNamespacesValid(namespaces);
this.baseDir = baseDir;
this.rootDir = new File(baseDir, "file_storage_root");
this.namespaceFile = new File(baseDir,
"file_storage_namespaces.properties");
if (rootDir.exists() && namespaceFile.exists()) {
this.namespacesMap = confirmNamespaces(namespaces);
} else if (!rootDir.exists() && !namespaceFile.exists()) {
this.namespacesMap = mapNamespaces(namespaces);
initializeStorage();
} else if (rootDir.exists()) {
throw new IllegalStateException(
"Storage directory '' has been partially initialized. '"
+ rootDir.getPath() + "' exists, but '"
+ namespaceFile.getPath() + "' does not.");
} else {
throw new IllegalStateException(
"Storage directory '' has been partially initialized. '"
+ namespaceFile.getPath() + "' exists, but '"
+ rootDir.getPath() + "' does not.");
}
}
private void checkNamespacesValid(Collection<String> namespaces) {
if (namespaces == null) {
throw new NullPointerException("namespaces may not be null.");
}
}
/**
* 'baseDir' must point to an existing, writeable directory.
*/
private void checkBaseDirValid(File baseDir) {
if (baseDir == null) {
throw new NullPointerException("baseDir may not be null.");
}
if (!baseDir.exists()) {
throw new IllegalArgumentException(
"File upload directory does not exist: '"
+ baseDir.getPath() + "'");
}
if (!baseDir.isDirectory()) {
throw new IllegalArgumentException(
"File upload directory is not a directory: '"
+ baseDir.getPath() + "'");
}
if (!baseDir.canWrite()) {
throw new IllegalArgumentException(
"File upload directory is not writeable: '"
+ baseDir.getPath() + "'");
}
}
/**
* Get the configuration property for the file storage base directory, and
* check that it points to an existing, writeable directory.
*
* For use by the constructor in implementations of {@link FileStorage}.
*/
static File figureBaseDir() {
String baseDirPath = ConfigurationProperties
.getProperty(PROPERTY_FILE_STORAGE_BASE_DIR);
if (baseDirPath == null) {
throw new IllegalArgumentException(
"Configuration properties must contain a value for '"
+ PROPERTY_FILE_STORAGE_BASE_DIR + "'");
}
return new File(baseDirPath);
}
/**
* Get the configuration property for the default namespace, and derive the
* file namespace from it. The default namespace is assumed to be in this
* form: <code>http://vivo.mydomain.edu/individual/</code>
*
* For use by the constructor in implementations of {@link FileStorage}.
*
* @returns the file namespace is assumed to be in this form:
* <code>http://vivo.mydomain.edu/file/</code>
*/
static Collection<String> figureFileNamespace() {
String defaultNamespace = ConfigurationProperties
.getProperty(PROPERTY_DEFAULT_NAMESPACE);
if (defaultNamespace == null) {
throw new IllegalArgumentException(
"Configuration properties must contain a value for '"
+ PROPERTY_DEFAULT_NAMESPACE + "'");
}
String defaultSuffix = "/individual/";
String fileSuffix = "/file/";
if (!defaultNamespace.endsWith(defaultSuffix)) {
throw new IllegalArgumentException(
"Default namespace does not match the expected form: '"
+ defaultNamespace + "'");
}
int hostLength = defaultNamespace.length() - defaultSuffix.length();
String fileNamespace = defaultNamespace.substring(0, hostLength)
+ fileSuffix;
return Collections.singleton(fileNamespace);
}
/**
* Assign arbitrary prefixes to these namespaces.
*/
private Map<Character, String> mapNamespaces(Collection<String> namespaces) {
Map<Character, String> map = new HashMap<Character, String>();
char prefixChar = 'a';
for (String namespace : namespaces) {
map.put(prefixChar, namespace);
prefixChar++;
if (prefixChar > 'z') {
throw new IllegalArgumentException(
"Can't handle more than 26 namespaces.");
}
}
return map;
}
/**
* Create the root directory and the namespaces file. Write the namespaces
* map to the namespaces file.
*/
private void initializeStorage() throws IOException {
boolean created = this.rootDir.mkdir();
if (!created) {
throw new IOException("Failed to create root directory '"
+ this.rootDir + "'");
}
PrintWriter writer = null;
try {
writer = new PrintWriter(this.namespaceFile);
for (Entry<Character, String> entry : this.namespacesMap.entrySet()) {
writer.println(entry.getKey() + " = " + entry.getValue());
}
} finally {
if (writer != null) {
writer.close();
}
}
}
/**
* Confirm that the namespaces file contains mappings for these namespaces,
* and only these namespaces.
*/
private Map<Character, String> confirmNamespaces(
Collection<String> namespaces) throws IOException {
Map<Character, String> map;
try {
Properties props = new Properties();
props.load(new FileReader(this.namespaceFile));
map = new HashMap<Character, String>();
for (Object key : props.keySet()) {
char keyChar = key.toString().charAt(0);
map.put(keyChar, (String) props.get(key));
}
} catch (Exception e) {
throw new IOException("Problem loading the namespace file.");
}
Set<String> requestedNamespaces = new HashSet<String>(namespaces);
Set<String> previousNamespaces = new HashSet<String>(map.values());
if (!requestedNamespaces.equals(previousNamespaces)) {
throw new IllegalStateException(
"File storage was previously initialized with a "
+ "different set of namespaces than are found "
+ "in the current request. Previous: "
+ previousNamespaces + ", Requested: "
+ requestedNamespaces);
}
return map;
}
// ----------------------------------------------------------------------
// Public methods
// ----------------------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public void createFile(String id, String filename, InputStream bytes)
throws FileAlreadyExistsException, IOException {
String existingFilename = getFilename(id);
if ((existingFilename != null) && (!filename.equals(existingFilename))) {
throw new FileAlreadyExistsException(id, existingFilename, filename);
}
File file = FileStorageHelper.getFullPath(this.rootDir, id, filename);
OutputStream out = null;
try {
out = new BufferedOutputStream(new FileOutputStream(file));
InputStream in = new BufferedInputStream(bytes);
byte[] buffer = new byte[4096];
int howMany;
while (-1 != (howMany = in.read(buffer))) {
out.write(buffer, 0, howMany);
}
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean deleteFile(String id) throws IOException {
String existingFilename = getFilename(id);
if (existingFilename == null) {
return false;
}
File file = FileStorageHelper.getFullPath(this.rootDir, id,
existingFilename);
file.delete();
if (file.exists()) {
throw new IOException("Failed to delete file with ID '" + id
+ "', file location '" + file + "'");
}
return true;
}
/**
* {@inheritDoc}
* <p>
* For a non-null result, a directory must exist for the ID, and it must
* contain a file (it may or may not contain other directories).
* </p>
*/
@Override
public String getFilename(String id) throws IOException {
File dir = FileStorageHelper.getPathToIdDirectory(this.rootDir, id);
if ((!dir.exists()) || (!dir.isDirectory())) {
return null;
}
File[] files = dir.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return pathname.isFile();
}
});
if (files.length == 0) {
return null;
}
if (files.length > 1) {
throw new IllegalStateException(
"More than one file associated with ID: '" + id
+ "', directory location '" + dir + "'");
}
return FileStorageHelper.decodeName(files[0].getName());
}
/**
* {@inheritDoc}
*/
@Override
public byte[] getfile(String id, String filename)
throws FileNotFoundException, IOException {
// gets the bytes from the file
// throws FileNotFoundException if the file does not exist
// throws IOException
// TODO Auto-generated method stub
throw new RuntimeException("FileStorage.getfile() not implemented.");
}
}

View file

@ -0,0 +1,108 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
/**
* <p>
* The code in this package implements the Vitro file-storage system.
* </p>
*
* <h1>Relationship to PairTree</h1>
*
* <p>
* The system incorporates a number of ideas from the PairTree specification,
* <ul>
* <li>
* The basic pairtree algorithm
* mapping an encoded identifier string into a filesystem directory path.
* </li>
* <li>
* Identifier string cleaning
* encoding identifiers in a two-step process so that all illegal
* characters are eliminated, but some commonly-used
* illegal characters are handled by simple substitution.
* Actually, it becomes a three-step process because namespaces are
* invoved.
* </li>
* </ul>
* but is different in several respects:
* <ul>
* <li>
* Each "object" will consist only of a single file,
* causing the entire issue of object encapsulation to be moot.
* </li>
* <li>
* Filenames will be cleaned in the same manner as identifiers,
* guarding against illegal characters in filenames.
* </li>
* <li>
* Character encoding will include backslashes,
* for compatibility with Windows.
* </li>
* <li>
* Character encoding will include tildes, to allow for "namespaces".
* </li>
* <li>
* A namespace/prefix capability will be used to shorten file paths,
* but with more flexibility than the prefix algorithm given in the specification.
* </li>
* </ul>
* </p>
*
* <h1>Directory structure</h1>
*
* <p>
* A typical structure would look like this:
* <pre>
* + basedir
* |
* +--+ file_storage_namespaces.properties
* |
* +--+ file_storage_root
* </pre>
* The <code>file_storage_root</code> directory contains the subdirectories
* that implement the encoded IDs, and the final directory for each ID will
* contain a single file that corresponds to that ID.
* </p>
*
* <h1>Namespaces</h1>
*
* <p>
* To reduce the length of the file paths, the system will can be initialized
* to recognize certain sets of characters (loosely termed "namespaces") and
* to replace them with a given prefix and separator character during ID
* encoding.
* </p>
* <p>
* For example, the sytem might be initialized with a "namespace" of
* "http://vivo.mydomain.edu/file/". If that is the only namespace, it will
* be internally assigned a prefix of "a", so a URI like this:
* <pre>http://vivo.mydomain.edu/file/n3424/myPhoto.jpg</pre>
* would be converted to this:
* <pre>a~n3424/myPhoto.jpg</pre>
* </p>
* <p>
* The namespaces and their assigned prefixes are stored in a properties file
* when the structure is initialized. When the structure is re-opened, the
* file is read to find the correct prefixes. The file
* might look like this:
* <pre>
* a = http://the.first.namespace/
* b = http://the.second.namespace/
* </pre>
* </p>
*
* <h1>ID encoding</h1>
*
* <p>
* </p>
*
* <h1>Filename encoding</h1>
*
* <p>
* The name of the file is encoded as needed to guard against illegal
* characters for the filesystem, but in practice we expect little encoding
* to be required, since few files are named with the special characters.
* </p>
*
*/
package edu.cornell.mannlib.vitro.webapp.utils.filestorage;