VIVO-533 When loading templates, mimic the language filtering of the RDFService.

This commit is contained in:
j2blake 2013-11-18 17:13:09 -05:00
parent 19f2d14b5f
commit 818962d0a6
6 changed files with 683 additions and 363 deletions

View file

@ -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 {

View file

@ -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 {
}
}

View file

@ -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) {

View file

@ -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
}
}
}