diff --git a/doc/install.html b/doc/install.html index cd048e367..22932f888 100644 --- a/doc/install.html +++ b/doc/install.html @@ -745,6 +745,21 @@ + + + Show only the most appropriate data values based on the Accept-Language + header supplied by the browser. Default is false if not set. + + + + + RDFService.languageFilter + + + false + + + Force VIVO to use a specific language or Locale instead of those @@ -780,65 +795,6 @@ - - - For developers only. - Defeat the Freemarker template cache, so each template - is read from disk on each request. This permits developers to immediately - see the effect of changes to the template. The default is false, which - means that a cached copy of each template will be used for 60 seconds - before the disk is checked for a new version. -
Setting this option to "true" slows down Vitro performance. - - - - - developer.defeatFreemarkerCache - - - false - - - - - - For developers only. - Defeat the cache of language-specific text strings, - so the language file is read from disk on each request. - This permits developers to immediately - see the effect of changes to the text strings. - The default is false, which means that the language file is - read when VIVO starts up, or when a new theme is selected. -
Setting this option to "true" slows down Vitro performance. - - - - - developer.defeatI18nCache = true - - - false - - - - - - For developers only. - Add starting and ending delimiters to each Freemarker template, so you can see - which template were invoked by viewing the generated HTML. - The default is false. -
Setting this option to "true" slows down Vitro performance. - - - - - developer.insertFreemarkerDelimiters = true - - - false - - - diff --git a/webapp/build.xml b/webapp/build.xml index 00f61f423..93927a7f7 100644 --- a/webapp/build.xml +++ b/webapp/build.xml @@ -263,7 +263,12 @@ - + + + + + + diff --git a/webapp/config/example.developer.properties b/webapp/config/example.developer.properties new file mode 100644 index 000000000..482cecd21 --- /dev/null +++ b/webapp/config/example.developer.properties @@ -0,0 +1,107 @@ +# +# ----------------------------------------------------------------------------- +# Runtime properties for developer mode. +# +# If the developer.properties file is present in your VIVO home directory, it +# will be loaded as VIVO starts up, taking effect immediately. +# +# Each of these properties can be set or changed while VIVO is running, but it +# can be convenient to set them in advance. +# +# WARNING: Some of these options can seriously degrade performance. They should +# not be enabled in a production instance of VIVO. +# +# ----------------------------------------------------------------------------- +# + +#------------------------------------------------------------------------------ +# General options +#------------------------------------------------------------------------------ + +# +# The "master switch" for developer mode. If this is not set to true, then none +# of the other properties have any effect. +# +# developer.enabled = true + +# +# If developer mode is enabled, this will determine who can modify the +# developer settings. If 'true', then any user can modify the settings. If +# false, then only a site administrator (or root) can modify the settings. +# The default is 'false'. +# +# developer.permitAnonymousControl + +#------------------------------------------------------------------------------ +# Freemarker +#------------------------------------------------------------------------------ + +# +# Add HTML comments to each Freemarker template, so you can see what each +# templates to the page, by viewing the source of the page in the browser. +# The default is 'false'. +# +# developer.insertFreemarkerDelimiters = true + +# +# Defeat the Freemarker template cache, so each template is read from disk +# on each request. This permits developers to immediately see the effect of +# changes to the template. The default is 'false', which means that a cached +# copy of each template will be used for 60 seconds before the disk is checked +# for a new version. +# +# developer.defeatFreemarkerCache = true + +#------------------------------------------------------------------------------ +# Internationalization +#------------------------------------------------------------------------------ + +# +# Defeat the cache of language-specific text strings, so the language file +# is read from disk on each request. This permits developers to immediately +# see the effect of changes to the text strings. The default is 'false', which +# means that the language file is only read when VIVO starts up, or when a new +# theme is selected. +# +# developer.i18n.defeatCache = true + +# +# Write a line to the log every time a template or a controller requests a +# language-specific string from the properties files. +# +# developer.i18n.logStringRequests + + +#------------------------------------------------------------------------------ +# Logging SPARQL queries +#------------------------------------------------------------------------------ + +# +# Turn on logging of all SPARQL queries. The logging is at the INFO level. +# Each entry includes: +# - the elapsed time spent on the query, in seconds, +# - the name of the method on RDFService that received the query, +# - the format of the result stream from the RDFService method, +# - the text of the query. +# Note that all access to the content models is done through SPARQL queries, +# but some go through translation layers before reaching the RDFService for +# logging and execution. The default is 'false'. +# +# developer.loggingRDFService.enable = true + +# +# If SPARQL query logging is enabled, this will add a stack trace to each log +# entry. The stack trace is abridged, so it starts after the +# ApplicationFilterChain, omits any Jena classes, and ends at the RDFService. +# The default is 'false'. +# +# developer.loggingRDFService.stackTrace = true + +# +# If SPARQL query logging is enabled, a regular expression can be used to +# restrict the number of entries that are produced. The expression is +# tested against each line in the (unabridged) stack trace. If the +# expression doesn't match any line in the stack trace, then no log entry +# is made. The default is 'false'. +# +# developer.loggingRDFService.restriction = true diff --git a/webapp/config/example.runtime.properties b/webapp/config/example.runtime.properties index d72d632d7..fa1100def 100644 --- a/webapp/config/example.runtime.properties +++ b/webapp/config/example.runtime.properties @@ -116,9 +116,9 @@ proxy.eligibleTypeList = http://www.w3.org/2002/07/owl#Thing # # Show only the most appropriate data values based on the Accept-Language -# header supplied by the browser. Default is true if not set. +# header supplied by the browser. Default is false if not set. # -RDFService.languageFilter = true +# RDFService.languageFilter = true # # Tell VIVO to generate HTTP headers on its responses to facilitate caching the @@ -152,35 +152,3 @@ RDFService.languageFilter = true # This should not be used with languages.forceLocale, which will override it. # # languages.selectableLocales = en, es, fr - -# -# For developers only: Setting this option to "true" slows down Vitro performance. -# -# Defeat the Freemarker template cache, so each template is read from disk -# on each request. This permits developers to immediately see the effect of -# changes to the template. The default is false, which means -# that a cached copy of each template will be used for 60 seconds before -# the disk is checked for a new version. -# -# developer.defeatFreemarkerCache = true - -# -# For developers only: Setting this option to "true" slows down Vitro performance. -# -# Defeat the cache of language-specific text strings, so the language file -# is read from disk on each request. This permits developers to immediately -# see the effect of changes to the text strings. The default is -# false, which means that the language file is read when -# VIVO starts up, or when a new theme is selected. -# -# developer.defeatI18nCache = true - -# -# For developers only: Setting this option to "true" slows down Vitro performance. -# -# Add starting and ending delimiters to each Freemarker template, so you can see -# which template were invoked by viewing the generated HTML. The default is -# false. -# -# developer.insertFreemarkerDelimiters = true - diff --git a/webapp/rdf/auth/everytime/permission_config.n3 b/webapp/rdf/auth/everytime/permission_config.n3 index 9b61615f2..c4a88018a 100644 --- a/webapp/rdf/auth/everytime/permission_config.n3 +++ b/webapp/rdf/auth/everytime/permission_config.n3 @@ -24,6 +24,7 @@ auth:ADMIN auth:hasPermission simplePermission:UseMiscellaneousAdminPages ; auth:hasPermission simplePermission:UseSparqlQueryPage ; auth:hasPermission simplePermission:PageViewableAdmin ; + auth:hasPermission simplePermission:EnableDeveloperPanel ; # permissions for CURATOR and above. auth:hasPermission simplePermission:EditOntology ; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java index cf0a7e272..33c609c5b 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java @@ -76,6 +76,8 @@ public class SimplePermission extends Permission { NAMESPACE + "UseAdvancedDataToolsPages"); public static final SimplePermission USE_SPARQL_QUERY_PAGE = new SimplePermission( NAMESPACE + "UseSparqlQueryPage"); + public static final SimplePermission ENABLE_DEVELOPER_PANEL = new SimplePermission( + NAMESPACE + "EnableDeveloperPanel"); // ---------------------------------------------------------------------- diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/edit/Classes2ClassesOperationController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/edit/Classes2ClassesOperationController.java index e35b80ca0..81d8f68c1 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/edit/Classes2ClassesOperationController.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/edit/Classes2ClassesOperationController.java @@ -57,7 +57,7 @@ public class Classes2ClassesOperationController extends BaseEditController { return; } - VClassDao vcDao = request.getUnfilteredAssertionsWebappDaoFactory().getVClassDao(); + VClassDao vcDao = request.getLanguageNeutralWebappDaoFactory().getVClassDao(); String modeStr = request.getParameter("opMode"); modeStr = (modeStr == null) ? "" : modeStr; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DelimitingTemplateLoader.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DelimitingTemplateLoader.java index a0650366e..9b103ad82 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DelimitingTemplateLoader.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DelimitingTemplateLoader.java @@ -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 { diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FlatteningTemplateLoader.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FlatteningTemplateLoader.java deleted file mode 100644 index 23f625291..000000000 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FlatteningTemplateLoader.java +++ /dev/null @@ -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; - -/** - *

