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..78a1d2a72 --- /dev/null +++ b/webapp/config/example.developer.properties @@ -0,0 +1,135 @@ +# +# ----------------------------------------------------------------------------- +# 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 + + +#------------------------------------------------------------------------------ +# Page configuration +#------------------------------------------------------------------------------ + +# +# Turn on logging of custom list view configuration files. Each time a property +# uses a list view other than the default, note it in the log. The default is +# 'false'. +# +# developer.pageContents.logCustomListView = true + +# +# Turn on logging of custom short views. Each time an individual uses a short +# view other than the default, note it in the log. The default is 'false'. +# +# developer.pageContents.logCustomShortView = 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, restrict the number of log entries by +# matching a regular expression against the query string. If the expression +# doesn't match the string, then no log entry is made. The default is "", +# which means no restriction. +# +# developer.loggingRDFService.queryRestriction = .* + +# +# If SPARQL query logging is enabled, restrict the number of log entries by +# matching a regular expression against the stack trace. The abridged stack +# trace is concatenated into a single string of fully qualified class names +# and method names. If the expression doesn't match the string, then no log +# entry is made. The default is "", which means no restriction. +# +# developer.loggingRDFService.stackRestriction = .* 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/languages/example/i18n/all_es.properties b/webapp/languages/es_GO/i18n/all_es_GO.properties similarity index 99% rename from webapp/languages/example/i18n/all_es.properties rename to webapp/languages/es_GO/i18n/all_es_GO.properties index 8c8d081a5..5544aafb0 100644 --- a/webapp/languages/example/i18n/all_es.properties +++ b/webapp/languages/es_GO/i18n/all_es_GO.properties @@ -444,7 +444,7 @@ please_create = Por favor, cree a_classgroup = un grupo de clase associate_classes_with_group = y las clases asociadas con el grupo creado. -refresh_content = Actualizar contenido +site_maintenance = Mantenimiento del sitio rebuild_search_index = Reconstruir índice de búsqueda rebuild_vis_cache = Reconstruir caché de visualización recompute_inferences_mixed_caps = Inferencias Recompute @@ -481,6 +481,9 @@ restrict_logins_mixed_caps = Restringir conexiones site_information = Información del sitio user_accounts = Las cuentas de usuario +activate_developer_panel = Activar el panel desarrollador +activate_developer_panel_mixed_caps = Activar el panel desarrollador + # # search controller ( PagedSearchController.java ) # diff --git a/webapp/languages/example/templates/freemarker/search-help_es.ftl b/webapp/languages/es_GO/templates/freemarker/search-help_es_GO.ftl similarity index 100% rename from webapp/languages/example/templates/freemarker/search-help_es.ftl rename to webapp/languages/es_GO/templates/freemarker/search-help_es_GO.ftl diff --git a/webapp/languages/example/templates/freemarker/termsOfUse_es.ftl b/webapp/languages/es_GO/templates/freemarker/termsOfUse_es_GO.ftl similarity index 100% rename from webapp/languages/example/templates/freemarker/termsOfUse_es.ftl rename to webapp/languages/es_GO/templates/freemarker/termsOfUse_es_GO.ftl diff --git a/webapp/languages/example/templates/freemarker/userAccounts-acctCreatedEmail.ftl b/webapp/languages/es_GO/templates/freemarker/userAccounts-acctCreatedEmail_es_GO.ftl similarity index 100% rename from webapp/languages/example/templates/freemarker/userAccounts-acctCreatedEmail.ftl rename to webapp/languages/es_GO/templates/freemarker/userAccounts-acctCreatedEmail_es_GO.ftl diff --git a/webapp/languages/example/templates/freemarker/userAccounts-acctCreatedExternalOnlyEmail.ftl b/webapp/languages/es_GO/templates/freemarker/userAccounts-acctCreatedExternalOnlyEmail_es_GO.ftl similarity index 100% rename from webapp/languages/example/templates/freemarker/userAccounts-acctCreatedExternalOnlyEmail.ftl rename to webapp/languages/es_GO/templates/freemarker/userAccounts-acctCreatedExternalOnlyEmail_es_GO.ftl diff --git a/webapp/languages/example/templates/freemarker/userAccounts-confirmEmailChangedEmail.ftl b/webapp/languages/es_GO/templates/freemarker/userAccounts-confirmEmailChangedEmail_es_GO.ftl similarity index 100% rename from webapp/languages/example/templates/freemarker/userAccounts-confirmEmailChangedEmail.ftl rename to webapp/languages/es_GO/templates/freemarker/userAccounts-confirmEmailChangedEmail_es_GO.ftl diff --git a/webapp/languages/example/templates/freemarker/userAccounts-firstTimeExternalEmail.ftl b/webapp/languages/es_GO/templates/freemarker/userAccounts-firstTimeExternalEmail_es_GO.ftl similarity index 100% rename from webapp/languages/example/templates/freemarker/userAccounts-firstTimeExternalEmail.ftl rename to webapp/languages/es_GO/templates/freemarker/userAccounts-firstTimeExternalEmail_es_GO.ftl diff --git a/webapp/languages/example/templates/freemarker/userAccounts-passwordCreatedEmail.ftl b/webapp/languages/es_GO/templates/freemarker/userAccounts-passwordCreatedEmail_es_GO.ftl similarity index 100% rename from webapp/languages/example/templates/freemarker/userAccounts-passwordCreatedEmail.ftl rename to webapp/languages/es_GO/templates/freemarker/userAccounts-passwordCreatedEmail_es_GO.ftl diff --git a/webapp/languages/example/templates/freemarker/userAccounts-passwordResetCompleteEmail.ftl b/webapp/languages/es_GO/templates/freemarker/userAccounts-passwordResetCompleteEmail_es_GO.ftl similarity index 100% rename from webapp/languages/example/templates/freemarker/userAccounts-passwordResetCompleteEmail.ftl rename to webapp/languages/es_GO/templates/freemarker/userAccounts-passwordResetCompleteEmail_es_GO.ftl diff --git a/webapp/languages/example/templates/freemarker/userAccounts-passwordResetPendingEmail.ftl b/webapp/languages/es_GO/templates/freemarker/userAccounts-passwordResetPendingEmail_es_GO.ftl similarity index 100% rename from webapp/languages/example/templates/freemarker/userAccounts-passwordResetPendingEmail.ftl rename to webapp/languages/es_GO/templates/freemarker/userAccounts-passwordResetPendingEmail_es_GO.ftl 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/BaseSiteAdminController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/BaseSiteAdminController.java index 69d25aa44..1a10c92a3 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/BaseSiteAdminController.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/BaseSiteAdminController.java @@ -53,14 +53,14 @@ public class BaseSiteAdminController extends FreemarkerHttpServlet { body.put("dataInput", getDataInputData(vreq)); body.put("siteConfig", getSiteConfigData(vreq)); - body.put("indexCacheRebuild", getIndexCacheRebuildUrls(vreq)); + body.put("siteMaintenance", getSiteMaintenanceUrls(vreq)); body.put("ontologyEditor", getOntologyEditorData(vreq)); body.put("dataTools", getDataToolsUrls(vreq)); return new TemplateResponseValues(TEMPLATE_DEFAULT, body); } - protected Map getIndexCacheRebuildUrls(VitroRequest vreq) { + protected Map getSiteMaintenanceUrls(VitroRequest vreq) { Map urls = new HashMap(); @@ -73,6 +73,14 @@ public class BaseSiteAdminController extends FreemarkerHttpServlet { urls.put("rebuildSearchIndex", UrlBuilder.getUrl("/SearchIndex")); } + if (PolicyHelper.isAuthorizedForActions(vreq, SimplePermission.LOGIN_DURING_MAINTENANCE.ACTIONS)) { + urls.put("restrictLogins", UrlBuilder.getUrl("/admin/restrictLogins")); + } + + if (PolicyHelper.isAuthorizedForActions(vreq, SimplePermission.ENABLE_DEVELOPER_PANEL.ACTIONS)) { + urls.put("activateDeveloperPanel", "javascript:new DeveloperPanel(developerAjaxUrl).setupDeveloperPanel({developerEnabled: true});"); + } + return urls; } @@ -143,10 +151,6 @@ public class BaseSiteAdminController extends FreemarkerHttpServlet { data.put("startupStatusAlert", !StartupStatus.getBean(getServletContext()).allClear()); } - if (PolicyHelper.isAuthorizedForActions(vreq, SimplePermission.LOGIN_DURING_MAINTENANCE.ACTIONS)) { - data.put("restrictLogins", UrlBuilder.getUrl("/admin/restrictLogins")); - } - return data; } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DeletePropertyController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DeletePropertyController.java index 4f1bbd1c4..5586b8cb3 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DeletePropertyController.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DeletePropertyController.java @@ -167,21 +167,21 @@ public class DeletePropertyController extends FreemarkerHttpServlet { } - - - - //process object property private void processObjectProperty(VitroRequest vreq) { ObjectProperty prop = EditConfigurationUtils.getObjectProperty(vreq); //if this property is true, it means the object needs to be deleted along with statement - if(prop.getStubObjectRelation()) + //while the second test is to see if a different object uri (i.e. not the direct objet of the predicate) + //needs to be deleted + if(prop.getStubObjectRelation() || hasDeleteObjectUri(vreq)) { deleteObjectIndividual(vreq); } - deleteObjectPropertyStatement(vreq); + if(!hasDeleteObjectUri(vreq)) { + deleteObjectPropertyStatement(vreq); + } } @@ -194,7 +194,7 @@ public class DeletePropertyController extends FreemarkerHttpServlet { wdf.getPropertyInstanceDao().deleteObjectPropertyStatement(subjectUri, predicateUri, objectUri); } - private Individual getObjectIndividualForStubRelation(VitroRequest vreq, String objectUri) { + private Individual getObjectIndividualForDeletion(VitroRequest vreq, String objectUri) { Individual object = EditConfigurationUtils.getIndividual(vreq, objectUri); if(object == null) { @@ -208,9 +208,13 @@ public class DeletePropertyController extends FreemarkerHttpServlet { private void deleteObjectIndividual(VitroRequest vreq) { String objectUri = EditConfigurationUtils.getObjectUri(vreq); - Individual object = getObjectIndividualForStubRelation(vreq, objectUri); + if(hasDeleteObjectUri(vreq)) { + //if a different individual needs to be deleted, get that uri instead + objectUri = getDeleteObjectUri(vreq); + } + Individual object = getObjectIndividualForDeletion(vreq, objectUri); if(object != null) { - log.warn("Deleting individual " + object.getName() + "since property has been set to force range object deletion"); + log.warn("Deleting individual " + object.getName() + "since property has been set to force range object deletion or has been set to delete a specific object"); WebappDaoFactory wdf = vreq.getWebappDaoFactory(); wdf.getIndividualDao().deleteIndividual(object); } else { @@ -218,6 +222,16 @@ public class DeletePropertyController extends FreemarkerHttpServlet { log.error("could not find object as request attribute or in model " + objectUri); } } + + //This checks if the object uri is not the individual to be deleted but another individual connected + private String getDeleteObjectUri(VitroRequest vreq) { + return (String) vreq.getParameter("deleteObjectUri"); + } + + private boolean hasDeleteObjectUri(VitroRequest vreq) { + String deleteObjectUri = getDeleteObjectUri(vreq); + return (deleteObjectUri != null && !deleteObjectUri.isEmpty()); + } 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/controller/freemarker/SimpleReasonerRecomputeController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/SimpleReasonerRecomputeController.java index fd16e2a75..47181cf4b 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/SimpleReasonerRecomputeController.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/SimpleReasonerRecomputeController.java @@ -56,7 +56,7 @@ public class SimpleReasonerRecomputeController extends FreemarkerHttpServlet { "SimpleReasonerRecomputController.Recomputer"); thread.setWorkLevel(WORKING); thread.start(); - messageStr = "Recompute of inferences started. See vivo log for further details."; + messageStr = "Recompute of inferences started. See log for further details."; } else { body.put("formAction", UrlBuilder.getUrl("/RecomputeInferences")); } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/ObjectPropertyDaoJena.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/ObjectPropertyDaoJena.java index ecdc2bfbc..7d0e42509 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/ObjectPropertyDaoJena.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/ObjectPropertyDaoJena.java @@ -71,8 +71,7 @@ public class ObjectPropertyDaoJena extends PropertyDaoJena implements ObjectProp } public void deleteObjectProperty(String propertyURI) { - ObjectProperty op = new ObjectProperty(); - op.setURI(propertyURI); + ObjectProperty op = getObjectPropertyByURI(propertyURI); deleteObjectProperty(op); } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/QueryUtils.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/QueryUtils.java index f2c686657..c056a28ec 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/QueryUtils.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/QueryUtils.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -116,7 +117,11 @@ public class QueryUtils { return getQueryResults(queryStr, vreq.getRDFService()); } - public static ResultSet getLanguageNeutralQueryResults(String queryStr, VitroRequest vreq) { + public static ResultSet getQueryResults(String queryStr, QuerySolution initialBindings, RDFService rdfService) { + return getQueryResults(bindVariables(queryStr, initialBindings), rdfService); + } + + public static ResultSet getLanguageNeutralQueryResults(String queryStr, VitroRequest vreq) { return getQueryResults(queryStr, vreq.getUnfilteredRDFService()); } @@ -130,4 +135,38 @@ public class QueryUtils { } } + /** + * The RDFService interface doesn't support initial bindings, so do text + * substitutions instead. + */ + public static String bindVariables(String queryStr, + QuerySolution initialBindings) { + String bound = queryStr; + for (Iterator it = initialBindings.varNames(); it.hasNext();) { + String name = it.next(); + RDFNode node = initialBindings.get(name); + if (node.isLiteral()) { + bound = bound.replace('?' + name, literalToString(node.asLiteral())); + } else if (node.isURIResource()) { + bound = bound.replace('?' + name, '<'+node.asResource().getURI()+ '>'); + }else { + log.warn("Failed to bind anonymous resource variable '" + name + + "' to query '" + bound + "'"); + } + } + return bound; + } + + private static String literalToString(Literal l) { + StringBuilder buffer = new StringBuilder(); + buffer.append('"').append(l.getLexicalForm()).append('"'); + if (l.getDatatypeURI() != null) { + buffer.append("^^<").append(l.getDatatypeURI()).append(">"); + } else if (StringUtils.isNotEmpty(l.getLanguage())) { + buffer.append("@").append(l.getLanguage()); + } + return buffer.toString(); + } + + } 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/edit/n3editing/configuration/generators/DefaultAddMissingIndividualFormGenerator.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/generators/DefaultAddMissingIndividualFormGenerator.java index 2f53d3619..0a8a866a3 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/generators/DefaultAddMissingIndividualFormGenerator.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/generators/DefaultAddMissingIndividualFormGenerator.java @@ -56,7 +56,10 @@ public class DefaultAddMissingIndividualFormGenerator implements EditConfigurati public static boolean isCreateNewIndividual(VitroRequest vreq, HttpSession session) { String command = vreq.getParameter("cmd"); String predicateUri = EditConfigurationUtils.getPredicateUri(vreq); - ObjectProperty objProp = vreq.getWebappDaoFactory().getObjectPropertyDao().getObjectPropertyByURI(predicateUri); + //This method also looks at domain and range uris and so is different than just getting the + //object property based on predicate uri alone + ObjectProperty objProp = EditConfigurationUtils.getObjectPropertyForPredicate(vreq, + predicateUri); if(objProp != null) { return(objProp.getOfferCreateNewOption() && ( 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..2ae556862 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/loader/FreemarkerTemplateLoader.java @@ -0,0 +1,323 @@ +/* $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 java.util.regex.Matcher; +import java.util.regex.Pattern; + +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 { + static final Pattern PATTERN = Pattern.compile("(.+?)" // base name + + "(_[a-z]{2})?" // optional language + + "(_[A-Z]{2})?" // optional country + + "(\\.\\w+)?" // optional extension + ); + + final Path path; + final String base; + final String language; + final String region; + final String extension; + + public PathPieces(String pathString) { + this(Paths.get(pathString)); + } + + public PathPieces(Path path) { + this.path = path; + + String filename = path.getFileName().toString(); + + Matcher m = PATTERN.matcher(filename); + if (m.matches()) { + base = getGroup(m, 1); + language = getGroup(m, 2); + region = getGroup(m, 3); + extension = getGroup(m, 4); + } else { + base = filename; + language = ""; + region = ""; + extension = ""; + } + } + + private String getGroup(Matcher m, int i) { + return (m.start(i) == -1) ? "" : m.group(i); + } + + /** + * If I'm searching for this, is that an acceptable match? + * + * Note that this is asymetrical -- a search term without a region will + * match a candidate with a region, but not vice versa. Same with + * language. + */ + 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)); + } + + /** + * How good a match is that to this? + */ + public int score(PathPieces that) { + if (matches(that)) { + if (that.language.equals(language)) { + if (that.region.equals(region)) { + return 3; // exact match. + } else { + return 2; // same language, approximate region. + } + } else { + return 1; // approximate 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..43d33c55e 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java @@ -3,10 +3,14 @@ package edu.cornell.mannlib.vitro.webapp.i18n; import java.io.IOException; +import java.util.Comparator; +import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.concurrent.atomic.AtomicReference; import javax.servlet.ServletContext; @@ -15,8 +19,10 @@ 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.i18n.selection.SelectedLocale; +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 +37,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 +108,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 +119,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 +133,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); } @@ -170,7 +172,7 @@ public class I18n { * Instead of looking in the classpath, look in the theme i18n directory and * the application i18n directory. */ - private static class ThemeBasedControl extends ResourceBundle.Control { + static class ThemeBasedControl extends ResourceBundle.Control { private static final String BUNDLE_DIRECTORY = "i18n/"; private final ServletContext ctx; private final String themeDirectory; @@ -218,6 +220,52 @@ public class I18n { themeI18nPath, this); } + /** + * When creating the chain of acceptable Locales, include approximate + * matches before giving up and using the root Locale. + * + * Check the list of supported Locales to see if any have the same + * language but different region. If we find any, sort them and insert + * them into the usual result list, just before the root Locale. + */ + @Override + public List getCandidateLocales(String baseName, Locale locale) { + // Find the list of Locales that would normally be returned. + List usualList = super + .getCandidateLocales(baseName, locale); + + // If our "selectable locales" include no approximate matches that + // are not already in the list, we're done. + SortedSet approximateMatches = findApproximateMatches(locale); + approximateMatches.removeAll(usualList); + if (approximateMatches.isEmpty()) { + return usualList; + } + + // Otherwise, insert those approximate matches into the list just + // before the ROOT locale. + List mergedList = new LinkedList<>(usualList); + int rootLocaleHere = mergedList.indexOf(Locale.ROOT); + if (rootLocaleHere == -1) { + mergedList.addAll(approximateMatches); + } else { + mergedList.addAll(rootLocaleHere, approximateMatches); + } + return mergedList; + } + + private SortedSet findApproximateMatches(Locale locale) { + SortedSet set = new TreeSet<>(new LocaleComparator()); + + for (Locale l : SelectedLocale.getSelectableLocales(ctx)) { + if (locale.getLanguage().equals(l.getLanguage())) { + set.add(l); + } + } + + return set; + } + /** * The documentation for ResourceBundle.Control.newBundle() says I * should throw these exceptions. @@ -238,5 +286,20 @@ public class I18n { "format must be one of these: " + FORMAT_DEFAULT); } } + + } + + private static class LocaleComparator implements Comparator { + @Override + public int compare(Locale o1, Locale o2) { + int c = o1.getLanguage().compareTo(o2.getLanguage()); + if (c == 0) { + c = o1.getCountry().compareTo(o2.getCountry()); + if (c == 0) { + c = o1.getVariant().compareTo(o2.getVariant()); + } + } + return c; + } } } 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/i18n/selection/LocaleSelectionSetup.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionSetup.java index c89819421..081019a04 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionSetup.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionSetup.java @@ -134,7 +134,8 @@ public class LocaleSelectionSetup implements ServletContextListener { throws IllegalArgumentException { Locale locale = LocaleUtils.toLocale(localeString); - if (!LocaleUtils.isAvailableLocale(locale)) { + if (!"es_GO".equals(localeString) && // No complaint about bogus locale + !LocaleUtils.isAvailableLocale(locale)) { ssWarning("'" + locale + "' is not a recognized locale."); } return locale; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/SelectedLocale.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/SelectedLocale.java index 7ae4586cb..8356af109 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/SelectedLocale.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/SelectedLocale.java @@ -2,6 +2,7 @@ package edu.cornell.mannlib.vitro.webapp.i18n.selection; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -124,7 +125,14 @@ public abstract class SelectedLocale { * an empty list, but never returns null. */ public static List getSelectableLocales(HttpServletRequest req) { - ServletContext ctx = req.getSession().getServletContext(); + return getSelectableLocales(req.getSession().getServletContext()); + } + + /** + * Get the list of selectable Locales from the servlet context. May return + * an empty list, but never returns null. + */ + public static List getSelectableLocales(ServletContext ctx) { Object ctxInfo = ctx.getAttribute(ATTRIBUTE_NAME); if (ctxInfo instanceof ContextSelectedLocale) { List selectableLocales = ((ContextSelectedLocale) ctxInfo) @@ -164,7 +172,8 @@ public abstract class SelectedLocale { } this.forcedLocale = null; - this.selectableLocales = selectableLocales; + this.selectableLocales = Collections + .unmodifiableList(new ArrayList<>(selectableLocales)); } public Locale getForcedLocale() { 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 3e685ed1e..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()) { @@ -346,7 +355,9 @@ public class KnowledgeBaseUpdater { } public static boolean isUpdatableABoxGraph(String graphName) { - return (!graphName.contains("tbox") && !graphName.contains("filegraph")); + return (graphName != null && !graphName.contains("tbox") + && !graphName.contains("filegraph") + && !graphName.contains("x-arq:UnionGraph")); } /** 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..c0950e967 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,16 +15,20 @@ 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. * * If not enabled, or if the logging level is insufficient, this does nothing. * - * If enabled, it checks for restrictions. If there is a restriction pattern - * (regular expression), the a log message will only be printed if one of the - * fully-qualified class names in the stack trace matches that pattern. + * If enabled, it checks for restrictions. If there is a restriction on the call + * stack (regular expression), then a log message will only be printed if the + * pattern is found in the concatenated call stack (fully-qualified class names + * and method names). If there is a restriction on the query string (regular + * expression) then a log message will only be printed if the pattern is found + * in the query string. * * If everything passes muster, the constructor will record the time that the * instance was created. @@ -41,16 +45,13 @@ 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; private boolean isEnabled; private boolean traceRequested; - private Pattern restriction; + private Pattern queryStringRestriction; + private Pattern callStackRestriction; private String methodName; private List trace = Collections.emptyList(); @@ -65,27 +66,33 @@ public class RDFServiceLogger implements AutoCloseable { if (isEnabled && log.isInfoEnabled()) { loadStackTrace(); - if (passesRestrictions()) { + if (passesQueryRestriction() && passesStackRestriction()) { this.startTime = System.currentTimeMillis(); } } } 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); + queryStringRestriction = patternFromSettings(settings, + Keys.LOGGING_RDF_QUERY_RESTRICTION); + callStackRestriction = patternFromSettings(settings, + Keys.LOGGING_RDF_STACK_RESTRICTION); + } - String restrictionString = props.getProperty(PROPERTY_RESTRICTION); - if (StringUtils.isNotBlank(restrictionString)) { - try { - restriction = Pattern.compile(restrictionString); - } catch (Exception e) { - log.error("Failed to compile the pattern for " - + PROPERTY_RESTRICTION + " = " + restriction + " " + e); - isEnabled = false; - } + private Pattern patternFromSettings(DeveloperSettings settings, Keys key) { + String patternString = settings.getString(key); + if (StringUtils.isBlank(patternString)) { + return null; + } + try { + return Pattern.compile(patternString); + } catch (Exception e) { + log.error("Failed to compile the pattern for " + key + " = " + + patternString + " " + e); + return Pattern.compile("^_____NEVER MATCH_____$"); } } @@ -144,16 +151,39 @@ public class RDFServiceLogger implements AutoCloseable { } } - private boolean passesRestrictions() { - if (restriction == null) { + private boolean passesQueryRestriction() { + if (queryStringRestriction == null) { return true; } - for (StackTraceElement ste : trace) { - if (restriction.matcher(ste.getClassName()).find()) { - return true; + String q = assembleQueryString(); + return queryStringRestriction.matcher(q).find(); + } + + private String assembleQueryString() { + StringBuilder query = new StringBuilder(); + for (Object arg : args) { + if (arg instanceof String) { + query.append((String) arg).append(" "); } } - return false; + return query.deleteCharAt(query.length() - 1).toString(); + } + + private boolean passesStackRestriction() { + if (callStackRestriction == null) { + return true; + } + String q = assembleCallStackString(); + return callStackRestriction.matcher(q).find(); + } + + private String assembleCallStackString() { + StringBuilder stack = new StringBuilder(); + for (StackTraceElement ste : trace) { + stack.append(ste.getClassName()).append(" ") + .append(ste.getMethodName()).append(" "); + } + return stack.deleteCharAt(stack.length() - 1).toString(); } @Override 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..b20c6cba3 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 @@ -4,6 +4,7 @@ package edu.cornell.mannlib.vitro.webapp.rdfservice.impl.sparql; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.util.ArrayList; @@ -14,7 +15,9 @@ import java.util.concurrent.ConcurrentLinkedQueue; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; import org.apache.commons.httpclient.NameValuePair; +import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.params.HttpMethodParams; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -38,6 +41,7 @@ import com.hp.hpl.jena.rdf.model.ModelFactory; import com.hp.hpl.jena.rdf.model.RDFNode; import com.hp.hpl.jena.rdf.model.Statement; import com.hp.hpl.jena.rdf.model.StmtIterator; +import com.hp.hpl.jena.sparql.engine.http.QueryEngineHTTP; import edu.cornell.mannlib.vitro.webapp.dao.jena.SparqlGraph; import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeListener; @@ -84,11 +88,11 @@ public class RDFServiceSparql extends RDFServiceImpl implements RDFService { this.readRepository = new HTTPRepository(readEndpointURI); this.updateRepository = new HTTPRepository(updateEndpointURI); - testConnection(); - MultiThreadedHttpConnectionManager mgr = new MultiThreadedHttpConnectionManager(); - mgr.getParams().setDefaultMaxConnectionsPerHost(10); + mgr.getParams().setDefaultMaxConnectionsPerHost(50); this.httpClient = new HttpClient(mgr); + + testConnection(); } private void testConnection() { @@ -278,13 +282,28 @@ public class RDFServiceSparql extends RDFServiceImpl implements RDFService { */ @Override public InputStream sparqlSelectQuery(String queryStr, RDFService.ResultFormat resultFormat) throws RDFServiceException { - - Query query = createQuery(queryStr); - QueryExecution qe = QueryExecutionFactory.sparqlService(readEndpointURI, query); - + + //QueryEngineHTTP qh = new QueryEngineHTTP(readEndpointURI, queryStr); + + GetMethod meth = new GetMethod(readEndpointURI); try { - ResultSet resultSet = qe.execSelect(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + meth.addRequestHeader("Accept", "application/sparql-results+xml"); + NameValuePair param = new NameValuePair(); + param.setName("query"); + param.setValue(queryStr); + NameValuePair[] params = new NameValuePair[1]; + params[0] = param; + meth.setQueryString(params); + int response = httpClient.executeMethod(meth); + if (response > 399) { + log.error("response " + response + " to query. \n"); + log.debug("update string: \n" + queryStr); + throw new RDFServiceException("Unable to perform SPARQL UPDATE"); + } + + InputStream in = meth.getResponseBodyAsStream(); + ResultSet resultSet = ResultSetFactory.fromXML(in); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); switch (resultFormat) { case CSV: @@ -301,12 +320,14 @@ public class RDFServiceSparql extends RDFServiceImpl implements RDFService { break; default: throw new RDFServiceException("unrecognized result format"); - } - + } InputStream result = new ByteArrayInputStream(outputStream.toByteArray()); return result; + } catch (IOException ioe) { + throw new RuntimeException(ioe); } finally { - qe.close(); + //qh.close(); + meth.releaseConnection(); } } @@ -474,7 +495,7 @@ public class RDFServiceSparql extends RDFServiceImpl implements RDFService { int response = httpClient.executeMethod(meth); if (response > 399) { log.error("response " + response + " to update. \n"); - log.debug("update string: \n" + updateString); + //log.debug("update string: \n" + updateString); throw new RDFServiceException("Unable to perform SPARQL UPDATE"); } } finally { @@ -744,6 +765,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/search/indexing/AdditionalURIsForObjectProperties.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/indexing/AdditionalURIsForObjectProperties.java index ba6d6d1e8..38fdf1ea7 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/indexing/AdditionalURIsForObjectProperties.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/indexing/AdditionalURIsForObjectProperties.java @@ -10,22 +10,17 @@ import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import com.hp.hpl.jena.query.Query; -import com.hp.hpl.jena.query.QueryExecution; -import com.hp.hpl.jena.query.QueryExecutionFactory; -import com.hp.hpl.jena.query.QueryFactory; import com.hp.hpl.jena.query.QuerySolution; import com.hp.hpl.jena.query.QuerySolutionMap; import com.hp.hpl.jena.query.ResultSet; -import com.hp.hpl.jena.query.Syntax; -import com.hp.hpl.jena.rdf.model.Model; import com.hp.hpl.jena.rdf.model.RDFNode; import com.hp.hpl.jena.rdf.model.Resource; import com.hp.hpl.jena.rdf.model.ResourceFactory; import com.hp.hpl.jena.rdf.model.Statement; -import com.hp.hpl.jena.shared.Lock; import com.hp.hpl.jena.vocabulary.RDFS; +import edu.cornell.mannlib.vitro.webapp.dao.jena.QueryUtils; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; import edu.cornell.mannlib.vitro.webapp.search.beans.StatementToURIsToUpdate; /** @@ -38,13 +33,13 @@ import edu.cornell.mannlib.vitro.webapp.search.beans.StatementToURIsToUpdate; public class AdditionalURIsForObjectProperties implements StatementToURIsToUpdate { protected static final Log log = LogFactory.getLog(AdditionalURIsForObjectProperties.class); - protected Model model; + protected final RDFService rdfService; - public AdditionalURIsForObjectProperties( Model model){ - this.model = model; - } - - @Override + public AdditionalURIsForObjectProperties(RDFService rdfService) { + this.rdfService = rdfService; + } + + @Override public List findAdditionalURIsToIndex(Statement stmt) { if( stmt == null ) return Collections.emptyList(); @@ -102,37 +97,27 @@ public class AdditionalURIsForObjectProperties implements StatementToURIsToUpdat Resource uriResource = ResourceFactory.createResource(uri); initialBinding.add("uri", uriResource); - Query sparqlQuery = QueryFactory.create( QUERY_FOR_RELATED ); - model.getLock().enterCriticalSection(Lock.READ); - try{ - QueryExecution qExec = QueryExecutionFactory.create(sparqlQuery, model, initialBinding); - try{ - ResultSet results = qExec.execSelect(); - while(results.hasNext()){ - QuerySolution soln = results.nextSolution(); - Iterator iter = soln.varNames() ; - while( iter.hasNext()){ - String name = iter.next(); - RDFNode node = soln.get( name ); - if( node != null ){ - if( node.isURIResource() ){ - additionalUris.add( node.as( Resource.class ).getURI() ); - }else{ - log.warn( "value from query for var " + name + " was not a URIResource, it was " + node); - } - }else{ - log.warn("value for query for var " + name + " was null"); - } + ResultSet results = QueryUtils.getQueryResults(QUERY_FOR_RELATED, + initialBinding, rdfService); + + while(results.hasNext()){ + QuerySolution soln = results.nextSolution(); + Iterator iter = soln.varNames() ; + while( iter.hasNext()){ + String name = iter.next(); + RDFNode node = soln.get( name ); + if( node != null ){ + if( node.isURIResource() ){ + additionalUris.add( node.as( Resource.class ).getURI() ); + }else{ + log.warn( "value from query for var " + name + " was not a URIResource, it was " + node); } - } - }catch(Throwable t){ - log.error(t,t); - } finally{ - qExec.close(); - } - }finally{ - model.getLock().leaveCriticalSection(); + }else{ + log.warn("value for query for var " + name + " was null"); + } + } } + return additionalUris; } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/indexing/AdditionalUriFinders.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/indexing/AdditionalUriFinders.java index 498d74334..4758de021 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/indexing/AdditionalUriFinders.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/indexing/AdditionalUriFinders.java @@ -5,9 +5,8 @@ package edu.cornell.mannlib.vitro.webapp.search.indexing; import java.util.ArrayList; import java.util.List; -import com.hp.hpl.jena.ontology.OntModel; - import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; import edu.cornell.mannlib.vitro.webapp.search.beans.StatementToURIsToUpdate; /** @@ -16,12 +15,12 @@ import edu.cornell.mannlib.vitro.webapp.search.beans.StatementToURIsToUpdate; */ public class AdditionalUriFinders { - public static List getList(OntModel jenaOntModel, + public static List getList(RDFService rdfService, IndividualDao indDao) { // TODO How many of these are only relevant to VIVO? List uriFinders = new ArrayList<>(); uriFinders.add(new AdditionalURIsForDataProperties()); - uriFinders.add(new AdditionalURIsForObjectProperties(jenaOntModel)); + uriFinders.add(new AdditionalURIsForObjectProperties(rdfService)); uriFinders.add(new AdditionalURIsForTypeStatements()); uriFinders.add(new URIsForClassGroupChange(indDao)); return uriFinders; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/solr/SolrSetup.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/solr/SolrSetup.java index 04ae6971d..883418cee 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/solr/SolrSetup.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/solr/SolrSetup.java @@ -23,6 +23,7 @@ import edu.cornell.mannlib.vitro.webapp.dao.filtering.WebappDaoFactoryFiltering; import edu.cornell.mannlib.vitro.webapp.dao.filtering.filters.VitroFilterUtils; import edu.cornell.mannlib.vitro.webapp.dao.filtering.filters.VitroFilters; import edu.cornell.mannlib.vitro.webapp.dao.jena.ModelContext; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceFactory; import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils; import edu.cornell.mannlib.vitro.webapp.search.beans.StatementToURIsToUpdate; @@ -133,7 +134,8 @@ public class SolrSetup implements javax.servlet.ServletContextListener{ wadf = new WebappDaoFactoryFiltering(wadf, vf); // make objects that will find additional URIs for context nodes etc - List uriFinders = AdditionalUriFinders.getList(jenaOntModel,wadf.getIndividualDao()); + RDFService rdfService = RDFServiceUtils.getRDFServiceFactory(context).getRDFService(); + List uriFinders = AdditionalUriFinders.getList(rdfService,wadf.getIndividualDao()); // Make the IndexBuilder IndexBuilder builder = new IndexBuilder( solrIndexer, wadf, uriFinders ); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/solr/documentBuilding/ExcludeNonFlagVitro.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/solr/documentBuilding/ExcludeNonFlagVitro.java index 3381423d0..fdd2df365 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/solr/documentBuilding/ExcludeNonFlagVitro.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/solr/documentBuilding/ExcludeNonFlagVitro.java @@ -1,44 +1,57 @@ /* $This file is distributed under the terms of the license in /doc/license.txt$ */ package edu.cornell.mannlib.vitro.webapp.search.solr.documentBuilding; +import static edu.cornell.mannlib.vitro.webapp.search.solr.documentBuilding.IndividualToSolrDocument.DONT_EXCLUDE; + import java.util.List; -import org.apache.solr.common.SolrInputDocument; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import edu.cornell.mannlib.vitro.webapp.beans.Individual; -import edu.cornell.mannlib.vitro.webapp.beans.VClass; import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; /** - * Exclude individuals with types from the Vitro namespace from the - * search index. (Other than old vitro Flag types). + * Exclude individuals with most specific types from the Vitro namespace from + * the search index. (Other than old vitro Flag types). */ public class ExcludeNonFlagVitro implements SearchIndexExcluder { + private static final Log log = LogFactory.getLog(ExcludeNonFlagVitro.class); - @Override - public String checkForExclusion(Individual ind) { - if( ind != null && ind.getVClasses() != null ) { - String excludeMsg = skipIfVitro(ind, ind.getVClasses() ); - if( excludeMsg != null) - return excludeMsg; - } - return null; - } + @Override + public String checkForExclusion(Individual ind) { + if (ind == null) { + return DONT_EXCLUDE; + } + + List mostSpecificTypeUris = ind.getMostSpecificTypeURIs(); + if (mostSpecificTypeUris == null) { + return DONT_EXCLUDE; + } + + String message = skipIfVitro(ind, mostSpecificTypeUris); + if (!StringUtils.equals(DONT_EXCLUDE, message)) { + log.debug("msg=" + message + ", individual=" + ind.getURI() + " (" + + ind.getLabel() + "), types=" + mostSpecificTypeUris); + } + return message; + } + + String skipIfVitro(Individual ind, List mostSpecificTypeUris) { + for (String typeUri : mostSpecificTypeUris) { + if (typeUri == null) { + continue; + } + if (typeUri.startsWith(VitroVocabulary.vitroURI + "Flag")) { + continue; + } + if (typeUri.startsWith(VitroVocabulary.vitroURI)) { + return "Skipped " + ind.getURI() + " because in " + + VitroVocabulary.vitroURI + " namespace"; + } + } + return DONT_EXCLUDE; + } - String skipIfVitro(Individual ind, List vclasses) { - for( VClass type: vclasses ){ - if( type != null && type.getURI() != null ){ - String typeURI = type.getURI(); - - if(typeURI.startsWith( VitroVocabulary.vitroURI ) - && ! typeURI.startsWith(VitroVocabulary.vitroURI + "Flag") ){ - - return "Skipped " + ind.getURI()+" because in " - + VitroVocabulary.vitroURI + " namespace"; - } - } - } - return null; - } - } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/solr/documentBuilding/IndividualToSolrDocument.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/solr/documentBuilding/IndividualToSolrDocument.java index c32bb2503..a2f62782c 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/solr/documentBuilding/IndividualToSolrDocument.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/solr/documentBuilding/IndividualToSolrDocument.java @@ -110,6 +110,9 @@ public class IndividualToSolrDocument { for( SearchIndexExcluder excluder : excludes){ try{ String msg = excluder.checkForExclusion(ind); + log.debug("individual=" + ind.getURI() + " (" + ind.getLabel() + + "), excluder=" + excluder + ", types=" + + ind.getMostSpecificTypeURIs() + ", msg=" + msg); if( msg != DONT_EXCLUDE) return msg; }catch (Exception e) { diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/services/shortview/FakeApplicationOntologyService.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/services/shortview/FakeApplicationOntologyService.java index 92fcc8ae7..a650620cc 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/services/shortview/FakeApplicationOntologyService.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/services/shortview/FakeApplicationOntologyService.java @@ -380,6 +380,12 @@ public class FakeApplicationOntologyService { return dataGetters; } + @Override + public String toString() { + return "[template=" + templateName + ", dataGetters=" + dataGetters + + "]"; + } + } /** The view specifications that we read from the config file. */ diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/services/shortview/ShortViewLogger.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/services/shortview/ShortViewLogger.java new file mode 100644 index 000000000..220185445 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/services/shortview/ShortViewLogger.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.services.shortview; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.services.shortview.FakeApplicationOntologyService.TemplateAndDataGetters; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings.Keys; + +/** + * When we use a short view other than the default, log it. + */ +public class ShortViewLogger { + private static final Log log = LogFactory.getLog(ShortViewLogger.class); + + public static void log(VitroRequest vreq, String contextName, + Individual individual, String classUri, TemplateAndDataGetters tdg) { + if (isLogging(vreq)) { + log.info("Using custom short view in " + contextName + " because '" + + individual.getURI() + "' (" + individual.getLabel() + + ") has type '" + classUri + "': " + tdg); + } + } + + public static void log(VitroRequest vreq, String contextName, + Individual individual) { + if (isLogging(vreq)) { + log.info("Using default short view in " + contextName + " for '" + + individual.getURI() + "' (" + individual.getLabel() + ")"); + } + } + + private static boolean isLogging(VitroRequest vreq) { + if (!log.isInfoEnabled()) { + return false; + } + DeveloperSettings settings = DeveloperSettings.getBean(vreq); + return settings.getBoolean(Keys.ENABLED) + && settings + .getBoolean(Keys.PAGE_CONTENTS_LOG_CUSTOM_SHORT_VIEW); + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/services/shortview/ShortViewServiceImpl.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/services/shortview/ShortViewServiceImpl.java index 8e18208a0..c072f866e 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/services/shortview/ShortViewServiceImpl.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/services/shortview/ShortViewServiceImpl.java @@ -15,9 +15,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import edu.cornell.mannlib.vitro.webapp.beans.Individual; -import edu.cornell.mannlib.vitro.webapp.beans.ObjectPropertyStatement; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; -import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; import edu.cornell.mannlib.vitro.webapp.services.freemarker.FreemarkerProcessingService; import edu.cornell.mannlib.vitro.webapp.services.freemarker.FreemarkerProcessingService.TemplateParsingException; import edu.cornell.mannlib.vitro.webapp.services.freemarker.FreemarkerProcessingService.TemplateProcessingException; @@ -116,11 +114,13 @@ public class ShortViewServiceImpl implements ShortViewService { TemplateAndDataGetters tdg = faker.getShortViewProperties(vreq, individual, classUri, svContext.name()); if (tdg != null) { + ShortViewLogger.log(vreq, svContext.name(), individual, classUri, tdg); return tdg; } } // Didn't find one? Use the default values. + ShortViewLogger.log(vreq, svContext.name(), individual); return new TemplateAndDataGetters(svContext.getDefaultTemplateName()); } 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/servlet/setup/UpdateKnowledgeBase.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/UpdateKnowledgeBase.java index 0f739dd5d..8c8f53533 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/UpdateKnowledgeBase.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/UpdateKnowledgeBase.java @@ -141,7 +141,7 @@ public class UpdateKnowledgeBase implements ServletContextListener { KnowledgeBaseUpdater ontologyUpdater = new KnowledgeBaseUpdater(settings); boolean requiredUpdate = ontologyUpdater.updateRequired(ctx); - if(!JenaDataSourceSetupBase.isFirstStartup()) { + if(requiredUpdate && !JenaDataSourceSetupBase.isFirstStartup()) { try { ctx.setAttribute(KBM_REQURIED_AT_STARTUP, Boolean.TRUE); migrationChangesMade = ontologyUpdater.update(ctx); 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..169491372 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettings.java @@ -0,0 +1,325 @@ +/* $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_QUERY_RESTRICTION( + "developer.loggingRDFService.queryRestriction", false), + + /** + * Don't log with the LoggingRDFService unless the calling stack meets + * this restriction. + */ + LOGGING_RDF_STACK_RESTRICTION( + "developer.loggingRDFService.stackRestriction", false), + + /** + * Tell the CustomListViewLogger to note the use of non-default custom + * list views. + */ + PAGE_CONTENTS_LOG_CUSTOM_LIST_VIEW( + "developer.pageContents.logCustomListView", true), + + /** + * Tell the ShortViewLogger to note the use of non-default short views. + */ + PAGE_CONTENTS_LOG_CUSTOM_SHORT_VIEW( + "developer.pageContents.logCustomShortView", true); + + 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/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/customlistview/CustomListViewLogger.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/customlistview/CustomListViewLogger.java new file mode 100644 index 000000000..2509af61c --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/customlistview/CustomListViewLogger.java @@ -0,0 +1,38 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.web.templatemodels.customlistview; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.beans.ObjectProperty; +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; + +/** + * If enabled in the developer settings (and log levels), log every non-default + * custom list view. + */ +public class CustomListViewLogger { + private static final Log log = LogFactory + .getLog(CustomListViewLogger.class); + + public static void log(VitroRequest vreq, ObjectProperty op, + String configFileName) { + if (isLogging(vreq)) { + log.info("Using list view: '" + configFileName + "' for " + + op.getURI() + " (" + op.getLabel() + ")"); + + } + } + + private static boolean isLogging(VitroRequest vreq) { + if (!log.isInfoEnabled()) { + return false; + } + DeveloperSettings settings = DeveloperSettings.getBean(vreq); + return settings.getBoolean(Keys.ENABLED) + && settings.getBoolean(Keys.PAGE_CONTENTS_LOG_CUSTOM_LIST_VIEW); + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/customlistview/PropertyListConfig.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/customlistview/PropertyListConfig.java index 651114aff..ad645f393 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/customlistview/PropertyListConfig.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/customlistview/PropertyListConfig.java @@ -61,6 +61,8 @@ public class PropertyListConfig { String configFileName = wadf.getObjectPropertyDao().getCustomListViewConfigFileName(op); if (configFileName == null) { // no custom config; use default config configFileName = DEFAULT_CONFIG_FILE_NAME; + } else { + CustomListViewLogger.log(vreq, op, configFileName); } log.debug("Using list view config file " + configFileName + " for object property " + op.getURI()); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/edit/EditConfigurationTemplateModel.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/edit/EditConfigurationTemplateModel.java index cac4c484e..8a3e273ca 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/edit/EditConfigurationTemplateModel.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/edit/EditConfigurationTemplateModel.java @@ -491,6 +491,10 @@ public class EditConfigurationTemplateModel extends BaseTemplateModel { return getDataLiteralValuesFromParameter(); } + //Get a custom object uri for deletion if one exists, i.e. not the object uri for the property + public String getCustomDeleteObjectUri() { + return (String) vreq.getParameter("deleteObjectUri"); + } //Used for deletion in case there's a specific template to be employed public String getDeleteTemplate() { String templateName = vreq.getParameter("templateName"); @@ -544,7 +548,7 @@ public class EditConfigurationTemplateModel extends BaseTemplateModel { } for(VClass rangeVClass : rangeVClasses) { vclasses.add(rangeVClass); - List subURIs = wdf.getVClassDao().getSubClassURIs(rangeVClass.getURI()); + List subURIs = wdf.getVClassDao().getAllSubClassURIs(rangeVClass.getURI()); for (String subClassURI : subURIs) { VClass subClass = wdf.getVClassDao().getVClassByURI(subClassURI); if (subClass != null) { diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/individual/PropertyGroupTemplateModel.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/individual/PropertyGroupTemplateModel.java index b50469188..91d463f54 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/individual/PropertyGroupTemplateModel.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/individual/PropertyGroupTemplateModel.java @@ -9,6 +9,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper; +import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.display.DisplayDataProperty; import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.display.DisplayObjectProperty; import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.ifaces.RequestedAction; import edu.cornell.mannlib.vitro.webapp.beans.DataProperty; @@ -50,8 +51,15 @@ public class PropertyGroupTemplateModel extends BaseTemplateModel { properties.add(tm); } + } else if (p instanceof DataProperty){ + DataProperty dp = (DataProperty) p; + RequestedAction dop = new DisplayDataProperty(dp); + if (!PolicyHelper.isAuthorizedForActions(vreq, dop)) { + continue; + } + properties.add(new DataPropertyTemplateModel(dp, subject, vreq, editing, populatedDataPropertyList)); } else { - properties.add(new DataPropertyTemplateModel((DataProperty)p, subject, vreq, editing, populatedDataPropertyList)); + log.debug(p.getURI() + " is neither an ObjectProperty nor a DataProperty; skipping display"); } } } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/individual/PropertyTemplateModel.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/individual/PropertyTemplateModel.java index f7d5c603b..2fe95a71c 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/individual/PropertyTemplateModel.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/individual/PropertyTemplateModel.java @@ -11,11 +11,14 @@ 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.beans.BaseResourceBean.RoleLevel; +import edu.cornell.mannlib.vitro.webapp.beans.DataProperty; import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.beans.ObjectProperty; import edu.cornell.mannlib.vitro.webapp.beans.Property; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.Route; +import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyDao; import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; import edu.cornell.mannlib.vitro.webapp.web.templatemodels.BaseTemplateModel; @@ -94,6 +97,27 @@ public abstract class PropertyTemplateModel extends BaseTemplateModel { String editUrl = UrlBuilder.getUrl(getPropertyEditRoute(), "uri", property.getURI()); verboseDisplay.put("propertyEditUrl", editUrl); + + if(isFauxProperty(property)) { + verboseDisplay.put("fauxProperty", "true"); + } + } + + private boolean isFauxProperty(Property prop) { + if(!(prop instanceof ObjectProperty)) { + return false; + } + ObjectPropertyDao opDao = vreq.getWebappDaoFactory().getObjectPropertyDao(); + ObjectProperty baseProp = opDao.getObjectPropertyByURI(prop.getURI()); + if(baseProp == null) { + return false; + } + ObjectProperty possibleFaux = (ObjectProperty) prop; + if (possibleFaux.getDomainPublic() == null) { + return (baseProp.getDomainPublic() != null); + } else { + return !possibleFaux.getDomainPublic().equals(baseProp.getDomainPublic()); + } } protected abstract int getPropertyDisplayTier(Property p); 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/dao/jena/QueryUtilsTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/QueryUtilsTest.java new file mode 100644 index 000000000..837c5cf50 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/QueryUtilsTest.java @@ -0,0 +1,65 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.dao.jena; + +import static org.junit.Assert.*; + +import org.junit.Ignore; +import org.junit.Test; + +import com.hp.hpl.jena.query.QuerySolutionMap; +import com.hp.hpl.jena.rdf.model.Literal; +import com.hp.hpl.jena.rdf.model.ModelFactory; +import com.hp.hpl.jena.rdf.model.ResourceFactory; + +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; + +/** + * TODO + */ +public class QueryUtilsTest extends AbstractTestClass { + private QuerySolutionMap bindings = new QuerySolutionMap(); + + @Test + public void bindResource() { + bindings.add("uri", ResourceFactory.createResource("http://my.uri")); + assertBoundQueryEquals("a resource ?uri", "a resource "); + } + + @Test + public void bindPlainLiteral() { + bindings.add("plain", ResourceFactory.createPlainLiteral("too easy")); + assertBoundQueryEquals("This is ?plain ?plain", + "This is \"too easy\" \"too easy\""); + } + + @Test + public void bindTypedLiteral() { + bindings.add("typed", ResourceFactory.createTypedLiteral(100L)); + assertBoundQueryEquals("take this ?typed number", + "take this \"100\"^^ number"); + } + + @Test + public void bindLanguageLiteral() { + Literal l = ModelFactory.createDefaultModel().createLiteral("Spanish", + "es-ES"); + bindings.add("lang", l); + assertBoundQueryEquals("speak my ?lang?", "speak my \"Spanish\"@es-ES?"); + } + + @Ignore + @Test + public void bindAnon() { + fail("bindAnon not implemented"); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + private void assertBoundQueryEquals(String template, String expected) { + String actual = QueryUtils.bindVariables(template, bindings); + assertEquals("bounding results", expected, actual); + } +} 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..cd375cb8b --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/freemarker/loader/FreemarkerTemplateLoaderTest.java @@ -0,0 +1,360 @@ +/* $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 expectedBestFitString) { + Path expectedBestFit = (expectedBestFitString == null) ? null : Paths + .get(expectedBestFitString); + + SortedSet matches = runTheVisitor(searchTerm); + int actualHowMany = matches.size(); + Path actualBestFit = matches.isEmpty() ? null : matches.last().path; + + if (expectedHowMany != actualHowMany) { + fail("How many results: expected " + expectedHowMany + + ", but was " + actualHowMany + ": " + matches); + } + assertEquals("Best result", expectedBestFit, actualBestFit); + } + + /** + * Try for exact match, then pare down if needed, just like Freemarker + * would. + */ + private void assertFM(String searchTerm, int expectedNumberOfTries, + String expectedBestString) { + Path expectedBestFit = expectedBestString == null ? null : Paths + .get(expectedBestString); + PathPieces stPp = new PathPieces(searchTerm); + + int actualNumberOfTries = 0; + Path 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; + } + } + if (actualBestFit == null && StringUtils.isNotBlank(stPp.language)) { + actualNumberOfTries++; + SortedSet matches = runTheVisitor(stPp.base + + stPp.language + stPp.extension); + if (!matches.isEmpty()) { + actualBestFit = matches.last().path; + } + } + if (actualBestFit == null) { + actualNumberOfTries++; + SortedSet matches = runTheVisitor(stPp.base + + stPp.extension); + if (!matches.isEmpty()) { + actualBestFit = matches.last().path; + } + } + + 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/edu/cornell/mannlib/vitro/webapp/i18n/I18nTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/i18n/I18nTest.java new file mode 100644 index 000000000..db051f961 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/i18n/I18nTest.java @@ -0,0 +1,93 @@ +package edu.cornell.mannlib.vitro.webapp.i18n; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.junit.Before; +import org.junit.Test; + +import stubs.javax.servlet.ServletContextStub; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.i18n.selection.SelectedLocale; + +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +/** + * Test the I18N functionality. + * + * Start by checking the logic that finds approximate matches for + * language-specific property files. + */ +public class I18nTest extends AbstractTestClass { + private static final List SELECTABLE_LOCALES = locales("es_MX", + "en_US"); + + ServletContextStub ctx; + + @Before + public void setup() { + ctx = new ServletContextStub(); + } + + @Test + public void noMatchOnLanguageRegion() { + assertLocales("fr_CA", SELECTABLE_LOCALES, "fr_CA", "fr", ""); + } + + @Test + public void noMatchOnLanguage() { + assertLocales("fr", SELECTABLE_LOCALES, "fr", ""); + } + + @Test + public void noMatchOnRoot() { + assertLocales("", SELECTABLE_LOCALES, ""); + } + + @Test + public void matchOnLanguageRegion() { + assertLocales("es_ES", SELECTABLE_LOCALES, "es_ES", "es", "es_MX", ""); + } + + @Test + public void matchOnLanguage() { + assertLocales("es", SELECTABLE_LOCALES, "es", "es_MX", ""); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + private void assertLocales(String requested, List selectable, + String... expected) { + SelectedLocale.setSelectableLocales(ctx, selectable); + List expectedLocales = locales(expected); + + I18n.ThemeBasedControl control = new I18n.ThemeBasedControl(ctx, + "bogusThemeDirectory"); + List actualLocales = control.getCandidateLocales( + "bogusBaseName", locale(requested)); + + assertEquals("Expected locales", expectedLocales, actualLocales); + } + + private static List locales(String... strings) { + List locales = new ArrayList<>(); + for (String s : strings) { + locales.add(locale(s)); + } + return locales; + } + + private static Locale locale(String s) { + String[] parts = s.split("_"); + String language = (parts.length > 0) ? parts[0] : ""; + String country = (parts.length > 1) ? parts[1] : ""; + String variant = (parts.length > 2) ? parts[2] : ""; + return new Locale(language, country, variant); + } + +} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/search/indexing/AdditionalURIsForObjectPropertiesTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/search/indexing/AdditionalURIsForObjectPropertiesTest.java index eeacf0e6f..5b91a3816 100644 --- a/webapp/test/edu/cornell/mannlib/vitro/webapp/search/indexing/AdditionalURIsForObjectPropertiesTest.java +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/search/indexing/AdditionalURIsForObjectPropertiesTest.java @@ -15,9 +15,13 @@ import com.hp.hpl.jena.rdf.model.ResourceFactory; import com.hp.hpl.jena.vocabulary.OWL; import com.hp.hpl.jena.vocabulary.RDFS; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.jena.model.RDFServiceModel; + public class AdditionalURIsForObjectPropertiesTest { Model model; + RDFService rdfService; String testNS = "http://example.com/test#"; String n3 = "" + @@ -39,11 +43,12 @@ public class AdditionalURIsForObjectPropertiesTest { public void setUp() throws Exception { model = ModelFactory.createDefaultModel(); model.read(new StringReader(n3 ), null , "N3"); + rdfService = new RDFServiceModel(model); } @Test public void testChangeOfRdfsLabel() { - AdditionalURIsForObjectProperties aufop = new AdditionalURIsForObjectProperties(model); + AdditionalURIsForObjectProperties aufop = new AdditionalURIsForObjectProperties(rdfService); List uris = aufop.findAdditionalURIsToIndex( ResourceFactory.createStatement( ResourceFactory.createResource(testNS + "bob"), @@ -66,7 +71,7 @@ public class AdditionalURIsForObjectPropertiesTest { @Test public void testChangeOfObjPropStmt() { - AdditionalURIsForObjectProperties aufop = new AdditionalURIsForObjectProperties(model); + AdditionalURIsForObjectProperties aufop = new AdditionalURIsForObjectProperties(rdfService); List uris = aufop.findAdditionalURIsToIndex( ResourceFactory.createStatement( ResourceFactory.createResource(testNS + "bob"), @@ -88,7 +93,7 @@ public class AdditionalURIsForObjectPropertiesTest { @Test public void testOfDataPropChange() { - AdditionalURIsForObjectProperties aufop = new AdditionalURIsForObjectProperties(model); + AdditionalURIsForObjectProperties aufop = new AdditionalURIsForObjectProperties(rdfService); List uris = aufop.findAdditionalURIsToIndex( ResourceFactory.createStatement( ResourceFactory.createResource(testNS + "bob"), @@ -107,8 +112,9 @@ public class AdditionalURIsForObjectPropertiesTest { Model model = ModelFactory.createDefaultModel(); model.read(new StringReader( n3ForNIHVIVO_2902 ), null , "N3"); - - AdditionalURIsForObjectProperties aufop = new AdditionalURIsForObjectProperties(model); + RDFService rdfService = new RDFServiceModel(model); + + AdditionalURIsForObjectProperties aufop = new AdditionalURIsForObjectProperties(rdfService); List uris = aufop.findAdditionalURIsToIndex( ResourceFactory.createStatement( ResourceFactory.createResource("http://caruso-laptop.mannlib.cornell.edu:8090/vivo/individual/n2241"), 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"> +