diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileAlreadyExistsException.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileAlreadyExistsException.java new file mode 100644 index 000000000..7bd7a3c75 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileAlreadyExistsException.java @@ -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; + } + +} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorage.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorage.java new file mode 100644 index 000000000..7abd05cf3 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorage.java @@ -0,0 +1,108 @@ +/* $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. All methods are abstract except + * for the factory method. + */ +public abstract class FileStorage { + /** + * 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(); + + /** + * The default implementation will use this key to ask + * {@link ConfigurationProperties} for the file storage base directory. + */ + public static final String PROPERTY_FILE_STORAGE_BASE_DIR = "upload.directory"; + + /** + * The default implementation will use this key to ask + * {@link ConfigurationProperties} for the default URI namespace. + */ + public static final String PROPERTY_DEFAULT_NAMESPACE = "Vitro.defaultNamespace"; + + /** + *

+ * Get an instance of {@link FileStorage}. By default, this will be an + * instance of {@link FileStorageImpl}. + *

+ *

+ * 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. + *

+ */ + public static FileStorage getInstance() { + 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); + } + } + + /** + * 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. + * + */ + public abstract 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 + * null if it does not. + */ + public abstract 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. + */ + public abstract 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 true if a file existed, false otherwise. + */ + public abstract boolean deleteFile(String id) throws IOException; +} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageHelper.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageHelper.java new file mode 100644 index 000000000..6060496c6 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageHelper.java @@ -0,0 +1,10 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.filestorage; + +/** + * TODO + */ +public class FileStorageHelper { + +} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageImpl.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageImpl.java new file mode 100644 index 000000000..5ec7d243d --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageImpl.java @@ -0,0 +1,192 @@ +/* $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; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; + +import edu.cornell.mannlib.vitro.webapp.ConfigurationProperties; + +/** + * The default implementation of {@link FileStorage}. + */ +public class FileStorageImpl extends FileStorage { + + // static FileStorage getInstance() + // gets baseDir and namespaces from ConfigurationProperties + // gets instance class from system properties + // throws IllegalStateException if requires properties are missing + // throws IOException + // + // void createFile(String id, String filename, InputStream bytes) + // stores the bytes as a file with this name under this id + // if the file already exists, over-writes it + // throws FileAlreadyExistsException if a file already exists under this id + // with a different name + // throws IOException + // + // String getfilename(String id) + // returns the name of the file at this ID, or null if there is none. + // throws IOException + // + // byte[] getFile(String id, String filename) + // gets the bytes from the file + // throws FileNotFoundException if the file does not exist + // throws IOException + // + // boolean deleteFile(String id) + // removes the file at this id, returns true + // if no such file, takes no action, returns false + // throws IOException + // + // FileStorageImpl + // + + /** + * 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() { + this(figureBaseDir(), figureFileNamespace()); + } + + // package-level constructor(File baseDir, Collection namespaces), + // gets properties from arguments + // if baseDir is not initialized with file_storage_root and + // file_storage_prefixMap, do it. + // otherwise check for correctness and consistency + // throws IllegalStateException if partially initialized + // throws IllegalStateException if already initialized and namespaces don't + // match + + /** + * 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 namespaces) { + if (baseDir == null) { + throw new NullPointerException("baseDir may not be null."); + } + if (namespaces == null) { + throw new NullPointerException("namespaces 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. + */ + private 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: http://vivo.mydomain.edu/individual/ + * + * @returns the file namespace is assumed to be in this form: + * http://vivo.mydomain.edu/file/ + */ + private static Collection 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); + } + + /** + * {@inheritDoc} + */ + @Override + public void createFile(String id, String filename, InputStream bytes) + throws FileAlreadyExistsException, IOException { + // TODO Auto-generated method stub + throw new RuntimeException("FileStorage.createFile() not implemented."); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean deleteFile(String id) throws IOException { + // TODO Auto-generated method stub + throw new RuntimeException("FileStorage.deleteFile() not implemented."); + } + + /** + * {@inheritDoc} + */ + @Override + public String getFilename(String id) throws IOException { + // TODO Auto-generated method stub + throw new RuntimeException("FileStorage.getFilename() not implemented."); + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] getfile(String id, String filename) + throws FileNotFoundException, IOException { + // TODO Auto-generated method stub + throw new RuntimeException("FileStorage.getfile() not implemented."); + } + +} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/package-info.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/package-info.java new file mode 100644 index 000000000..157d6a660 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/package-info.java @@ -0,0 +1,100 @@ +/** + *

+ * The code in this package implements the Vitro file-storage system. + *

+ * + *

Relationship to PairTree

+ * + *

+ * The system incorporates a number of ideas from the PairTree specification, + *

    + *
  • + * The basic pairtree algorithm – + * mapping an encoded identifier string into a filesystem directory path. + *
  • + *
  • + * 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. + *
  • + *
+ * but is different in several respects: + *
    + *
  • + * Each “object” will consist only of a single file, + * causing the entire issue of object encapsulation to be moot. + *
  • + *
  • + * Filenames will be cleaned in the same manner as identifiers, + * guarding against illegal characters in filenames. + *
  • + *
  • + * Character encoding will include backslashes, + * for compatibility with Windows. + *
  • + *
  • + * Character encoding will include tildes, to allow for "namespaces". + *
  • + *
  • + * A namespace/prefix capability will be used to shorten file paths, + * but with more flexibility than the prefix algorithm given in the specification. + *
  • + *
+ *

+ * + *

Directory structure

+ * + *

+ * A typical structure would look like this: + *

+ *   + basedir
+ *   |
+ *   +--+ file_storage_namespaces.properties
+ *   |
+ *   +--+ file_storage_root
+ *   
+ * The file_storage_root 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. + *

+ * + *

Namespaces

+ * + *

+ * 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. + *

+ *

+ * 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: + *

http://vivo.mydomain.edu/file/n3424/myPhoto.jpg
+ * would be converted to this: + *
a~n3424/myPhoto.jpg
+ *

+ * + *

ID encoding

+ * + *

+ *

+ * + *

Filename encoding

+ * + *

+ * 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. + *

+ * + *

+ *

+ *

+ *

+ */ + +package edu.cornell.mannlib.vitro.webapp.utils.filestorage;