- * 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.ftlis - * 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, or null 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/dao/jena/VClassDaoJena.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/VClassDaoJena.java index 67dc61ea4..3c038ba13 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/VClassDaoJena.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/VClassDaoJena.java @@ -1048,11 +1048,20 @@ public class VClassDaoJena extends JenaBaseDao implements VClassDao { try { OntResource subclass = getOntClass(ontModel,c2c.getSubclassURI()); OntResource superclass = getOntClass(ontModel,c2c.getSuperclassURI()); + if(subclass == null || superclass == null) { + log.warn("unable to delete " + c2c.getSubclassURI() + + " rdfs:subClassOf " + c2c.getSuperclassURI()); + if (subclass == null) { + log.warn(c2c.getSubclassURI() + " not found in the model."); + } + if (superclass == null) { + log.warn(c2c.getSuperclassURI() + " not found in the model."); + } + return; + } Model removal = ModelFactory.createDefaultModel(); Model additions = ModelFactory.createDefaultModel(); // to repair any rdf:Lists - if ((subclass != null) && (superclass != null)) { - removal.add(ontModel.listStatements(subclass, RDFS.subClassOf, superclass)); - } + removal.add(ontModel.listStatements(subclass, RDFS.subClassOf, superclass)); if (subclass.isAnon()) { Model[] changeSet = getSmartRemoval(subclass, getOntModel()); removal.add(changeSet[0]); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filters/RequestModelsPrep.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filters/RequestModelsPrep.java index d4da08f70..032795028 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/filters/RequestModelsPrep.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filters/RequestModelsPrep.java @@ -336,11 +336,11 @@ public class RequestModelsPrep implements Filter { } /** - * Language awareness is enabled unless they explicitly disable it. + * Language awareness is disabled unless they explicitly enable it. */ private Boolean isLanguageAwarenessEnabled() { return Boolean.valueOf(props.getProperty("RDFService.languageFilter", - "true")); + "false")); } private RDFService addLanguageAwareness(HttpServletRequest req, diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfiguration.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfiguration.java index 17ad1fd0b..274da8d39 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfiguration.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfiguration.java @@ -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; @@ -18,15 +17,16 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; 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; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings.Keys; import edu.cornell.mannlib.vitro.webapp.web.directives.IndividualShortViewDirective; import edu.cornell.mannlib.vitro.webapp.web.directives.UrlDirective; import edu.cornell.mannlib.vitro.webapp.web.directives.WidgetDirective; @@ -34,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; @@ -55,18 +54,17 @@ import freemarker.template.TemplateModelException; * own locale, etc. * * Each time a request asks for the configuration, check to see whether the - * cache is still valid, and whether the theme has changed (needs a new - * TemplateLoader). Store the request info to the ThreadLocal. + * cache is still valid, whether the theme has changed (needs a new + * TemplateLoader), and whether the DeveloperSettings have changed (might need a + * new TemplateLoader). Store the request info to the ThreadLocal. */ public abstract class FreemarkerConfiguration { private static final Log log = LogFactory .getLog(FreemarkerConfiguration.class); - private static final String PROPERTY_DEFEAT_CACHE = "developer.defeatFreemarkerCache"; - private static final String PROPERTY_INSERT_DELIMITERS = "developer.insertFreemarkerDelimiters"; - private static volatile FreemarkerConfigurationImpl instance; private static volatile String previousThemeDir; + private static volatile Map previousSettingsMap; public static Configuration getConfig(HttpServletRequest req) { confirmInstanceIsSet(); @@ -92,14 +90,12 @@ public abstract class FreemarkerConfiguration { } } + /** If the developer doesn't want the cache, it's invalid. */ private static boolean isTemplateCacheInvalid(HttpServletRequest req) { - ConfigurationProperties props = ConfigurationProperties.getBean(req); - - // If the developer doesn't want the cache, it's invalid. - if (Boolean.valueOf(props.getProperty(PROPERTY_DEFEAT_CACHE))) { + DeveloperSettings settings = DeveloperSettings.getBean(req); + if (settings.getBoolean(Keys.DEFEAT_FREEMARKER_CACHE)) { return true; } - return false; } @@ -113,7 +109,8 @@ public abstract class FreemarkerConfiguration { private static void keepTemplateLoaderCurrentWithThemeDirectory( HttpServletRequest req) { String themeDir = getThemeDirectory(req); - if (hasThemeDirectoryChanged(themeDir)) { + if (hasThemeDirectoryChanged(themeDir) + || haveDeveloperSettingsChanged(req)) { TemplateLoader tl = createTemplateLoader(req, themeDir); instance.setTemplateLoader(tl); } @@ -134,44 +131,48 @@ public abstract class FreemarkerConfiguration { } } + private static boolean haveDeveloperSettingsChanged(HttpServletRequest req) { + Map settingsMap = DeveloperSettings.getBean(req) + .getSettingsMap(); + if (settingsMap.equals(previousSettingsMap)) { + return false; + } else { + previousSettingsMap = settingsMap; + return true; + } + } + private static TemplateLoader createTemplateLoader(HttpServletRequest req, String themeDir) { ServletContext ctx = req.getSession().getServletContext(); - ConfigurationProperties props = ConfigurationProperties.getBean(ctx); List loaders = new ArrayList(); - // 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. - if (Boolean.valueOf(props.getProperty(PROPERTY_INSERT_DELIMITERS))) { - return new DelimitingTemplateLoader(mtl); - } else { - return mtl; + DeveloperSettings settings = DeveloperSettings.getBean(req); + if (settings.getBoolean(Keys.INSERT_FREEMARKER_DELIMITERS)) { + tl = new DelimitingTemplateLoader(tl); } + + return tl; } private static void setThreadLocalsForRequest(HttpServletRequest req) { diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfigurationImpl.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfigurationImpl.java index b4318c97f..617a09f6c 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfigurationImpl.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfigurationImpl.java @@ -287,6 +287,7 @@ public class FreemarkerConfigurationImpl extends Configuration { urls.put("images", UrlBuilder.getUrl("/images")); urls.put("theme", UrlBuilder.getUrl(themeDir)); urls.put("index", UrlBuilder.getUrl("/browse")); + urls.put("developerAjax", UrlBuilder.getUrl("/admin/developerAjax")); return urls; } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/loader/FreemarkerTemplateLoader.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/loader/FreemarkerTemplateLoader.java new file mode 100644 index 000000000..33adcb152 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/loader/FreemarkerTemplateLoader.java @@ -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. + * + *
+ * "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"
+ * 
+ * + * 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: + * + *
+ * silly_es_MX.ftl
+ * silly_es.ftl
+ * silly_es_*.ftl
+ * silly.ftl
+ * silly_*.ftl
+ * 
+ * + * 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 matches = findAllMatches(new PathPieces(name)); + + if (matches.isEmpty()) { + return null; + } else { + return matches.last().path.toFile(); + } + } + + private SortedSet 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 { + private final PathPieces searchTerm; + private final SortedSet 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 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 { + 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 + } + + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java index 38a1bc059..05758f26b 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java @@ -15,8 +15,9 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings.Keys; /** * Provides access to a bundle of text strings, based on the name of the bundle, @@ -31,7 +32,6 @@ public class I18n { private static final Log log = LogFactory.getLog(I18n.class); public static final String DEFAULT_BUNDLE_NAME = "all"; - private static final String PROPERTY_DEVELOPER_DEFEAT_CACHE = "developer.defeatI18nCache"; /** * If this attribute is present on the request, then the cache has already @@ -103,6 +103,7 @@ public class I18n { protected I18nBundle getBundle(String bundleName, HttpServletRequest req) { log.debug("Getting bundle '" + bundleName + "'"); + I18nLogger i18nLogger = new I18nLogger(req); try { checkDevelopmentMode(req); checkForChangeInThemeDirectory(req); @@ -113,13 +114,13 @@ public class I18n { ResourceBundle.Control control = new ThemeBasedControl(ctx, dir); ResourceBundle rb = ResourceBundle.getBundle(bundleName, req.getLocale(), control); - return new I18nBundle(bundleName, rb); + return new I18nBundle(bundleName, rb, i18nLogger); } catch (MissingResourceException e) { log.warn("Didn't find text bundle '" + bundleName + "'"); - return I18nBundle.emptyBundle(bundleName); + return I18nBundle.emptyBundle(bundleName, i18nLogger); } catch (Exception e) { log.error("Failed to create text bundle '" + bundleName + "'", e); - return I18nBundle.emptyBundle(bundleName); + return I18nBundle.emptyBundle(bundleName, i18nLogger); } } @@ -127,11 +128,7 @@ public class I18n { * If we are in development mode, clear the cache on each request. */ private void checkDevelopmentMode(HttpServletRequest req) { - ConfigurationProperties bean = ConfigurationProperties.getBean(req); - - String flag = bean - .getProperty(PROPERTY_DEVELOPER_DEFEAT_CACHE, "false"); - if (Boolean.valueOf(flag.trim())) { + if (DeveloperSettings.getBean(req).getBoolean(Keys.I18N_DEFEAT_CACHE) ) { log.debug("In development mode - clearing the cache."); clearCacheOnRequest(req); } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java index 44b930089..02793529c 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java @@ -24,24 +24,28 @@ public class I18nBundle { private static final String MESSAGE_BUNDLE_NOT_FOUND = "Text bundle ''{0}'' not found."; private static final String MESSAGE_KEY_NOT_FOUND = "Text bundle ''{0}'' has no text for ''{1}''"; - public static I18nBundle emptyBundle(String bundleName) { - return new I18nBundle(bundleName); + public static I18nBundle emptyBundle(String bundleName, + I18nLogger i18nLogger) { + return new I18nBundle(bundleName, i18nLogger); } private final String bundleName; private final ResourceBundle resources; private final String notFoundMessage; + private final I18nLogger i18nLogger; - private I18nBundle(String bundleName) { - this(bundleName, new EmptyResourceBundle(), MESSAGE_BUNDLE_NOT_FOUND); + private I18nBundle(String bundleName, I18nLogger i18nLogger) { + this(bundleName, new EmptyResourceBundle(), MESSAGE_BUNDLE_NOT_FOUND, + i18nLogger); } - public I18nBundle(String bundleName, ResourceBundle resources) { - this(bundleName, resources, MESSAGE_KEY_NOT_FOUND); + public I18nBundle(String bundleName, ResourceBundle resources, + I18nLogger i18nLogger) { + this(bundleName, resources, MESSAGE_KEY_NOT_FOUND, i18nLogger); } private I18nBundle(String bundleName, ResourceBundle resources, - String notFoundMessage) { + String notFoundMessage, I18nLogger i18nLogger) { if (bundleName == null) { throw new IllegalArgumentException("bundleName may not be null"); } @@ -57,22 +61,27 @@ public class I18nBundle { this.bundleName = bundleName; this.resources = resources; this.notFoundMessage = notFoundMessage; + this.i18nLogger = i18nLogger; } public String text(String key, Object... parameters) { - String textString; if (resources.containsKey(key)) { textString = resources.getString(key); log.debug("In '" + bundleName + "', " + key + "='" + textString + "')"); - return formatString(textString, parameters); } else { String message = MessageFormat.format(notFoundMessage, bundleName, key); log.warn(message); - return "ERROR: " + message; + textString = "ERROR: " + message; } + String result = formatString(textString, parameters); + + if (i18nLogger != null) { + i18nLogger.log(bundleName, key, parameters, textString, result); + } + return result; } private static String formatString(String textString, Object... parameters) { diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nLogger.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nLogger.java new file mode 100644 index 000000000..8889195b6 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nLogger.java @@ -0,0 +1,47 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.i18n; + +import java.util.Arrays; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings.Keys; + +/** + * If enabled in developer mode, write a message to the log each time someone + * asks for a language string. + * + * The I18nBundle has a life span of one HTTP request, and so does this. + */ +public class I18nLogger { + private static final Log log = LogFactory.getLog(I18nLogger.class); + + private final boolean isLogging; + + public I18nLogger(HttpServletRequest req) { + DeveloperSettings settings = DeveloperSettings.getBean(req); + this.isLogging = settings.getBoolean(Keys.ENABLED) + && settings.getBoolean(Keys.I18N_LOG_STRINGS) + && log.isInfoEnabled(); + } + + public void log(String bundleName, String key, Object[] parameters, + String rawText, String formattedText) { + if (isLogging) { + String message = String.format( + "Retrieved from %s.%s with %s: '%s'", bundleName, key, + Arrays.toString(parameters), rawText); + + if (!rawText.equals(formattedText)) { + message += String.format(" --> '%s'", formattedText); + } + + log.info(message); + } + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/ontology/update/KnowledgeBaseUpdater.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/ontology/update/KnowledgeBaseUpdater.java index a64025294..75b5f09d5 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/ontology/update/KnowledgeBaseUpdater.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/ontology/update/KnowledgeBaseUpdater.java @@ -207,6 +207,14 @@ public class KnowledgeBaseUpdater { StmtIterator sit = anonModel.listStatements(); while (sit.hasNext()) { Statement stmt = sit.nextStatement(); + // Skip statements with blank nodes (unsupported) to avoid + // excessive deletion. In the future, the whole updater + // could be modified to change whole graphs at once through + // the RDFService, but right now this whole thing is statement + // based. + if (stmt.getSubject().isAnon() || stmt.getObject().isAnon()) { + continue; + } Iterator graphIt = dataset.listNames(); while(graphIt.hasNext()) { String graph = graphIt.next(); @@ -223,8 +231,9 @@ public class KnowledgeBaseUpdater { //log.info("removed " + anonModel.size() + " statements from SPARQL CONSTRUCTs"); } else { Model writeModel = dataset.getNamedModel(JenaDataSourceSetupBase.JENA_DB_MODEL); + Model dedupeModel = dataset.getDefaultModel(); Model additions = jiu.renameBNodes( - anonModel, settings.getDefaultNamespace() + "n", writeModel); + anonModel, settings.getDefaultNamespace() + "n", dedupeModel); Model actualAdditions = ModelFactory.createDefaultModel(); StmtIterator stmtIt = additions.listStatements(); while (stmtIt.hasNext()) { diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/jena/RDFServiceJena.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/jena/RDFServiceJena.java index f2eae2554..a8e2ece70 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/jena/RDFServiceJena.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/jena/RDFServiceJena.java @@ -217,6 +217,9 @@ public abstract class RDFServiceJena extends RDFServiceImpl implements RDFServic private List sort(List stmts) { List output = new ArrayList(); int originalSize = stmts.size(); + if(originalSize == 1) { + return stmts; + } List remaining = stmts; ConcurrentLinkedQueue subjQueue = new ConcurrentLinkedQueue(); for(Statement stmt : remaining) { diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/logging/RDFServiceLogger.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/logging/RDFServiceLogger.java index aecdffdec..d7f0ffa7a 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/logging/RDFServiceLogger.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/logging/RDFServiceLogger.java @@ -15,7 +15,8 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings.Keys; /** * Writes the log message for the LoggingRDFService. @@ -41,10 +42,6 @@ import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; public class RDFServiceLogger implements AutoCloseable { private static final Log log = LogFactory.getLog(RDFServiceLogger.class); - private static final String PROPERTY_ENABLED = "developer.loggingRDFService.enable"; - private static final String PROPERTY_STACK_TRACE = "developer.loggingRDFService.stackTrace"; - private static final String PROPERTY_RESTRICTION = "developer.loggingRDFService.restriction"; - private final ServletContext ctx; private final Object[] args; @@ -72,18 +69,21 @@ public class RDFServiceLogger implements AutoCloseable { } private void getProperties() { - ConfigurationProperties props = ConfigurationProperties.getBean(ctx); - isEnabled = Boolean.valueOf(props.getProperty(PROPERTY_ENABLED)); - traceRequested = Boolean.valueOf(props - .getProperty(PROPERTY_STACK_TRACE)); + DeveloperSettings settings = DeveloperSettings.getBean(ctx); + isEnabled = settings.getBoolean(Keys.LOGGING_RDF_ENABLE); + traceRequested = settings.getBoolean(Keys.LOGGING_RDF_STACK_TRACE); - String restrictionString = props.getProperty(PROPERTY_RESTRICTION); - if (StringUtils.isNotBlank(restrictionString)) { + String restrictionString = settings + .getString(Keys.LOGGING_RDF_RESTRICTION); + if (StringUtils.isBlank(restrictionString)) { + restriction = null; + } else { try { restriction = Pattern.compile(restrictionString); } catch (Exception e) { log.error("Failed to compile the pattern for " - + PROPERTY_RESTRICTION + " = " + restriction + " " + e); + + Keys.LOGGING_RDF_RESTRICTION + " = " + restriction + + " " + e); isEnabled = false; } } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/sparql/RDFServiceSparql.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/sparql/RDFServiceSparql.java index d890a1625..b207f1681 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/sparql/RDFServiceSparql.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/sparql/RDFServiceSparql.java @@ -744,6 +744,8 @@ public class RDFServiceSparql extends RDFServiceImpl implements RDFService { private List sort(List stmts) { List output = new ArrayList(); int originalSize = stmts.size(); + if (originalSize == 1) + return stmts; List remaining = stmts; ConcurrentLinkedQueue subjQueue = new ConcurrentLinkedQueue(); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/reasoner/ABoxRecomputer.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/reasoner/ABoxRecomputer.java index 0aa537aaa..fbbc5702f 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/reasoner/ABoxRecomputer.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/reasoner/ABoxRecomputer.java @@ -3,6 +3,7 @@ package edu.cornell.mannlib.vitro.webapp.reasoner; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; @@ -295,14 +296,35 @@ public class ABoxRecomputer { */ protected Collection getAllIndividualURIs() { - String queryString = "SELECT DISTINCT ?s WHERE { GRAPH ?g { ?s a ?type } " + - " FILTER (!bound(?g) || !regex(str(?g),\"tbox\")) } ORDER BY ?s"; - return getIndividualURIs(queryString); + HashSet individualURIs = new HashSet(); + + List classList = new ArrayList(); + + tboxModel.enterCriticalSection(Lock.READ); + try { + StmtIterator classIt = tboxModel.listStatements( + (Resource) null, RDF.type, OWL.Class); + while(classIt.hasNext()) { + Statement stmt = classIt.nextStatement(); + if(stmt.getSubject().isURIResource() + && stmt.getSubject().getURI() != null + && !stmt.getSubject().getURI().isEmpty()) { + classList.add(stmt.getSubject().getURI()); + } + } + } finally { + tboxModel.leaveCriticalSection(); + } + + for (String classURI : classList) { + String queryString = "SELECT ?s WHERE { ?s a <" + classURI + "> } "; + getIndividualURIs(queryString, individualURIs); + } + + return individualURIs; } - protected Collection getIndividualURIs(String queryString) { - - Set individuals = new HashSet(); + protected void getIndividualURIs(String queryString, Set individuals) { int batchSize = 50000; int offset = 0; @@ -342,7 +364,6 @@ public class ABoxRecomputer { offset += batchSize; } - return individuals; } protected void addedABoxTypeAssertion(Resource individual, Model inferenceModel, HashSet unknownTypes) { @@ -410,6 +431,11 @@ public class ABoxRecomputer { while (iter.hasNext()) { Statement stmt = iter.next(); + // skip statements with blank nodes to avoid excessive deletion + if (stmt.getSubject().isAnon() || stmt.getObject().isAnon()) { + continue; + } + inferenceModel.enterCriticalSection(Lock.WRITE); try { inferenceModel.remove(stmt); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/FileGraphSetup.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/FileGraphSetup.java index b882faba1..193cae0e5 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/FileGraphSetup.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/FileGraphSetup.java @@ -187,8 +187,7 @@ public class FileGraphSetup implements ServletContextListener { } else { baseModel.add(model); } - log.info("Attached file graph as " + type + " submodel " + p.getFileName()); - + log.debug("Attached file graph as " + type + " submodel " + p.getFileName()); } modelChanged = modelChanged | updateGraphInDB(dataset, model, type, p); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/JenaDataSourceSetupBase.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/JenaDataSourceSetupBase.java index b79b71fa2..7e39cd060 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/JenaDataSourceSetupBase.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/JenaDataSourceSetupBase.java @@ -221,7 +221,7 @@ public class JenaDataSourceSetupBase extends JenaBaseDaoCon { int[] maxActiveAndIdle = getMaxActiveAndIdle(ctx); cpds.setMaxPoolSize(maxActiveAndIdle[0]); cpds.setMinPoolSize(maxActiveAndIdle[1]); - cpds.setMaxIdleTime(3600); // ms + cpds.setMaxIdleTime(43200); // s cpds.setMaxIdleTimeExcessConnections(300); cpds.setAcquireIncrement(5); cpds.setNumHelperThreads(6); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettings.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettings.java new file mode 100644 index 000000000..822e434e4 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettings.java @@ -0,0 +1,305 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.developer; + +import java.io.File; +import java.io.FileReader; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; + +/** + * Hold the global developer settings. Render to JSON when requested. + * + * On first request, the "developer.properties" file is loaded from the Vitro + * home directory. If the file doesn't exist, or doesn't contain values for + * certain properties, those propertiew will keep their default values. + * + * An AJAX request can be used to update the properties. If the request has + * multiple values for a property, the first value will be used. If the request + * does not contain a value for a property, that property will keep its current + * value. + */ +public class DeveloperSettings { + private static final Log log = LogFactory.getLog(DeveloperSettings.class); + + public enum Keys { + /** + * Developer mode and developer panel is enabled. + */ + ENABLED("developer.enabled", true), + + /** + * Users don't need authority to use the developer panel. But they still + * can't enable it without authority. + */ + PERMIT_ANONYMOUS_CONTROL("developer.permitAnonymousControl", true), + + /** + * Load Freemarker templates every time they are requested. + */ + DEFEAT_FREEMARKER_CACHE("developer.defeatFreemarkerCache", true), + + /** + * Show where each Freemarker template starts and stops. + */ + INSERT_FREEMARKER_DELIMITERS("developer.insertFreemarkerDelimiters", + true), + + /** + * Load language property files every time they are requested. + */ + I18N_DEFEAT_CACHE("developer.i18n.defeatCache", true), + + /** + * Enable the I18nLogger to log each string request. + */ + I18N_LOG_STRINGS("developer.i18n.logStringRequests", true), + + /** + * Enable the LoggingRDFService + */ + LOGGING_RDF_ENABLE("developer.loggingRDFService.enable", true), + + /** + * When logging with the LoggingRDFService, include a stack trace + */ + LOGGING_RDF_STACK_TRACE("developer.loggingRDFService.stackTrace", true), + + /** + * Don't log with the LoggingRDFService unless the calling stack meets + * this restriction. + */ + LOGGING_RDF_RESTRICTION("developer.loggingRDFService.restriction", + false); + + private final String propertyName; + private final String elementId; + private final boolean bool; + + private Keys(String propertyName, boolean bool) { + this.propertyName = propertyName; + this.elementId = produceElementId(); + this.bool = bool; + } + + public String propertyName() { + return propertyName; + } + + public String elementId() { + return elementId; + } + + boolean isBoolean() { + return bool; + } + + /** + * The element ID is camel-case instead of period-delimited. So + * "developer.enabled" becomes "developerEnabled". + */ + String produceElementId() { + StringBuilder id = new StringBuilder(propertyName.length()); + boolean capitalize = false; + for (int i = 0; i < propertyName.length(); i++) { + char c = propertyName.charAt(i); + if (c == '.') { + capitalize = true; + } else if (capitalize) { + id.append(Character.toUpperCase(c)); + capitalize = false; + } else { + id.append(c); + } + } + return id.toString(); + } + + @Override + public String toString() { + return propertyName; + } + + static Keys fromElementId(String id) { + for (Keys k : Keys.values()) { + if (k.elementId.equals(id)) { + return k; + } + } + log.error("Can't find key for element id: '" + id + "'"); + return null; + } + + static Keys fromPropertyName(String name) { + for (Keys k : Keys.values()) { + if (k.propertyName.equals(name)) { + return k; + } + } + log.error("Can't find key for property name: '" + name + "'"); + return null; + } + + } + + // ---------------------------------------------------------------------- + // The factory + // ---------------------------------------------------------------------- + + private static final String ATTRIBUTE_NAME = DeveloperSettings.class + .getName(); + + public static DeveloperSettings getBean(HttpServletRequest req) { + return getBean(req.getSession().getServletContext()); + } + + public static DeveloperSettings getBean(ServletContext ctx) { + Object o = ctx.getAttribute(ATTRIBUTE_NAME); + if (o instanceof DeveloperSettings) { + return (DeveloperSettings) o; + } else { + DeveloperSettings ds = new DeveloperSettings(ctx); + ctx.setAttribute(ATTRIBUTE_NAME, ds); + return ds; + } + } + + // ---------------------------------------------------------------------- + // The instance + // ---------------------------------------------------------------------- + + private final Map settings = new EnumMap<>(Keys.class); + + private DeveloperSettings(ServletContext ctx) { + updateFromFile(ctx); + } + + /** + * Read the initial settings from "developer.properties" in the Vitro home + * directory. + * + * This method is "protected" so we can override it for unit tests. + */ + protected void updateFromFile(ServletContext ctx) { + Map fromFile = new HashMap<>(); + + ConfigurationProperties props = ConfigurationProperties.getBean(ctx); + String home = props.getProperty("vitro.home"); + File dsFile = Paths.get(home, "developer.properties").toFile(); + + if (dsFile.isFile()) { + try (FileReader reader = new FileReader(dsFile)) { + Properties dsProps = new Properties(); + dsProps.load(reader); + for (String key : dsProps.stringPropertyNames()) { + fromFile.put(Keys.fromPropertyName(key), + dsProps.getProperty(key)); + } + } catch (Exception e) { + log.warn("Failed to load 'developer.properties' file.", e); + } + } else { + log.debug("No developer.properties file."); + } + + log.debug("Properties from file: " + fromFile); + update(fromFile); + } + + /** Provide the parameter map from the HttpServletRequest */ + public void updateFromRequest(Map parameterMap) { + if (log.isDebugEnabled()) { + dumpParameterMap(parameterMap); + } + + Map fromRequest = new HashMap<>(); + for (String key : parameterMap.keySet()) { + fromRequest.put(Keys.fromElementId(key), parameterMap.get(key)[0]); + } + update(fromRequest); + } + + private void update(Map changedSettings) { + for (Keys key : Keys.values()) { + String s = changedSettings.get(key); + if (s != null) { + if (key.isBoolean()) { + settings.put(key, Boolean.valueOf(s)); + } else { + settings.put(key, s); + } + } + } + log.debug("DeveloperSettings: " + this); + } + + public Object get(Keys key) { + if (key.isBoolean()) { + return getBoolean(key); + } else { + return getString(key); + } + } + + public boolean getBoolean(Keys key) { + if (!key.isBoolean()) { + throw new IllegalArgumentException("Key '" + key + + "' does not take a boolean value."); + } + if (settings.containsKey(key)) { + if (Boolean.TRUE.equals(settings.get(Keys.ENABLED))) { + return (Boolean) settings.get(key); + } + } + return false; + } + + public String getString(Keys key) { + if (key.isBoolean()) { + throw new IllegalArgumentException("Key '" + key + + "' takes a boolean value."); + } + if (settings.containsKey(key)) { + if (Boolean.TRUE.equals(settings.get(Keys.ENABLED))) { + return (String) settings.get(key); + } + } + return ""; + } + + public Map getSettingsMap() { + Map map = new HashMap<>(); + for (Keys key : Keys.values()) { + map.put(key.elementId(), get(key)); + } + return map; + } + + @Override + public String toString() { + return "DeveloperSettings" + settings; + } + + /* For debugging. */ + private void dumpParameterMap(Map parameterMap) { + Map> map = new HashMap<>(); + for (String key : parameterMap.keySet()) { + map.put(key, Arrays.asList(parameterMap.get(key))); + } + log.debug("Parameter map: " + map); + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettingsServlet.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettingsServlet.java new file mode 100644 index 000000000..7422137ab --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettingsServlet.java @@ -0,0 +1,93 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.developer; + +import static edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings.Keys.PERMIT_ANONYMOUS_CONTROL; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission; +import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.ajax.VitroAjaxController; +import edu.cornell.mannlib.vitro.webapp.services.freemarker.FreemarkerProcessingService.TemplateProcessingException; +import edu.cornell.mannlib.vitro.webapp.services.freemarker.FreemarkerProcessingServiceSetup; + +/** + * Accept an AJAX request to update the developer settings. Return an HTML + * representation of the developer panel from the settings and a Freemarker + * template. + * + * If developer mode is not enabled, the HTML response is empty. + * + * You may only control the panel if you are logged in with sufficient + * authorization, or if anonymous control is permitted by the settings. + * + * If you are not allowed to control the panel, then the HTML response + * is only a statement that developer mode is enabled. Otherwise, it + * is a full panel (collapsed at first). + */ +public class DeveloperSettingsServlet extends VitroAjaxController { + private static final Log log = LogFactory + .getLog(DeveloperSettingsServlet.class); + + @Override + protected void doRequest(VitroRequest vreq, HttpServletResponse resp) + throws ServletException, IOException { + DeveloperSettings settings = DeveloperSettings.getBean(vreq); + + /* + * Are they allowed to control the panel? + */ + if (isAuthorized(vreq)) { + // Update the settings. + settings.updateFromRequest(vreq.getParameterMap()); + } else { + log.debug("Not authorized to update settings."); + } + + /* + * Build the response. + */ + try { + Map bodyMap = buildBodyMap(isAuthorized(vreq), + settings); + String rendered = renderTemplate(vreq, bodyMap); + resp.getWriter().write(rendered); + } catch (Exception e) { + doError(resp, e.toString(), 500); + } + } + + private Map buildBodyMap(boolean authorized, + DeveloperSettings settings) { + Map settingsMap = new HashMap<>(); + settingsMap.putAll(settings.getSettingsMap()); + settingsMap.put("mayControl", authorized); + Map bodyMap = new HashMap<>(); + bodyMap.put("settings", settingsMap); + return bodyMap; + } + + private String renderTemplate(VitroRequest vreq, Map bodyMap) + throws TemplateProcessingException { + return FreemarkerProcessingServiceSetup.getService(getServletContext()) + .renderTemplate("developerPanel.ftl", bodyMap, vreq); + } + + private boolean isAuthorized(VitroRequest vreq) { + boolean authBySetting = DeveloperSettings.getBean(vreq).getBoolean( + PERMIT_ANONYMOUS_CONTROL); + boolean authByPolicy = PolicyHelper.isAuthorizedForActions(vreq, + SimplePermission.ENABLE_DEVELOPER_PANEL.ACTION); + return authBySetting || authByPolicy; + } +} 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 deleted file mode 100644 index 60f6c5af6..000000000 --- a/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FlatteningTemplateLoaderTest.java +++ /dev/null @@ -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 - * - *
-	 * 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 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); - } - -} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/freemarker/loader/FreemarkerTemplateLoaderTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/freemarker/loader/FreemarkerTemplateLoaderTest.java new file mode 100644 index 000000000..5b4d8b40d --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/freemarker/loader/FreemarkerTemplateLoaderTest.java @@ -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 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 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 matches = runTheVisitor(stPp.base + + stPp.language + stPp.extension); + if (!matches.isEmpty()) { + actualBestFit = matches.last().path.toString(); + } + } + if (actualBestFit == null) { + actualNumberOfTries++; + SortedSet 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 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; + } + + } +} diff --git a/webapp/test/stubs/edu/cornell/mannlib/vitro/webapp/i18n/I18nStub.java b/webapp/test/stubs/edu/cornell/mannlib/vitro/webapp/i18n/I18nStub.java index 4fd6b7add..fbc802b5a 100644 --- a/webapp/test/stubs/edu/cornell/mannlib/vitro/webapp/i18n/I18nStub.java +++ b/webapp/test/stubs/edu/cornell/mannlib/vitro/webapp/i18n/I18nStub.java @@ -51,7 +51,7 @@ public class I18nStub extends I18n { private class I18nBundleStub extends I18nBundle { public I18nBundleStub(String bundleName) { - super(bundleName, new DummyResourceBundle()); + super(bundleName, new DummyResourceBundle(), null); } @Override diff --git a/webapp/themes/vitro/templates/menu.ftl b/webapp/themes/vitro/templates/menu.ftl index 09bb02473..badb45f0a 100644 --- a/webapp/themes/vitro/templates/menu.ftl +++ b/webapp/themes/vitro/templates/menu.ftl @@ -2,6 +2,8 @@ +<#include "developer.ftl"> +