From 544432acc137539beb632d9a1fbb63b0519820bf Mon Sep 17 00:00:00 2001 From: jeb228 Date: Mon, 24 May 2010 15:20:31 +0000 Subject: [PATCH] NIHVIVO-160 Complete the FileStorageHelper, with tests. --- .../webapp/utils/filestorage/FileStorage.java | 13 +- .../utils/filestorage/FileStorageHelper.java | 310 ++++++++++++++++-- .../utils/filestorage/FileStorageImpl.java | 90 ++++- .../InvalidCharacterException.java | 27 ++ .../filestorage/InvalidPathException.java | 27 ++ .../utils/filestorage/package-info.java | 62 +++- .../filestorage/FileStorageFactoryTest.java | 28 +- .../filestorage/FileStorageHelperTest.java | 239 ++++++++++++++ .../filestorage/FileStorageImplTest.java | 71 +++- 9 files changed, 807 insertions(+), 60 deletions(-) create mode 100644 webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/InvalidCharacterException.java create mode 100644 webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/InvalidPathException.java create mode 100644 webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageHelperTest.java diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorage.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorage.java index a9faebcbc..6cb49129a 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorage.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorage.java @@ -17,6 +17,12 @@ public interface FileStorage { * {@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 maximum permissible file size. + */ + String PROPERTY_FILE_MAXIMUM_SIZE = "file.maximum.size"; /** * The default implementation will use this key to ask @@ -24,6 +30,11 @@ public interface FileStorage { */ String PROPERTY_DEFAULT_NAMESPACE = "Vitro.defaultNamespace"; + /** + * How often to we insert path separator characters? + */ + int SHORTY_LENGTH = 3; + /** * Store the bytes from this stream as a file with the specified ID and * filename. If the file already exists, it is over-written. @@ -50,7 +61,7 @@ public interface FileStorage { * @throws FileNotFoundException * if there is no file that matches this ID and filename. */ - byte[] getfile(String id, String filename) throws FileNotFoundException, + byte[] getFile(String id, String filename) throws FileNotFoundException, IOException; /** diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageHelper.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageHelper.java index c05d42bf1..0fe4897cf 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageHelper.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageHelper.java @@ -2,59 +2,305 @@ package edu.cornell.mannlib.vitro.webapp.utils.filestorage; +import static edu.cornell.mannlib.vitro.webapp.utils.filestorage.FileStorage.SHORTY_LENGTH; + import java.io.File; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.log4j.Logger; /** - * TODO + * A collection of utility routines used by the file storage system. Routines + * exist to: + * */ public class FileStorageHelper { + private static final Logger LOG = Logger.getLogger(FileStorageHelper.class); + + public static final char HEX_ESCAPE_CHAR = '^'; + + public static final String HEX_ENCODE_SOURCES = "\"*+,<=>?^|\\~"; + + public static final char[] PATH_SINGLE_CHARACTER_SOURCES = new char[] { + '/', ':', '.' }; + public static final char[] PATH_SINGLE_CHARACTER_TARGETS = new char[] { + '=', '+', ',' }; + + /** Same as for path, except that a period is not translated. */ + public static final char[] NAME_SINGLE_CHARACTER_SOURCES = new char[] { + '/', ':' }; + /** Same as for path, except that a period is not translated. */ + public static final char[] NAME_SINGLE_CHARACTER_TARGETS = new char[] { + '=', '+' }; /** - * @param id - * @return - */ - public static String id2Path(String id) { - // TODO Auto-generated method stub - throw new RuntimeException("FileStorageHelper.id2Path() not implemented."); - } - - /** - * @param filename - * @return + * Encode the filename as needed to guard against illegal characters. + * + * @see edu.cornell.mannlib.vitro.webapp.utils.filestorage */ public static String encodeName(String filename) { - // TODO Auto-generated method stub - throw new RuntimeException("FileStorageHelper.encodeName() not implemented."); + String hexed = addHexEncoding(filename); + return addSingleCharacterConversions(hexed, + NAME_SINGLE_CHARACTER_SOURCES, NAME_SINGLE_CHARACTER_TARGETS); } /** - * @param rootDir - * @param id - * @param filename - * @return + * Encode special characters to hex sequences. */ - public static File getFullPath(File rootDir, String id, String filename) { - // TODO Auto-generated method stub - throw new RuntimeException("FileStorageHelper.getFullPath() not implemented."); + private static String addHexEncoding(String clear) { + for (int i = 0; i < clear.length(); i++) { + char c = clear.charAt(i); + if (c > 255) { + throw new InvalidCharacterException(c, i, clear); + } + } + + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < clear.length(); i++) { + result.append(hexEncodeCharacter(clear.charAt(i))); + } + + LOG.debug("Add hex encodings to '" + clear + "' giving '" + result + + "'"); + return result.toString(); } /** - * @param rootDir - * @param id - * @return + * Create a string holding either the character or its hex-encoding. */ - public static File getPathToIdDirectory(File rootDir, String id) { - // TODO Auto-generated method stub - throw new RuntimeException("FileStorageHelper.getPathToIdDirectory() not implemented."); + private static String hexEncodeCharacter(char c) { + if ((c < 0x21) || (c > 0x7e) || (HEX_ENCODE_SOURCES.indexOf(c) >= 0)) { + return new StringBuilder().append(HEX_ESCAPE_CHAR).append( + toHexDigit(c / 16)).append(toHexDigit(c % 16)).toString(); + + } else { + return Character.toString(c); + } } /** - * @param name - * @return + * Return the correct hex character for this integer value. */ - public static String decodeName(String name) { - // TODO Auto-generated method stub - throw new RuntimeException("FileStorageHelper.decodeName() not implemented."); + private static char toHexDigit(int i) { + return "0123456789abcdef".charAt(i); + } + + /** + * Perform common single-character substitutions. + */ + private static String addSingleCharacterConversions(String encoded, + char[] sources, char[] targets) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < encoded.length(); i++) { + char c = encoded.charAt(i); + result.append(translateSingleCharacter(c, sources, targets)); + } + LOG.debug("Add single character conversions to '" + encoded + + "' giving '" + result + "'"); + return result.toString(); + } + + /** + * If a character found in the "from" set, return its corresponding + * character from the "to" set. Otherwise, return the character itself. + */ + private static char translateSingleCharacter(char c, char[] from, char[] to) { + for (int j = 0; j < from.length; j++) { + if (c == from[j]) { + return to[j]; + } + } + return c; + } + + /** + * Restore the filename to its original form, removing the encoding. + * + * @see edu.cornell.mannlib.vitro.webapp.utils.filestorage + */ + public static String decodeName(String coded) { + String hexed = removeSingleCharacterConversions(coded, + NAME_SINGLE_CHARACTER_SOURCES, NAME_SINGLE_CHARACTER_TARGETS); + return removeHexEncoding(hexed); + } + + /** + * Convert common single-character substitutions back to their original + * values. + */ + private static String removeSingleCharacterConversions(String cleaned, + char[] sources, char[] targets) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < cleaned.length(); i++) { + char c = cleaned.charAt(i); + result.append(translateSingleCharacter(c, targets, sources)); + } + LOG.debug("Remove single character conversions from '" + cleaned + + "' giving '" + result + "'"); + return result.toString(); + } + + /** + * Convert hex-encoded characters back to their original values. + */ + private static String removeHexEncoding(String encoded) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < encoded.length(); i++) { + char c = encoded.charAt(i); + if (c == HEX_ESCAPE_CHAR) { + try { + if (i + 2 >= encoded.length()) { + throw new InvalidPathException( + "Invalid hex encoding in path: '" + encoded + + "'"); + } + String hexChars = encoded.substring(i + 1, i + 3); + int value = Integer.parseInt(hexChars, 16); + result.append((char) value); + i += 2; + } catch (NumberFormatException e) { + throw new InvalidPathException( + "Invalid hex encoding in path: '" + encoded + "'", + e); + } + } else { + result.append(c); + } + } + LOG.debug("Remove hex encodings from '" + encoded + "' giving '" + + result + "'"); + return result.toString(); + } + + /** + * Translate the object ID to a relative directory path. A recognized + * namespace is translated to its prefix, and illegal characters are + * encoded. The resulting string is broken up into 3-character directory + * names (or less). + * + * @see edu.cornell.mannlib.vitro.webapp.utils.filestorage + */ + public static String id2Path(String id, Map namespacesMap) { + char prefix = 0; + String localName = id; + for (Entry entry : namespacesMap.entrySet()) { + String namespace = entry.getValue(); + if (id.startsWith(namespace)) { + prefix = entry.getKey(); + localName = id.substring(namespace.length()); + break; + } + } + + String hexed = addHexEncoding(localName); + String cleaned = addSingleCharacterConversions(hexed, + PATH_SINGLE_CHARACTER_SOURCES, PATH_SINGLE_CHARACTER_TARGETS); + String prefixed = applyPrefixChar(prefix, cleaned); + return insertPathDelimiters(prefixed); + } + + /** + * Now that the cleaning is complete, add the prefix if there is one. + */ + private static String applyPrefixChar(char prefix, String cleaned) { + if (prefix == 0) { + return cleaned; + } else { + return prefix + "~" + cleaned; + } + } + + /** + * Add path delimiters as needed to turn the cleaned prefixed string into a + * relative path. + */ + private static String insertPathDelimiters(String prefixed) { + StringBuilder path = new StringBuilder(); + for (int i = 0; i < prefixed.length(); i++) { + if ((i % SHORTY_LENGTH == 0) && (i > 0)) { + path.append(File.separatorChar); + } + path.append(prefixed.charAt(i)); + } + return path.toString(); + } + + /** + * Translate the object ID and the file storage root directory into a full + * path to the directory that would represent that ID. + * + * @see edu.cornell.mannlib.vitro.webapp.utils.filestorage + */ + public static File getPathToIdDirectory(String id, + Map namespacesMap, File rootDir) { + return new File(rootDir, id2Path(id, namespacesMap)); + } + + /** + * Translate the object ID, the file storage root directory and the filename + * into a full path to where the file would be stored. + * + * @see edu.cornell.mannlib.vitro.webapp.utils.filestorage + */ + public static File getFullPath(File rootDir, String id, String filename, + Map namespacesMap) { + return new File(getPathToIdDirectory(id, namespacesMap, rootDir), + encodeName(filename)); + } + + /** + * Translate the configuration property for maximum file size from a + * String to a long. + * + * The string must be represent a positive integer, optionally followed by + * "K", "M", or "G" (to indicate kilobytes, megabytes, or gigabytes). + */ + public static long parseMaximumFileSize(String fileSizeString) { + long factor = 1L; + String integerString; + int shorter = fileSizeString.length() - 1; + if (fileSizeString.endsWith("K")) { + factor = 1024L; + integerString = fileSizeString.substring(0, shorter); + } else if (fileSizeString.endsWith("M")) { + factor = 1024L * 1024L; + integerString = fileSizeString.substring(0, shorter); + } else if (fileSizeString.endsWith("G")) { + factor = 1024L * 1024L * 1024L; + integerString = fileSizeString.substring(0, shorter); + } else { + integerString = fileSizeString; + } + + long value = 0; + try { + value = Long.parseLong(integerString); + + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Maximum file size is invalid: '" + fileSizeString + + "'. Must be a positive integer, " + + "optionally followed by 'K', 'M', or 'G'"); + } + + if (value <= 0L) { + throw new IllegalArgumentException( + "Maximum file size must be more than 0: '" + fileSizeString + + "'"); + } + + return value * factor; } } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageImpl.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageImpl.java index 98787606c..7a85d03d8 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageImpl.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageImpl.java @@ -4,8 +4,10 @@ package edu.cornell.mannlib.vitro.webapp.utils.filestorage; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileFilter; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; @@ -32,6 +34,7 @@ public class FileStorageImpl implements FileStorage { private final File baseDir; private final File rootDir; private final File namespaceFile; + private final long maximumFileSize; private final Map namespacesMap; // ---------------------------------------------------------------------- @@ -50,7 +53,7 @@ public class FileStorageImpl implements FileStorage { * missing, or if it isn't in the expected form. */ FileStorageImpl() throws IOException { - this(figureBaseDir(), figureFileNamespace()); + this(figureBaseDir(), figureFileNamespace(), figureMaximumFileSize()); } /** @@ -62,17 +65,20 @@ public class FileStorageImpl implements FileStorage { * if the configuration property doesn't point to an existing, * writeable directory. */ - FileStorageImpl(File baseDir, Collection namespaces) - throws IOException { + FileStorageImpl(File baseDir, Collection namespaces, + long maximumFileSize) throws IOException { checkBaseDirValid(baseDir); checkNamespacesValid(namespaces); + checkMaximumFileSizeValid(maximumFileSize); this.baseDir = baseDir; - this.rootDir = new File(baseDir, "file_storage_root"); + this.rootDir = new File(this.baseDir, "file_storage_root"); this.namespaceFile = new File(baseDir, "file_storage_namespaces.properties"); + this.maximumFileSize = maximumFileSize; + if (rootDir.exists() && namespaceFile.exists()) { this.namespacesMap = confirmNamespaces(namespaces); } else if (!rootDir.exists() && !namespaceFile.exists()) { @@ -91,6 +97,13 @@ public class FileStorageImpl implements FileStorage { } } + private void checkMaximumFileSizeValid(long maximumFileSize) { + if (maximumFileSize < 0) { + throw new IllegalArgumentException( + "Maximum file size may not be negative."); + } + } + private void checkNamespacesValid(Collection namespaces) { if (namespaces == null) { throw new NullPointerException("namespaces may not be null."); @@ -172,6 +185,23 @@ public class FileStorageImpl implements FileStorage { return Collections.singleton(fileNamespace); } + /** + * Get the configuration property for the maximum file size and translate it + * into a long integer. It must be a positive integer, optionally followed + * by "K", "M", or "G" (to indicate kilobytes, megabytes, or gigabytes). + */ + private static long figureMaximumFileSize() { + String fileSizeString = ConfigurationProperties + .getProperty(PROPERTY_FILE_MAXIMUM_SIZE); + if (fileSizeString == null) { + throw new IllegalArgumentException( + "Configuration properties must contain a value for '" + + PROPERTY_FILE_MAXIMUM_SIZE + "'"); + } + + return FileStorageHelper.parseMaximumFileSize(fileSizeString); + } + /** * Assign arbitrary prefixes to these namespaces. */ @@ -263,7 +293,8 @@ public class FileStorageImpl implements FileStorage { } - File file = FileStorageHelper.getFullPath(this.rootDir, id, filename); + File file = FileStorageHelper.getFullPath(this.rootDir, id, filename, + this.namespacesMap); OutputStream out = null; try { @@ -297,7 +328,7 @@ public class FileStorageImpl implements FileStorage { } File file = FileStorageHelper.getFullPath(this.rootDir, id, - existingFilename); + existingFilename, this.namespacesMap); file.delete(); if (file.exists()) { @@ -317,7 +348,8 @@ public class FileStorageImpl implements FileStorage { */ @Override public String getFilename(String id) throws IOException { - File dir = FileStorageHelper.getPathToIdDirectory(this.rootDir, id); + File dir = FileStorageHelper.getPathToIdDirectory(id, + this.namespacesMap, this.rootDir); if ((!dir.exists()) || (!dir.isDirectory())) { return null; @@ -344,16 +376,44 @@ public class FileStorageImpl implements FileStorage { /** * {@inheritDoc} + * + * @throws IOException + * if the file is larger than the maximum allowable size. */ @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 + public byte[] getFile(String id, String filename) throws IOException { - // TODO Auto-generated method stub - throw new RuntimeException("FileStorage.getfile() not implemented."); + File file = FileStorageHelper.getFullPath(this.rootDir, id, filename, + this.namespacesMap); + + if (!file.exists()) { + throw new FileNotFoundException("No file exists with ID '" + id + + "', file location '" + file + "'"); + } + + InputStream in = null; + try { + in = new BufferedInputStream(new FileInputStream(file)); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int howMany; + while (-1 != (howMany = in.read(buffer))) { + if (bytes.size() > this.maximumFileSize) { + throw new IOException("File is too large at this ID: '" + + id + "', file location '" + file + "'"); + } + bytes.write(buffer, 0, howMany); + } + bytes.close(); + return bytes.toByteArray(); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } } - } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/InvalidCharacterException.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/InvalidCharacterException.java new file mode 100644 index 000000000..7e1cf2cd4 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/InvalidCharacterException.java @@ -0,0 +1,27 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.filestorage; + +/** + * Indicates that an object ID contains an invalid character. + */ +public class InvalidCharacterException extends RuntimeException { + private final char invalid; + private final int position; + private final String context; + + + public InvalidCharacterException(char invalid, int position, String context) { + this.invalid = invalid; + this.position = position; + this.context = context; + } + + + @Override + public String getMessage() { + return String.format( + "Invalid character '%1$c'(0x%1$x) at position %2$d in '%3$s'", + (int)invalid, position, context); + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/InvalidPathException.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/InvalidPathException.java new file mode 100644 index 000000000..05138a30e --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/InvalidPathException.java @@ -0,0 +1,27 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.filestorage; + +/** + * Indicates a PairTree path ("ppath" or "relative path") that is not correctly + * formed, and cannot be converted to an object ID. + */ +public class InvalidPathException extends RuntimeException { + + public InvalidPathException() { + super(); + } + + public InvalidPathException(String message) { + super(message); + } + + public InvalidPathException(Throwable cause) { + super(cause); + } + + public InvalidPathException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/package-info.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/package-info.java index 377839531..e1c1ddfa7 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/package-info.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/filestorage/package-info.java @@ -44,6 +44,9 @@ * A namespace/prefix capability will be used to shorten file paths, * but with more flexibility than the prefix algorithm given in the specification. * + *
  • + * "shorty" directory names may be up to 3 characters long, not 2. + *
  • * *

    * @@ -91,8 +94,61 @@ *

    * *

    ID encoding

    - * + * *

    + * This is a multi-step process: + *

      + *
    • + * Namespace recognition - + * If the ID begins with a recognized namespace, then that namespace is + * stripped from the ID, and the prefix associated with that namespace + * is set aside for later in the process. + *
    • + *
    • + * Rare character encoding - + * Illegal characters are translated to their hexadecimal equivalents, + * as are some rarely used characters which will be given other + * purposes later in the process. The translated characters include any + * octet outside of the visible ASCII range (21-7e), and these additional + * characters: + *
       " * + , < = > ? ^ | \ ~ 
      + * The hexadecimal encoding consists of a caret followed by 2 hex digits, + * e.g.: ^7C + *
    • + *
    • + * Common character encoding - + * To keep the file paths short and readable, characters that are used + * commonly in IDs but may be illegal in the file system are translated + * to a single, lesser-used character. + *
        + *
      • / becomes =
      • + *
      • : becomes +
      • + *
      • . becomes ,
      • + *
      + *
    • + *
    • + * Prefixing - + * If a namespace was recognized on the ID in the first step, the + * associated prefix letter will be prepended to the string, with a + * tilde separator. + *
    • + *
    • + * Path breakdown - + * Finally, path separator characters are inserted after every third + * character in the processed ID string. + *
    • + *
    + * Examples: + *
    ark:/13030/xt12t3 becomes + * ark/+=1/303/0=x/t12/t3 + *
    http://n2t.info/urn:nbn:se:kb:repos-1 becomes + * htt/p+=/=n2/t,i/nfo/=ur/n+n/bn+/se+/kb+/rep/os-/1 + *
    what-the-*@?#!^!~? becomes + * wha/t-t/he-/^2a/@^3/f#!/^5e/!^7/e^3/f + *
    http://vivo.myDomain.edu/file/n3424 with namespace + * http://vivo.myDomain.edu/file/ and prefix + * a becomes + * a~n/342/4 *

    * *

    Filename encoding

    @@ -103,6 +159,10 @@ * to be required, since few files are named with the special characters. *

    * + *

    + * The encoding process is the same as the "rare character encoding" and + * "common character encoding" steps used for ID encoding. + *

    */ package edu.cornell.mannlib.vitro.webapp.utils.filestorage; diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageFactoryTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageFactoryTest.java index 578084ab8..5fafa7375 100644 --- a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageFactoryTest.java +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageFactoryTest.java @@ -3,6 +3,7 @@ package edu.cornell.mannlib.vitro.webapp.utils.filestorage; import static edu.cornell.mannlib.vitro.webapp.utils.filestorage.FileStorageFactory.PROPERTY_IMPLEMETATION_CLASSNAME; +import static org.junit.Assert.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -28,7 +29,8 @@ import edu.cornell.mannlib.vitro.testing.AbstractTestClass; import edu.cornell.mannlib.vitro.webapp.ConfigurationProperties; /** - * TODO + * This just checks the interaction between the configuration properties, the + * system properties, and the implementation of {@link FileStorage}. */ public class FileStorageFactoryTest extends AbstractTestClass { private static final String configProperties = "#mock config properties file\n"; @@ -62,7 +64,7 @@ public class FileStorageFactoryTest extends AbstractTestClass { @Test public void createDefaultImplementation() throws IOException { setConfigurationProperties(tempDir.getPath(), - "http://vivo.myDomain.edu/individual/"); + "http://vivo.myDomain.edu/individual/", "50M"); FileStorage fs = FileStorageFactory.getFileStorage(); assertEquals("implementation class", FileStorageImpl.class, fs .getClass()); @@ -80,13 +82,20 @@ public class FileStorageFactoryTest extends AbstractTestClass { @Test(expected = IllegalArgumentException.class) public void baseDirectoryDoesntExist() throws IOException { setConfigurationProperties("/bogus/Directory", - "http://vivo.myDomain.edu/individual/"); + "http://vivo.myDomain.edu/individual/", "50M"); FileStorageFactory.getFileStorage(); } @Test(expected = IllegalArgumentException.class) public void defaultNamespaceIsBogus() throws IOException { - setConfigurationProperties(tempDir.getPath(), "namespace"); + setConfigurationProperties(tempDir.getPath(), "namespace", "50M"); + FileStorageFactory.getFileStorage(); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidMaximumFileSize() throws IOException { + setConfigurationProperties(tempDir.getPath(), + "http://vivo.myDomain.edu/individual/", "50X"); FileStorageFactory.getFileStorage(); } @@ -115,10 +124,11 @@ public class FileStorageFactoryTest extends AbstractTestClass { // ---------------------------------------------------------------------- private void setConfigurationProperties(String baseDir, - String defaultNamespace) { + String defaultNamespace, String maxFileSize) { Map map = new HashMap(); - map.put("upload.directory", baseDir); - map.put("Vitro.defaultNamespace", defaultNamespace); + map.put(FileStorage.PROPERTY_FILE_STORAGE_BASE_DIR, baseDir); + map.put(FileStorage.PROPERTY_DEFAULT_NAMESPACE, defaultNamespace); + map.put(FileStorage.PROPERTY_FILE_MAXIMUM_SIZE, maxFileSize); try { Field f = ConfigurationProperties.class.getDeclaredField("theMap"); @@ -147,7 +157,7 @@ public class FileStorageFactoryTest extends AbstractTestClass { return "filename"; } - public byte[] getfile(String id, String filename) + public byte[] getFile(String id, String filename) throws FileNotFoundException, IOException { return new byte[0]; } @@ -171,7 +181,7 @@ public class FileStorageFactoryTest extends AbstractTestClass { return "filename"; } - public byte[] getfile(String id, String filename) + public byte[] getFile(String id, String filename) throws FileNotFoundException, IOException { return new byte[0]; } diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageHelperTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageHelperTest.java new file mode 100644 index 000000000..d75350dd9 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageHelperTest.java @@ -0,0 +1,239 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.filestorage; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +/** + * + */ +public class FileStorageHelperTest { + private static String RAW_NAME_1 = "simpleName"; + private static String ENCODED_NAME_1 = "simpleName"; + private static String RAW_NAME_2 = "common:/Chars.pdf"; + private static String ENCODED_NAME_2 = "common+=Chars.pdf"; + private static String RAW_NAME_3 = "rare\"+~chars"; + private static String ENCODED_NAME_3 = "rare^22^2b^7echars"; + private static String RAW_NAME_4 = "combination+ EMPTY_NAMESPACES = Collections + .emptyMap(); + private static Map NAMESPACES = initPrefixMap(); + + private static Map initPrefixMap() { + Map map = new HashMap(); + map.put('a', "junk"); + map.put('b', "http://vivo.myDomain.edu/file/"); + return map; + } + + // ---------------------------------------------------------------------- + // encodeName + // ---------------------------------------------------------------------- + + @Test + public void encodeName1() { + assertNameEncoding(RAW_NAME_1, ENCODED_NAME_1); + } + + @Test + public void encodeName2() { + assertNameEncoding(RAW_NAME_2, ENCODED_NAME_2); + } + + @Test + public void encodeName3() { + assertNameEncoding(RAW_NAME_3, ENCODED_NAME_3); + } + + @Test + public void encodeName4() { + assertNameEncoding(RAW_NAME_4, ENCODED_NAME_4); + } + + @Test + public void encodeName5() { + assertNameEncoding(RAW_NAME_5, ENCODED_NAME_5); + } + + @Test(expected = InvalidCharacterException.class) + public void encodeName6() { + FileStorageHelper.encodeName(RAW_NAME_6); + } + + private void assertNameEncoding(String rawName, String expected) { + String encoded = FileStorageHelper.encodeName(rawName); + assertEquals("encoded name", expected, encoded); + } + + // ---------------------------------------------------------------------- + // decodeName + // ---------------------------------------------------------------------- + + @Test + public void decodeName1() { + assertNameDecoding(ENCODED_NAME_1, RAW_NAME_1); + } + + @Test + public void decodeName2() { + assertNameDecoding(ENCODED_NAME_2, RAW_NAME_2); + } + + @Test + public void decodeName3() { + assertNameDecoding(ENCODED_NAME_3, RAW_NAME_3); + } + + @Test + public void decodeName4() { + assertNameDecoding(ENCODED_NAME_4, RAW_NAME_4); + } + + @Test + public void decodeName5() { + assertNameDecoding(ENCODED_NAME_5, RAW_NAME_5); + } + + private void assertNameDecoding(String encodedName, String expected) { + String decoded = FileStorageHelper.decodeName(encodedName); + assertEquals("decodedName", expected, decoded); + } + + // ---------------------------------------------------------------------- + // idToPath + // ---------------------------------------------------------------------- + + @Test + public void idToPath1() { + assertIdToPath(ID_1, EMPTY_NAMESPACES, RELATIVE_PATH_1); + } + + @Test + public void idToPath2() { + assertIdToPath(ID_2, EMPTY_NAMESPACES, RELATIVE_PATH_2); + } + + @Test + public void idToPath3() { + assertIdToPath(ID_3, EMPTY_NAMESPACES, RELATIVE_PATH_3); + } + + @Test + public void idToPath3WithNamespace() { + assertIdToPath(ID_3, NAMESPACES, RELATIVE_PREFIXED_PATH_3); + } + + private void assertIdToPath(String id, Map namespaces, + String expected) { + String adjustedExpected = expected.replace('/', File.separatorChar); + String relativePath = FileStorageHelper.id2Path(id, namespaces); + assertEquals("idToPath", adjustedExpected, relativePath); + } + + // ---------------------------------------------------------------------- + // getPathToIdDirectory + // ---------------------------------------------------------------------- + + @Test + public void getPathToIdDirectory1() { + assertPathToIdDirectory(ID_1, EMPTY_NAMESPACES, ROOT_DIR_1, + ABSOLUTE_PATH_1); + } + + @Test + public void getPathToIdDirectory2() { + assertPathToIdDirectory(ID_1, EMPTY_NAMESPACES, ROOT_DIR_2, + ABSOLUTE_PATH_2); + } + + private void assertPathToIdDirectory(String id, + Map namespaces, File rootDir, File expected) { + File actual = FileStorageHelper.getPathToIdDirectory(id, namespaces, + rootDir); + File adjustedExpected = new File(expected.getPath().replace('/', + File.separatorChar)); + assertEquals("pathToIdDirectory", adjustedExpected, actual); + } + + // ---------------------------------------------------------------------- + // getFullPath + // ---------------------------------------------------------------------- + + @Test + public void getFullPath() { + File actual = FileStorageHelper.getFullPath(FULL_ROOT, FULL_ID, + FULL_NAME, NAMESPACES); + assertEquals("fullPath", FULL_RESULT_PATH, actual); + } + + // ---------------------------------------------------------------------- + // parseMaximumFileSize + // ---------------------------------------------------------------------- + + @Test + public void parseMaximumFileSizeBare() { + long size = FileStorageHelper.parseMaximumFileSize("1467898"); + assertEquals("", 1467898, size); + } + + @Test + public void parseMaximumFileSizeWithSuffixes() { + long size = FileStorageHelper.parseMaximumFileSize("152K"); + assertEquals("", 152L * 1024L, size); + + size = FileStorageHelper.parseMaximumFileSize("47M"); + assertEquals("", 47L * 1024L * 1024L, size); + + size = FileStorageHelper.parseMaximumFileSize("3G"); + assertEquals("", 3L * 1024L * 1024L * 1024L, size); + } + + @Test(expected = IllegalArgumentException.class) + public void parseMaximumFileSizeInvalidSuffix() { + FileStorageHelper.parseMaximumFileSize("152X"); + } + + @Test(expected = IllegalArgumentException.class) + public void parseMaximumFileSizeNegativeNumber() { + FileStorageHelper.parseMaximumFileSize("-3K"); + } + + @Test(expected = IllegalArgumentException.class) + public void parseMaximumFileSizeEmbeddedBadCharacter() { + FileStorageHelper.parseMaximumFileSize("1G52K"); + } + +} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageImplTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageImplTest.java index 91dad1f32..701ff00ca 100644 --- a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageImplTest.java +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/filestorage/FileStorageImplTest.java @@ -2,16 +2,18 @@ package edu.cornell.mannlib.vitro.webapp.utils.filestorage; -import static org.junit.Assert.*; +import static org.junit.Assert.fail; import org.junit.Ignore; import org.junit.Test; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; + /** * Test the FileStorage methods. The zero-argument constructor was tested in * {@link FileStorageFactoryTest}. */ -public class FileStorageImplTest { +public class FileStorageImplTest extends AbstractTestClass { @Ignore @Test public void baseDirDoesntExist() { @@ -48,4 +50,69 @@ public class FileStorageImplTest { fail("initializedNamespacesDontMatch not implemented"); } + @Ignore + @Test + public void createFileOriginal() { + fail("createFileOriginal not implemented"); + } + + @Ignore + @Test + public void createFileOverwrite() { + fail("createFileOverwrite not implemented"); + } + + @Ignore + @Test + public void createFileConflictingName() { + fail("createFileConflictingName not implemented"); + } + + @Ignore + @Test + public void getFilenameExists() { + fail("getFilenameExists not implemented"); + } + + @Ignore + @Test + public void getFilenameDoesntExist() { + fail("getFilenameDoesntExist not implemented"); + } + + @Ignore + @Test + public void getFilenameMultipleFiles() { + fail("getFilenameMultipleFiles not implemented"); + } + + @Ignore + @Test + public void getFileFound() { + fail("getFilenameFound not implemented"); + } + + @Ignore + @Test + public void getFileNotFound() { + fail("getFileNotFound not implemented"); + } + + @Ignore + @Test + public void getFileTooLarge() { + fail("getFileTooLarge not implemented"); + } + + @Ignore + @Test + public void deleteFileExists() { + fail("deleteFileExists not implemented"); + } + + @Ignore + @Test + public void deleteFileDoesntExist() { + fail("deleteFileDoesntExist not implemented"); + } }