VIVO-533 When loading templates, mimic the language filtering of the RDFService.
This commit is contained in:
parent
19f2d14b5f
commit
818962d0a6
6 changed files with 683 additions and 363 deletions
|
@ -38,6 +38,7 @@ public class DelimitingTemplateLoader implements TemplateLoader {
|
|||
@Override
|
||||
public Object findTemplateSource(String name) throws IOException {
|
||||
Object innerTS = innerLoader.findTemplateSource(name);
|
||||
log.debug("template source for '" + name + "' is '" + innerTS + "'");
|
||||
if (innerTS == null) {
|
||||
return null;
|
||||
} else {
|
||||
|
|
|
@ -1,162 +0,0 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.controller.freemarker;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import freemarker.cache.TemplateLoader;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A {@link TemplateLoader} that treats a directory and its sub-directories as a
|
||||
* flat namespace.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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 <code>myFile.ftl</code> might return a reference to a file at
|
||||
* <code>base/myFile.ftl</code> or at <code>base/this/myFile.ftl</code>
|
||||
* </p>
|
||||
* <p>
|
||||
* The order in which the sub-directories are searched is unspecified. The first
|
||||
* matching file will be returned.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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
|
||||
* <code>path/file.ftl</code> or <code>/absolute/path/file.ftl</code>is
|
||||
* functionally identical to a request for <code>file.ftl</code>
|
||||
* </p>
|
||||
* <p>
|
||||
* </p>
|
||||
*/
|
||||
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, or <code>null</code> 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 {
|
||||
}
|
||||
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
package edu.cornell.mannlib.vitro.webapp.freemarker.config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
@ -21,9 +20,9 @@ import org.apache.commons.logging.LogFactory;
|
|||
import edu.cornell.mannlib.vitro.webapp.config.RevisionInfoBean;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.DelimitingTemplateLoader;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FlatteningTemplateLoader;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder;
|
||||
import edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration.EditConfigurationConstants;
|
||||
import edu.cornell.mannlib.vitro.webapp.freemarker.loader.FreemarkerTemplateLoader;
|
||||
import edu.cornell.mannlib.vitro.webapp.i18n.freemarker.I18nMethodModel;
|
||||
import edu.cornell.mannlib.vitro.webapp.startup.StartupStatus;
|
||||
import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings;
|
||||
|
@ -35,7 +34,6 @@ import edu.cornell.mannlib.vitro.webapp.web.methods.IndividualLocalNameMethod;
|
|||
import edu.cornell.mannlib.vitro.webapp.web.methods.IndividualPlaceholderImageUrlMethod;
|
||||
import edu.cornell.mannlib.vitro.webapp.web.methods.IndividualProfileUrlMethod;
|
||||
import freemarker.cache.ClassTemplateLoader;
|
||||
import freemarker.cache.FileTemplateLoader;
|
||||
import freemarker.cache.MultiTemplateLoader;
|
||||
import freemarker.cache.TemplateLoader;
|
||||
import freemarker.ext.beans.BeansWrapper;
|
||||
|
@ -150,38 +148,31 @@ public abstract class FreemarkerConfiguration {
|
|||
|
||||
List<TemplateLoader> loaders = new ArrayList<TemplateLoader>();
|
||||
|
||||
// Theme template loader
|
||||
// Theme template loader - only if the theme has a template directory.
|
||||
String themeTemplatePath = ctx.getRealPath(themeDir) + "/templates";
|
||||
File themeTemplateDir = new File(themeTemplatePath);
|
||||
// A theme need not contain a template directory.
|
||||
if (themeTemplateDir.exists()) {
|
||||
try {
|
||||
FileTemplateLoader themeFtl = new FileTemplateLoader(
|
||||
themeTemplateDir);
|
||||
loaders.add(themeFtl);
|
||||
} catch (IOException e) {
|
||||
log.error("Error creating theme template loader", e);
|
||||
}
|
||||
loaders.add(new FreemarkerTemplateLoader(themeTemplateDir));
|
||||
}
|
||||
|
||||
// Vitro template loader
|
||||
String vitroTemplatePath = ctx.getRealPath("/templates/freemarker");
|
||||
loaders.add(new FlatteningTemplateLoader(new File(vitroTemplatePath)));
|
||||
loaders.add(new FreemarkerTemplateLoader(new File(vitroTemplatePath)));
|
||||
|
||||
// TODO VIVO-243 Why is this here?
|
||||
loaders.add(new ClassTemplateLoader(FreemarkerConfiguration.class, ""));
|
||||
|
||||
|
||||
TemplateLoader[] loaderArray = loaders
|
||||
.toArray(new TemplateLoader[loaders.size()]);
|
||||
MultiTemplateLoader mtl = new MultiTemplateLoader(loaderArray);
|
||||
TemplateLoader tl = new MultiTemplateLoader(loaderArray);
|
||||
|
||||
// If requested, add delimiters to the templates.
|
||||
DeveloperSettings settings = DeveloperSettings.getBean(req);
|
||||
if (settings.getBoolean(Keys.INSERT_FREEMARKER_DELIMITERS)) {
|
||||
return new DelimitingTemplateLoader(mtl);
|
||||
} else {
|
||||
return mtl;
|
||||
tl = new DelimitingTemplateLoader(tl);
|
||||
}
|
||||
|
||||
return tl;
|
||||
}
|
||||
|
||||
private static void setThreadLocalsForRequest(HttpServletRequest req) {
|
||||
|
|
|
@ -0,0 +1,314 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.freemarker.loader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Comparator;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import freemarker.cache.TemplateLoader;
|
||||
|
||||
/**
|
||||
* Loads Freemarker templates from a given directory.
|
||||
*
|
||||
* Different from a file loader in two ways:
|
||||
*
|
||||
* 1) Flattens the directory. When it searches for a template, it will look in
|
||||
* the base directory and in any sub-directories. While doing this, it ignores
|
||||
* any path that is attached to the template name.
|
||||
*
|
||||
* So if you were to ask for 'admin/silly.ftl', it would search for 'silly.ftl'
|
||||
* in the base directory, and in any sub-directories, until it finds one.
|
||||
*
|
||||
* 2) Accepts approximate matches on locales. When asked for a template, it will
|
||||
* accepts an approximate match that matches the basename and extension, and
|
||||
* language or region if specifed. So a search for a template with no language
|
||||
* or region will prefer an exact match, but will accept one with language or
|
||||
* both language and region.
|
||||
*
|
||||
* <pre>
|
||||
* "this_es_MX.ftl" matches "this_es_MX.ftl"
|
||||
* "this_es.ftl" matches "this_es.ftl" or "this_es_MX.ftl"
|
||||
* "this.ftl" matches "this.ftl" or "this_es.ftl" or "this_es_MX.ftl"
|
||||
* </pre>
|
||||
*
|
||||
* This allows Freemarker to mimic the behavior of the language filtering RDF
|
||||
* service, because if Freemarker does not find a match for "this_es_MX.ftl", it
|
||||
* will try again with "this_es.ftl" and "this.ftl". So the net effect is that a
|
||||
* search for "silly_es_MX.ftl" would eventually return any of these, in order
|
||||
* of preference:
|
||||
*
|
||||
* <pre>
|
||||
* silly_es_MX.ftl
|
||||
* silly_es.ftl
|
||||
* silly_es_*.ftl
|
||||
* silly.ftl
|
||||
* silly_*.ftl
|
||||
* </pre>
|
||||
*
|
||||
* If more than one template file qualifies, we choose by best fit, shortest
|
||||
* path, and alphabetical order, to insure that identical requests produce
|
||||
* identical results.
|
||||
*/
|
||||
public class FreemarkerTemplateLoader implements TemplateLoader {
|
||||
private static final Log log = LogFactory
|
||||
.getLog(FreemarkerTemplateLoader.class);
|
||||
|
||||
private final File baseDir;
|
||||
|
||||
public FreemarkerTemplateLoader(File baseDir) {
|
||||
if (baseDir == null) {
|
||||
throw new NullPointerException("baseDir may not be null.");
|
||||
}
|
||||
|
||||
String path = baseDir.getAbsolutePath();
|
||||
if (!baseDir.exists()) {
|
||||
throw new IllegalArgumentException("Template directory '" + path
|
||||
+ "' does not exist");
|
||||
}
|
||||
if (!baseDir.isDirectory()) {
|
||||
throw new IllegalArgumentException("Template directory '" + path
|
||||
+ "' is not a directory");
|
||||
}
|
||||
if (!baseDir.canRead()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Can't read template directory '" + path + "'");
|
||||
}
|
||||
|
||||
log.debug("Created template loader - baseDir is '" + path + "'");
|
||||
this.baseDir = baseDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the best template for this name. Walk the tree finding all possible
|
||||
* matches, then choose our favorite.
|
||||
*/
|
||||
@Override
|
||||
public Object findTemplateSource(String name) throws IOException {
|
||||
if (StringUtils.isBlank(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SortedSet<PathPieces> matches = findAllMatches(new PathPieces(name));
|
||||
|
||||
if (matches.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
return matches.last().path.toFile();
|
||||
}
|
||||
}
|
||||
|
||||
private SortedSet<PathPieces> findAllMatches(PathPieces searchTerm) {
|
||||
PathPiecesFileVisitor visitor = new PathPiecesFileVisitor(searchTerm);
|
||||
try {
|
||||
Files.walkFileTree(baseDir.toPath(), visitor);
|
||||
} catch (IOException e) {
|
||||
log.error(e);
|
||||
}
|
||||
return visitor.getMatches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the file when it was last modified.
|
||||
*
|
||||
* @param templateSource
|
||||
* a File that was obtained earlier from findTemplateSource().
|
||||
*/
|
||||
@Override
|
||||
public long getLastModified(Object templateSource) {
|
||||
return asFile(templateSource).lastModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Reader on this File. The framework will close the Reader after
|
||||
* reading it.
|
||||
*
|
||||
* @param templateSource
|
||||
* a File that was obtained earlier from findTemplateSource().
|
||||
*/
|
||||
@Override
|
||||
public Reader getReader(Object templateSource, String encoding)
|
||||
throws IOException {
|
||||
return new FileReader(asFile(templateSource));
|
||||
}
|
||||
|
||||
/**
|
||||
* Nothing to do here. No resources to free up.
|
||||
*
|
||||
* @param templateSource
|
||||
* a File that was obtained earlier from findTemplateSource().
|
||||
*/
|
||||
@Override
|
||||
public void closeTemplateSource(Object templateSource) throws IOException {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* That templateSource is a File, right?
|
||||
*/
|
||||
private File asFile(Object templateSource) {
|
||||
if (templateSource instanceof File) {
|
||||
return (File) templateSource;
|
||||
} else {
|
||||
throw new IllegalArgumentException("templateSource is not a File: "
|
||||
+ templateSource);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Helper classes
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Break a path into handy segments, so we can see whether they match the
|
||||
* search term, and how well they match.
|
||||
*/
|
||||
static class PathPieces {
|
||||
final Path path;
|
||||
final String base;
|
||||
final String language;
|
||||
final String region;
|
||||
final String extension;
|
||||
|
||||
public PathPieces(String searchTerm) {
|
||||
this(Paths.get(searchTerm));
|
||||
}
|
||||
|
||||
public PathPieces(Path path) {
|
||||
this.path = path;
|
||||
|
||||
String filename = path.getFileName().toString();
|
||||
int dotHere = filename.lastIndexOf('.');
|
||||
String basename;
|
||||
if (dotHere != -1) {
|
||||
basename = filename.substring(0, dotHere);
|
||||
this.extension = filename.substring(dotHere);
|
||||
} else {
|
||||
basename = filename;
|
||||
this.extension = "";
|
||||
}
|
||||
|
||||
int break2 = basename.lastIndexOf('_');
|
||||
int break1 = basename.lastIndexOf('_', break2 - 1);
|
||||
if (break1 != -1) {
|
||||
this.base = basename.substring(0, break1);
|
||||
this.language = basename.substring(break1, break2);
|
||||
this.region = basename.substring(break2);
|
||||
} else if (break2 != -1) {
|
||||
this.base = basename.substring(0, break2);
|
||||
this.language = basename.substring(break2);
|
||||
this.region = "";
|
||||
} else {
|
||||
this.base = basename;
|
||||
this.language = "";
|
||||
this.region = "";
|
||||
}
|
||||
}
|
||||
|
||||
/** This is the search term. Does that candidate qualify as a result? */
|
||||
public boolean matches(PathPieces that) {
|
||||
return base.equals(that.base) && extension.equals(that.extension)
|
||||
&& (language.isEmpty() || language.equals(that.language))
|
||||
&& (region.isEmpty() || region.equals(that.region));
|
||||
}
|
||||
|
||||
public int score(PathPieces that) {
|
||||
if (matches(that)) {
|
||||
if (that.language.equals(language)) {
|
||||
if (that.region.equals(region)) {
|
||||
return 3; // match language and region
|
||||
} else {
|
||||
return 2; // match language, default region.
|
||||
}
|
||||
} else {
|
||||
return 1; // default language.
|
||||
}
|
||||
} else {
|
||||
return -1; // doesn't match.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PathPieces[" + base + ", " + language + ", " + region
|
||||
+ ", " + extension + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* While walking the file tree, collect all files that match the search
|
||||
* term, as a sorted set of PathPieces.
|
||||
*/
|
||||
static class PathPiecesFileVisitor extends SimpleFileVisitor<Path> {
|
||||
private final PathPieces searchTerm;
|
||||
private final SortedSet<PathPieces> matches;
|
||||
|
||||
public PathPiecesFileVisitor(PathPieces searchTerm) {
|
||||
this.searchTerm = searchTerm;
|
||||
this.matches = new TreeSet<>(new PathPiecesComparator(searchTerm));
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
if (fileQualifies(path)) {
|
||||
PathPieces found = new PathPieces(path);
|
||||
if (searchTerm.matches(found)) {
|
||||
matches.add(found);
|
||||
}
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
public boolean fileQualifies(Path path) {
|
||||
return Files.isRegularFile(path) && Files.isReadable(path);
|
||||
}
|
||||
|
||||
public SortedSet<PathPieces> getMatches() {
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce an ordering of paths by desirability. Best match, then shortest
|
||||
* directory path, and finally alphabetical order.
|
||||
*/
|
||||
static class PathPiecesComparator implements Comparator<PathPieces> {
|
||||
private final PathPieces searchFor;
|
||||
|
||||
public PathPiecesComparator(PathPieces searchFor) {
|
||||
this.searchFor = searchFor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(PathPieces p1, PathPieces p2) {
|
||||
int scoring = searchFor.score(p1) - searchFor.score(p2);
|
||||
if (scoring != 0) {
|
||||
return scoring; // prefer matches to region and language
|
||||
}
|
||||
|
||||
int pathLength = p1.path.getNameCount() - p2.path.getNameCount();
|
||||
if (pathLength != 0) {
|
||||
return -pathLength; // shorter is better
|
||||
}
|
||||
|
||||
return -p1.path.compareTo(p2.path); // early in alphabet is better
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
/* $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.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
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
|
||||
*
|
||||
* <pre>
|
||||
* 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.
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
// ----------------------------------------------------------------------
|
||||
// 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 File tempDir;
|
||||
private static File notADirectory;
|
||||
private static File upperTemplate;
|
||||
private static File lowerTemplate;
|
||||
|
||||
private FlatteningTemplateLoader loader;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUpFiles() throws IOException {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Some systems only record last-modified times to the nearest second, so we
|
||||
* can't rely on them changing during the course of the test. Force the
|
||||
* change, and test for it.
|
||||
*/
|
||||
@Test
|
||||
public void teplateLastModified() throws IOException {
|
||||
Object source = loader.findTemplateSource(TEMPLATE_NAME_UPPER);
|
||||
long modified = loader.getLastModified(source);
|
||||
long now = System.currentTimeMillis();
|
||||
assertTrue("near to now: modified=" + formatTimeStamp(modified)
|
||||
+ ", now=" + formatTimeStamp(now),
|
||||
2000 > Math.abs(modified - now));
|
||||
|
||||
upperTemplate.setLastModified(5000);
|
||||
assertEquals("modified modified", 5000, loader.getLastModified(source));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeDoesntCrash() throws IOException {
|
||||
Object source = loader.findTemplateSource(TEMPLATE_NAME_UPPER);
|
||||
loader.closeTemplateSource(source);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// helper methods
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
private String formatTimeStamp(long time) {
|
||||
SimpleDateFormat formatter = new SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss.SSS");
|
||||
return formatter.format(time);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,359 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.freemarker.loader;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.SortedSet;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.junit.Test;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.freemarker.loader.FreemarkerTemplateLoader.PathPieces;
|
||||
import edu.cornell.mannlib.vitro.webapp.freemarker.loader.FreemarkerTemplateLoader.PathPiecesFileVisitor;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
public class FreemarkerTemplateLoaderTest {
|
||||
private PathPiecesFileVisitor visitor;
|
||||
private String[] paths;
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// PathPieces tests
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
public void ppLanguageRegionExtension() {
|
||||
assertPathPieces("this_en_US.ftl", "this", "_en", "_US", ".ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ppLanguageRegion() {
|
||||
assertPathPieces("this_en_US", "this", "_en", "_US", "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ppLanguageExtension() {
|
||||
assertPathPieces("this_en.ftl", "this", "_en", "", ".ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ppLanguage() {
|
||||
assertPathPieces("this_en", "this", "_en", "", "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ppDefaultExtension() {
|
||||
assertPathPieces("this.ftl", "this", "", "", ".ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ppDefault() {
|
||||
assertPathPieces("this", "this", "", "", "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ppExtraUnderscoreExtension() {
|
||||
assertPathPieces("woo_hoo_en_US.ftl", "woo_hoo", "_en", "_US", ".ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ppExtraUnderscore() {
|
||||
assertPathPieces("woo_hoo_en_US", "woo_hoo", "_en", "_US", "");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Specific function tests
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
public void baseAndExtensionMatch() {
|
||||
paths("match-me.ftl");
|
||||
assertMatches("match-me.ftl", 1, "match-me.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void baseAndExtensionDontMatch() {
|
||||
paths("match-me.ftl");
|
||||
assertMatches("fail.ftl", 0, null);
|
||||
assertMatches("match-me", 0, null);
|
||||
assertMatches("match-me.FTL", 0, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchRegardlessOfDepth() {
|
||||
paths("short-path.ftl", "long/long-path.ftl");
|
||||
assertMatches("long/short-path.ftl", 1, "short-path.ftl");
|
||||
assertMatches("long-path.ftl", 1, "long/long-path.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preferShorterPath() {
|
||||
paths("shorter-is-better", "long/shorter-is-better");
|
||||
assertMatches("shorter-is-better", 2, "shorter-is-better");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preferShorterPathToExactPath() {
|
||||
paths("shorter-is-better", "long/shorter-is-better");
|
||||
assertMatches("long/shorter-is-better", 2, "shorter-is-better");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void languageAndRegionMustMatchExactly() {
|
||||
paths("this_es_MX.ftl", "this_es_ES.ftl", "this_es.ftl");
|
||||
assertMatches("this_es_ES.ftl", 1, "this_es_ES.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void languageAndRegionNoMatch() {
|
||||
paths("this_es_MX.ftl", "this_es_ES.ftl", "this_es.ftl");
|
||||
assertMatches("this_es_GO.ftl", 0, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void languagePrefersExactMatch() {
|
||||
paths("this_es_MX.ftl", "this_es.ftl", "this_es_ES.ftl");
|
||||
assertMatches("this_es.ftl", 3, "this_es.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void languageAcceptsApproximateMatch() {
|
||||
paths("this_es_MX.ftl");
|
||||
assertMatches("this_es.ftl", 1, "this_es_MX.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void languagePrefersApproximateAlphabetical() {
|
||||
paths("this_es_MX.ftl", "this_es_ES.ftl");
|
||||
assertMatches("this_es.ftl", 2, "this_es_ES.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPrefersExactMatch() {
|
||||
paths("this_fr.ftl", "this.ftl", "this_fr_BE.ftl");
|
||||
assertMatches("this.ftl", 3, "this.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPrefersDefaultRegion() {
|
||||
paths("this_fr_BE.ftl", "this_fr.ftl", "this_fr_CA.ftl");
|
||||
assertMatches("this.ftl", 3, "this_fr.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPrefersLanguageAlphabetical() {
|
||||
paths("this_es.ftl", "this_fr.ftl");
|
||||
assertMatches("this.ftl", 2, "this_es.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPrefersRegionAlphabetical() {
|
||||
paths("this_fr_BE.ftl", "this_fr_CA.ftl");
|
||||
assertMatches("this.ftl", 2, "this_fr_BE.ftl");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Freemarker simulation tests
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
public static final String[] FREEMARKER_TEST_PATHS = {
|
||||
"long/this_fr_BE.ftl", "language_fr.ftl", "default.ftl",
|
||||
"language-approx_en_US.ftl" };
|
||||
|
||||
@Test
|
||||
public void freemarkerLangAndRegionExact() {
|
||||
paths = FREEMARKER_TEST_PATHS;
|
||||
assertFM("this_fr_BE.ftl", 1, "long/this_fr_BE.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freemarkerLangAndRegionMatchLang() {
|
||||
paths = FREEMARKER_TEST_PATHS;
|
||||
assertFM("language_fr_CA.ftl", 2, "language_fr.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freemarkerLangAndRegionMatchDefault() {
|
||||
paths = FREEMARKER_TEST_PATHS;
|
||||
assertFM("default_es_ES.ftl", 3, "default.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freemarkerLangAndRegionNoMatch() {
|
||||
paths = FREEMARKER_TEST_PATHS;
|
||||
assertFM("bogus_en_US.ftl", 3, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freemarkerLangExact() {
|
||||
paths = FREEMARKER_TEST_PATHS;
|
||||
assertFM("language_fr.ftl", 1, "language_fr.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freemarkerLangMatchLangAndRegion() {
|
||||
paths = FREEMARKER_TEST_PATHS;
|
||||
assertFM("language-approx_en.ftl", 1, "language-approx_en_US.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freemarkerLangMatchDefault() {
|
||||
paths = FREEMARKER_TEST_PATHS;
|
||||
assertFM("default_en.ftl", 2, "default.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freemarkerLangNoMatch() {
|
||||
paths = FREEMARKER_TEST_PATHS;
|
||||
assertFM("bogus_it.ftl", 2, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freemarkerDefaultExact() {
|
||||
paths = FREEMARKER_TEST_PATHS;
|
||||
assertFM("default.ftl", 1, "default.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freemarkerDefaultMatchLang() {
|
||||
paths = FREEMARKER_TEST_PATHS;
|
||||
assertFM("language.ftl", 1, "language_fr.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freemarkerDefaultMatchLangAndRegion() {
|
||||
paths = FREEMARKER_TEST_PATHS;
|
||||
assertFM("this.ftl", 1, "long/this_fr_BE.ftl");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freemarkerDefaultNoMatch() {
|
||||
paths = FREEMARKER_TEST_PATHS;
|
||||
assertFM("bogus.ftl", 1, null);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Helper methods
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
private void paths(String... p) {
|
||||
this.paths = p;
|
||||
}
|
||||
|
||||
private void assertPathPieces(String path, String base, String language,
|
||||
String region, String extension) {
|
||||
PathPieces pp = new PathPieces(path);
|
||||
String[] expected = new String[] { base, language, region, extension };
|
||||
String[] actual = new String[] { pp.base, pp.language, pp.region,
|
||||
pp.extension };
|
||||
assertEquals("pieces", Arrays.asList(expected), Arrays.asList(actual));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param searchTerm
|
||||
* template we are looking for
|
||||
* @param expectedHowMany
|
||||
* How many matches do we expect?
|
||||
* @param expectedBestFit
|
||||
* What should the best match turn out to be?
|
||||
* @throws IOException
|
||||
*/
|
||||
private void assertMatches(String searchTerm, int expectedHowMany,
|
||||
String expectedBestFit) {
|
||||
SortedSet<PathPieces> matches = runTheVisitor(searchTerm);
|
||||
int actualHowMany = matches.size();
|
||||
String actualBestFit = matches.isEmpty() ? null : matches.last().path
|
||||
.toString();
|
||||
|
||||
if (expectedHowMany != actualHowMany) {
|
||||
fail("How many results: expected " + expectedHowMany
|
||||
+ ", but was " + actualHowMany + ": " + matches);
|
||||
}
|
||||
if (!StringUtils.equals(expectedBestFit, actualBestFit)) {
|
||||
fail("Best result: expected '" + expectedBestFit + "', but was '"
|
||||
+ actualBestFit + "': " + matches);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try for exact match, then pare down if needed, just like Freemarker
|
||||
* would.
|
||||
*/
|
||||
private void assertFM(String searchTerm, int expectedNumberOfTries,
|
||||
String expectedBestFit) {
|
||||
PathPieces stPp = new PathPieces(searchTerm);
|
||||
|
||||
int actualNumberOfTries = 0;
|
||||
String actualBestFit = null;
|
||||
|
||||
if (StringUtils.isNotBlank(stPp.region)) {
|
||||
actualNumberOfTries++;
|
||||
SortedSet<PathPieces> matches = runTheVisitor(stPp.base
|
||||
+ stPp.language + stPp.region + stPp.extension);
|
||||
if (!matches.isEmpty()) {
|
||||
actualBestFit = matches.last().path.toString();
|
||||
}
|
||||
}
|
||||
if (actualBestFit == null && StringUtils.isNotBlank(stPp.language)) {
|
||||
actualNumberOfTries++;
|
||||
SortedSet<PathPieces> matches = runTheVisitor(stPp.base
|
||||
+ stPp.language + stPp.extension);
|
||||
if (!matches.isEmpty()) {
|
||||
actualBestFit = matches.last().path.toString();
|
||||
}
|
||||
}
|
||||
if (actualBestFit == null) {
|
||||
actualNumberOfTries++;
|
||||
SortedSet<PathPieces> matches = runTheVisitor(stPp.base
|
||||
+ stPp.extension);
|
||||
if (!matches.isEmpty()) {
|
||||
actualBestFit = matches.last().path.toString();
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("How many tries", expectedNumberOfTries,
|
||||
actualNumberOfTries);
|
||||
assertEquals("best fit", expectedBestFit, actualBestFit);
|
||||
}
|
||||
|
||||
private SortedSet<PathPieces> runTheVisitor(String searchTerm) {
|
||||
try {
|
||||
visitor = new PathPiecesFileVisitorStub(new PathPieces(searchTerm));
|
||||
for (String p : this.paths) {
|
||||
visitor.visitFile(Paths.get(p), null);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
fail("Failed: " + e);
|
||||
}
|
||||
|
||||
return visitor.getMatches();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Helper classes
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* We want to test the PathPiecesFileVisitor, but we can't have it checking
|
||||
* to see whether the files actually exist.
|
||||
*/
|
||||
private static class PathPiecesFileVisitorStub extends
|
||||
PathPiecesFileVisitor {
|
||||
public PathPiecesFileVisitorStub(PathPieces searchTerm) {
|
||||
super(searchTerm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean fileQualifies(Path path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue