+ * A {@link TemplateLoader} that treats a directory and its sub-directories as a + * flat namespace. + *
+ *
+ * When a request is made to find a template source, the loader will search its
+ * base directory and any sub-directories for a file with a matching name. So a
+ * request for myFile.ftl
might return a reference to a file at
+ * base/myFile.ftl
or at base/this/myFile.ftl
+ *
+ * The order in which the sub-directories are searched is unspecified. The first + * matching file will be returned. + *
+ *
+ * A path (absolute or relative) on the source name would be meaningless, so any
+ * such path will be stripped before the search is made. That is, a request for
+ * path/file.ftl
or /absolute/path/file.ftl
is
+ * functionally identical to a request for file.ftl
+ *
+ *
+ */ +public class FlatteningTemplateLoader implements TemplateLoader { + private static final Log log = LogFactory + .getLog(FlatteningTemplateLoader.class); + + private final File baseDir; + + public FlatteningTemplateLoader(File baseDir) { + if (baseDir == null) { + throw new NullPointerException("baseDir may not be null."); + } + if (!baseDir.exists()) { + throw new IllegalArgumentException("Template directory '" + + baseDir.getAbsolutePath() + "' does not exist"); + } + if (!baseDir.isDirectory()) { + throw new IllegalArgumentException("Template directory '" + + baseDir.getAbsolutePath() + "' is not a directory"); + } + if (!baseDir.canRead()) { + throw new IllegalArgumentException("Can't read template " + + "directory '" + baseDir.getAbsolutePath() + "'"); + } + + log.debug("Created template loader - baseDir is '" + + baseDir.getAbsolutePath() + "'"); + this.baseDir = baseDir; + } + + /** + * Look for a file by this name in the base directory, or its + * subdirectories, disregarding any path information. + * + * @return a {@link File} that can be used in subsequent calls the template + * loader methods, ornull
if no template is found.
+ */
+ @Override
+ public Object findTemplateSource(String name) throws IOException {
+ if (name == null) {
+ return null;
+ }
+
+ int lastSlashHere = name.indexOf('/');
+ String trimmedName = (lastSlashHere == -1) ? name : name
+ .substring(lastSlashHere + 1);
+
+ // start the recursive search.
+ File source = findFile(trimmedName, baseDir);
+ if (source == null) {
+ log.debug("For template name '" + name
+ + "', found no template file.");
+ } else {
+ log.debug("For template name '" + name + "', template file is "
+ + source.getAbsolutePath());
+ }
+ return source;
+ }
+
+ /**
+ * Recursively search for a file of this name.
+ */
+ private File findFile(String name, File dir) {
+ for (File child : dir.listFiles()) {
+ if (child.isDirectory()) {
+ File file = findFile(name, child);
+ if (file != null) {
+ return file;
+ }
+ } else {
+ if (child.getName().equals(name)) {
+ return child;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Ask the file when it was last modified.
+ *
+ * @param templateSource
+ * a {@link File} that was obtained earlier from
+ * {@link #findTemplateSource(String)}.
+ */
+ @Override
+ public long getLastModified(Object templateSource) {
+ if (!(templateSource instanceof File)) {
+ throw new IllegalArgumentException("templateSource is not a File: "
+ + templateSource);
+ }
+
+ return ((File) templateSource).lastModified();
+ }
+
+ /**
+ * Get a {@link Reader} on this {@link File}. The framework will see that
+ * the {@link Reader} is closed when it has been read.
+ *
+ * @param templateSource
+ * a {@link File} that was obtained earlier from
+ * {@link #findTemplateSource(String)}.
+ */
+ @Override
+ public Reader getReader(Object templateSource, String encoding)
+ throws IOException {
+ if (!(templateSource instanceof File)) {
+ throw new IllegalArgumentException("templateSource is not a File: "
+ + templateSource);
+ }
+
+ return new FileReader(((File) templateSource));
+ }
+
+ /**
+ * Nothing to do here. No resources to free up.
+ *
+ * @param templateSource
+ * a {@link File} that was obtained earlier from
+ * {@link #findTemplateSource(String)}.
+ */
+ @Override
+ public void closeTemplateSource(Object templateSource) throws IOException {
+ }
+
+}
diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerHttpServlet.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerHttpServlet.java
index d307fef1f..9be84278f 100644
--- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerHttpServlet.java
+++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerHttpServlet.java
@@ -169,7 +169,7 @@ public class FreemarkerHttpServlet extends VitroHttpServlet {
try {
TemplateLoader[] loaders;
- FileTemplateLoader vitroFtl = new FileTemplateLoader(new File(vitroTemplatePath));
+ FlatteningTemplateLoader vitroFtl = new FlatteningTemplateLoader(new File(vitroTemplatePath));
ClassTemplateLoader ctl = new ClassTemplateLoader(getClass(), "");
File themeTemplateDir = new File(themeTemplatePath);
diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FlatteningTemplateLoaderTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FlatteningTemplateLoaderTest.java
new file mode 100644
index 000000000..c47046e73
--- /dev/null
+++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FlatteningTemplateLoaderTest.java
@@ -0,0 +1,222 @@
+/* $This file is distributed under the terms of the license in /doc/license.txt$ */
+
+package edu.cornell.mannlib.vitro.webapp.controller.freemarker;
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import edu.cornell.mannlib.vitro.testing.AbstractTestClass;
+
+/**
+ * Test the methods of {@link FlatteningTemplateLoader}.
+ */
+public class FlatteningTemplateLoaderTest extends AbstractTestClass {
+ /**
+ * TODO test plan
+ *
+ * + * findTemplateSource + * null arg + * not found + * found in top level + * found in lower level + * with path + * + * getReader + * get it, read it, check it, close it. + * + * getLastModified + * check the create date within a range + * modify it and check again. + * + *+ */ + // ---------------------------------------------------------------------- + // setup and teardown + // ---------------------------------------------------------------------- + + private static final String SUBDIRECTORY_NAME = "sub"; + + private static final String TEMPLATE_NAME_UPPER = "template.ftl"; + private static final String TEMPLATE_NAME_UPPER_WITH_PATH = "path/template.ftl"; + private static final String TEMPLATE_UPPER_CONTENTS = "The contents of the file."; + + private static final String TEMPLATE_NAME_LOWER = "another.ftl"; + private static final String TEMPLATE_LOWER_CONTENTS = "Another template file."; + + private static long setupTime; + private static File tempDir; + private static File notADirectory; + private static File upperTemplate; + private static File lowerTemplate; + + private FlatteningTemplateLoader loader; + + @BeforeClass + public static void setUpFiles() throws IOException { + setupTime = System.currentTimeMillis(); + + notADirectory = File.createTempFile( + FlatteningTemplateLoader.class.getSimpleName(), ""); + + tempDir = createTempDirectory(FlatteningTemplateLoader.class + .getSimpleName()); + upperTemplate = createFile(tempDir, TEMPLATE_NAME_UPPER, + TEMPLATE_UPPER_CONTENTS); + + File subdirectory = new File(tempDir, SUBDIRECTORY_NAME); + subdirectory.mkdir(); + lowerTemplate = createFile(subdirectory, TEMPLATE_NAME_LOWER, + TEMPLATE_LOWER_CONTENTS); + } + + @Before + public void initializeLoader() { + loader = new FlatteningTemplateLoader(tempDir); + } + + @AfterClass + public static void cleanUpFiles() throws IOException { + purgeDirectoryRecursively(tempDir); + } + + // ---------------------------------------------------------------------- + // the tests + // ---------------------------------------------------------------------- + + @Test(expected = NullPointerException.class) + public void constructorNull() { + new FlatteningTemplateLoader(null); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNonExistent() { + new FlatteningTemplateLoader(new File("bogusDirName")); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNotADirectory() { + new FlatteningTemplateLoader(notADirectory); + } + + @Test + public void findNull() throws IOException { + Object source = loader.findTemplateSource(null); + assertNull("find null", source); + } + + @Test + public void findNotFound() throws IOException { + Object source = loader.findTemplateSource("bogus"); + assertNull("not found", source); + } + + @Test + public void findInTopLevel() throws IOException { + Object source = loader.findTemplateSource(TEMPLATE_NAME_UPPER); + assertEquals("top level", upperTemplate, source); + } + + @Test + public void findInLowerLevel() throws IOException { + Object source = loader.findTemplateSource(TEMPLATE_NAME_LOWER); + assertEquals("lower level", lowerTemplate, source); + } + + @Test + public void findIgnoringPath() throws IOException { + Object source = loader + .findTemplateSource(TEMPLATE_NAME_UPPER_WITH_PATH); + assertEquals("top level", upperTemplate, source); + } + + @Test + public void checkTheReader() throws IOException { + Object source = loader.findTemplateSource(TEMPLATE_NAME_UPPER); + Reader reader = loader.getReader(source, "UTF-8"); + String contents = readAll(reader); + assertEquals("read the contents", contents, TEMPLATE_UPPER_CONTENTS); + } + + /** + * We may not know exactly when the file was last modified, but it should + * fall into a known range. + */ + @Test + public void lastModified() throws IOException { + Object source = loader.findTemplateSource(TEMPLATE_NAME_UPPER); + long modified = loader.getLastModified(source); + long firstBoundary = System.currentTimeMillis(); + assertInRange("created", setupTime, firstBoundary, modified); + + rewriteFile(upperTemplate, TEMPLATE_UPPER_CONTENTS); + long secondBoundary = System.currentTimeMillis(); + modified = loader.getLastModified(source); + assertInRange("modified", firstBoundary, secondBoundary, modified); + } + + @Test + public void closeDoesntCrash() throws IOException { + Object source = loader.findTemplateSource(TEMPLATE_NAME_UPPER); + loader.closeTemplateSource(source); + } + + // ---------------------------------------------------------------------- + // helper methods + // ---------------------------------------------------------------------- + + /** + * Fill an existing file with new contents. + */ + private void rewriteFile(File file, String contents) throws IOException { + Writer writer = null; + try { + writer = new FileWriter(file); + writer.write(contents); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Assert that the modified time falls between (or on) the two boundary + * times. + */ + private void assertInRange(String message, long lowerBound, + long upperBound, long modified) { + if (modified < lowerBound) { + fail(message + ": " + formatTimeStamp(modified) + + " is less than the lower bound " + + formatTimeStamp(lowerBound)); + } + if (modified > upperBound) { + fail(message + ": " + formatTimeStamp(modified) + + " is greater than the upper bound " + + formatTimeStamp(upperBound)); + } + } + + private String formatTimeStamp(long time) { + SimpleDateFormat formatter = new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss.SSS"); + return formatter.format(time); + } + +}