From d579f0e1c25b470dcad335363da746d8ba51cd9d Mon Sep 17 00:00:00 2001 From: tworrall Date: Mon, 11 Nov 2013 15:28:35 -0500 Subject: [PATCH 1/4] editLink macro was referencing displayNames rather than rangeURIs --- .../templates/freemarker/lib/lib-properties.ftl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/webapp/web/templates/freemarker/lib/lib-properties.ftl b/webapp/web/templates/freemarker/lib/lib-properties.ftl index d5b5ec9c2..efb98f2a8 100644 --- a/webapp/web/templates/freemarker/lib/lib-properties.ftl +++ b/webapp/web/templates/freemarker/lib/lib-properties.ftl @@ -171,22 +171,22 @@ name will be used as the label. --> <#macro editingLinks propertyLocalName propertyName statement editable rangeUri=""> <#if editable > <#if (!rangeUri?contains("Authorship") && !rangeUri?contains("URL") && !rangeUri?contains("Editorship") && propertyLocalName != "hasResearchArea")> - <@editLink propertyLocalName propertyName statement /> + <@editLink propertyLocalName propertyName statement rangeUri/> <@deleteLink propertyLocalName propertyName statement /> -<#macro editLink propertyLocalName propertyName statement> +<#macro editLink propertyLocalName propertyName statement rangeUri=""> <#if propertyLocalName?contains("ARG_2000028")> - <#if propertyName?contains("mailing address")> + <#if rangeUri?contains("Address")> <#local url = statement.editUrl + "&addressUri=" + "${statement.address!}"> - <#elseif propertyName?contains("phone") || propertyName?contains("fax")> + <#elseif rangeUri?contains("Telephone") || rangeUri?contains("Fax")> <#local url = statement.editUrl + "&phoneUri=" + "${statement.phone!}"> - <#elseif propertyName?contains("primary email") || propertyName?contains("additional emails")> + <#elseif rangeUri?contains("Work") || rangeUri?contains("Email")> <#local url = statement.editUrl + "&emailUri=" + "${statement.email!}"> - <#elseif propertyName?contains("full name")> + <#elseif rangeUri?contains("Name")> <#local url = statement.editUrl + "&fullNameUri=" + "${statement.fullName!}"> - <#elseif propertyName?contains("preferred title")> + <#elseif rangeUri?contains("Title")> <#local url = statement.editUrl + "&titleUri=" + "${statement.title!}"> <#else> From 4d33dd42874551ec682818602de11ad5a71145f1 Mon Sep 17 00:00:00 2001 From: j2blake Date: Sun, 17 Nov 2013 11:50:06 -0500 Subject: [PATCH 2/4] VIVO-541 First cut at the developer panel. --- doc/install.html | 59 ---- webapp/build.xml | 7 +- webapp/config/example.developer.properties | 107 ++++++++ webapp/config/example.runtime.properties | 32 --- .../rdf/auth/everytime/permission_config.n3 | 1 + .../auth/permissions/SimplePermission.java | 2 + .../config/FreemarkerConfiguration.java | 38 ++- .../config/FreemarkerConfigurationImpl.java | 1 + .../mannlib/vitro/webapp/i18n/I18n.java | 17 +- .../mannlib/vitro/webapp/i18n/I18nBundle.java | 29 +- .../mannlib/vitro/webapp/i18n/I18nLogger.java | 47 ++++ .../impl/logging/RDFServiceLogger.java | 24 +- .../utils/developer/DeveloperSettings.java | 251 ++++++++++++++++++ .../developer/DeveloperSettingsServlet.java | 93 +++++++ .../mannlib/vitro/webapp/i18n/I18nStub.java | 2 +- webapp/themes/vitro/templates/menu.ftl | 2 + webapp/web/WEB-INF/web.xml | 9 + webapp/web/js/developer/developerPanel.js | 82 ++++++ .../freemarker/page/partials/developer.ftl | 5 + .../page/partials/developerPanel.ftl | 91 +++++++ .../freemarker/page/partials/menu.ftl | 2 + 21 files changed, 762 insertions(+), 139 deletions(-) create mode 100644 webapp/config/example.developer.properties create mode 100644 webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nLogger.java create mode 100644 webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettings.java create mode 100644 webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettingsServlet.java create mode 100644 webapp/web/js/developer/developerPanel.js create mode 100644 webapp/web/templates/freemarker/page/partials/developer.ftl create mode 100644 webapp/web/templates/freemarker/page/partials/developerPanel.ftl diff --git a/doc/install.html b/doc/install.html index cd048e367..dd40c7a09 100644 --- a/doc/install.html +++ b/doc/install.html @@ -780,65 +780,6 @@ - - - For developers only. - Defeat the Freemarker template cache, so each template - is read from disk on each request. This permits developers to immediately - see the effect of changes to the template. The default is false, which - means that a cached copy of each template will be used for 60 seconds - before the disk is checked for a new version. -
Setting this option to "true" slows down Vitro performance. - - - - - developer.defeatFreemarkerCache - - - false - - - - - - For developers only. - Defeat the cache of language-specific text strings, - so the language file is read from disk on each request. - This permits developers to immediately - see the effect of changes to the text strings. - The default is false, which means that the language file is - read when VIVO starts up, or when a new theme is selected. -
Setting this option to "true" slows down Vitro performance. - - - - - developer.defeatI18nCache = true - - - false - - - - - - For developers only. - Add starting and ending delimiters to each Freemarker template, so you can see - which template were invoked by viewing the generated HTML. - The default is false. -
Setting this option to "true" slows down Vitro performance. - - - - - developer.insertFreemarkerDelimiters = true - - - false - - - diff --git a/webapp/build.xml b/webapp/build.xml index 00f61f423..93927a7f7 100644 --- a/webapp/build.xml +++ b/webapp/build.xml @@ -263,7 +263,12 @@ - + + + + + + diff --git a/webapp/config/example.developer.properties b/webapp/config/example.developer.properties new file mode 100644 index 000000000..482cecd21 --- /dev/null +++ b/webapp/config/example.developer.properties @@ -0,0 +1,107 @@ +# +# ----------------------------------------------------------------------------- +# Runtime properties for developer mode. +# +# If the developer.properties file is present in your VIVO home directory, it +# will be loaded as VIVO starts up, taking effect immediately. +# +# Each of these properties can be set or changed while VIVO is running, but it +# can be convenient to set them in advance. +# +# WARNING: Some of these options can seriously degrade performance. They should +# not be enabled in a production instance of VIVO. +# +# ----------------------------------------------------------------------------- +# + +#------------------------------------------------------------------------------ +# General options +#------------------------------------------------------------------------------ + +# +# The "master switch" for developer mode. If this is not set to true, then none +# of the other properties have any effect. +# +# developer.enabled = true + +# +# If developer mode is enabled, this will determine who can modify the +# developer settings. If 'true', then any user can modify the settings. If +# false, then only a site administrator (or root) can modify the settings. +# The default is 'false'. +# +# developer.permitAnonymousControl + +#------------------------------------------------------------------------------ +# Freemarker +#------------------------------------------------------------------------------ + +# +# Add HTML comments to each Freemarker template, so you can see what each +# templates to the page, by viewing the source of the page in the browser. +# The default is 'false'. +# +# developer.insertFreemarkerDelimiters = true + +# +# Defeat the Freemarker template cache, so each template is read from disk +# on each request. This permits developers to immediately see the effect of +# changes to the template. The default is 'false', which means that a cached +# copy of each template will be used for 60 seconds before the disk is checked +# for a new version. +# +# developer.defeatFreemarkerCache = true + +#------------------------------------------------------------------------------ +# Internationalization +#------------------------------------------------------------------------------ + +# +# Defeat the cache of language-specific text strings, so the language file +# is read from disk on each request. This permits developers to immediately +# see the effect of changes to the text strings. The default is 'false', which +# means that the language file is only read when VIVO starts up, or when a new +# theme is selected. +# +# developer.i18n.defeatCache = true + +# +# Write a line to the log every time a template or a controller requests a +# language-specific string from the properties files. +# +# developer.i18n.logStringRequests + + +#------------------------------------------------------------------------------ +# Logging SPARQL queries +#------------------------------------------------------------------------------ + +# +# Turn on logging of all SPARQL queries. The logging is at the INFO level. +# Each entry includes: +# - the elapsed time spent on the query, in seconds, +# - the name of the method on RDFService that received the query, +# - the format of the result stream from the RDFService method, +# - the text of the query. +# Note that all access to the content models is done through SPARQL queries, +# but some go through translation layers before reaching the RDFService for +# logging and execution. The default is 'false'. +# +# developer.loggingRDFService.enable = true + +# +# If SPARQL query logging is enabled, this will add a stack trace to each log +# entry. The stack trace is abridged, so it starts after the +# ApplicationFilterChain, omits any Jena classes, and ends at the RDFService. +# The default is 'false'. +# +# developer.loggingRDFService.stackTrace = true + +# +# If SPARQL query logging is enabled, a regular expression can be used to +# restrict the number of entries that are produced. The expression is +# tested against each line in the (unabridged) stack trace. If the +# expression doesn't match any line in the stack trace, then no log entry +# is made. The default is 'false'. +# +# developer.loggingRDFService.restriction = true diff --git a/webapp/config/example.runtime.properties b/webapp/config/example.runtime.properties index d72d632d7..d680c1621 100644 --- a/webapp/config/example.runtime.properties +++ b/webapp/config/example.runtime.properties @@ -152,35 +152,3 @@ RDFService.languageFilter = true # This should not be used with languages.forceLocale, which will override it. # # languages.selectableLocales = en, es, fr - -# -# For developers only: Setting this option to "true" slows down Vitro performance. -# -# Defeat the Freemarker template cache, so each template is read from disk -# on each request. This permits developers to immediately see the effect of -# changes to the template. The default is false, which means -# that a cached copy of each template will be used for 60 seconds before -# the disk is checked for a new version. -# -# developer.defeatFreemarkerCache = true - -# -# For developers only: Setting this option to "true" slows down Vitro performance. -# -# Defeat the cache of language-specific text strings, so the language file -# is read from disk on each request. This permits developers to immediately -# see the effect of changes to the text strings. The default is -# false, which means that the language file is read when -# VIVO starts up, or when a new theme is selected. -# -# developer.defeatI18nCache = true - -# -# For developers only: Setting this option to "true" slows down Vitro performance. -# -# Add starting and ending delimiters to each Freemarker template, so you can see -# which template were invoked by viewing the generated HTML. The default is -# false. -# -# developer.insertFreemarkerDelimiters = true - diff --git a/webapp/rdf/auth/everytime/permission_config.n3 b/webapp/rdf/auth/everytime/permission_config.n3 index 9b61615f2..c4a88018a 100644 --- a/webapp/rdf/auth/everytime/permission_config.n3 +++ b/webapp/rdf/auth/everytime/permission_config.n3 @@ -24,6 +24,7 @@ auth:ADMIN auth:hasPermission simplePermission:UseMiscellaneousAdminPages ; auth:hasPermission simplePermission:UseSparqlQueryPage ; auth:hasPermission simplePermission:PageViewableAdmin ; + auth:hasPermission simplePermission:EnableDeveloperPanel ; # permissions for CURATOR and above. auth:hasPermission simplePermission:EditOntology ; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java index cf0a7e272..33c609c5b 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java @@ -76,6 +76,8 @@ public class SimplePermission extends Permission { NAMESPACE + "UseAdvancedDataToolsPages"); public static final SimplePermission USE_SPARQL_QUERY_PAGE = new SimplePermission( NAMESPACE + "UseSparqlQueryPage"); + public static final SimplePermission ENABLE_DEVELOPER_PANEL = new SimplePermission( + NAMESPACE + "EnableDeveloperPanel"); // ---------------------------------------------------------------------- diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfiguration.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfiguration.java index 17ad1fd0b..7872974bb 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 @@ -18,7 +18,6 @@ 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; @@ -27,6 +26,8 @@ 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.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; @@ -55,18 +56,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 +92,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 +111,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,10 +133,20 @@ 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(); @@ -167,7 +176,8 @@ public abstract class FreemarkerConfiguration { MultiTemplateLoader mtl = new MultiTemplateLoader(loaderArray); // If requested, add delimiters to the templates. - if (Boolean.valueOf(props.getProperty(PROPERTY_INSERT_DELIMITERS))) { + DeveloperSettings settings = DeveloperSettings.getBean(req); + if (settings.getBoolean(Keys.INSERT_FREEMARKER_DELIMITERS)) { return new DelimitingTemplateLoader(mtl); } else { return mtl; 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/i18n/I18n.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java index 38a1bc059..05758f26b 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java @@ -15,8 +15,9 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings.Keys; /** * Provides access to a bundle of text strings, based on the name of the bundle, @@ -31,7 +32,6 @@ public class I18n { private static final Log log = LogFactory.getLog(I18n.class); public static final String DEFAULT_BUNDLE_NAME = "all"; - private static final String PROPERTY_DEVELOPER_DEFEAT_CACHE = "developer.defeatI18nCache"; /** * If this attribute is present on the request, then the cache has already @@ -103,6 +103,7 @@ public class I18n { protected I18nBundle getBundle(String bundleName, HttpServletRequest req) { log.debug("Getting bundle '" + bundleName + "'"); + I18nLogger i18nLogger = new I18nLogger(req); try { checkDevelopmentMode(req); checkForChangeInThemeDirectory(req); @@ -113,13 +114,13 @@ public class I18n { ResourceBundle.Control control = new ThemeBasedControl(ctx, dir); ResourceBundle rb = ResourceBundle.getBundle(bundleName, req.getLocale(), control); - return new I18nBundle(bundleName, rb); + return new I18nBundle(bundleName, rb, i18nLogger); } catch (MissingResourceException e) { log.warn("Didn't find text bundle '" + bundleName + "'"); - return I18nBundle.emptyBundle(bundleName); + return I18nBundle.emptyBundle(bundleName, i18nLogger); } catch (Exception e) { log.error("Failed to create text bundle '" + bundleName + "'", e); - return I18nBundle.emptyBundle(bundleName); + return I18nBundle.emptyBundle(bundleName, i18nLogger); } } @@ -127,11 +128,7 @@ public class I18n { * If we are in development mode, clear the cache on each request. */ private void checkDevelopmentMode(HttpServletRequest req) { - ConfigurationProperties bean = ConfigurationProperties.getBean(req); - - String flag = bean - .getProperty(PROPERTY_DEVELOPER_DEFEAT_CACHE, "false"); - if (Boolean.valueOf(flag.trim())) { + if (DeveloperSettings.getBean(req).getBoolean(Keys.I18N_DEFEAT_CACHE) ) { log.debug("In development mode - clearing the cache."); clearCacheOnRequest(req); } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java index 44b930089..02793529c 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java @@ -24,24 +24,28 @@ public class I18nBundle { private static final String MESSAGE_BUNDLE_NOT_FOUND = "Text bundle ''{0}'' not found."; private static final String MESSAGE_KEY_NOT_FOUND = "Text bundle ''{0}'' has no text for ''{1}''"; - public static I18nBundle emptyBundle(String bundleName) { - return new I18nBundle(bundleName); + public static I18nBundle emptyBundle(String bundleName, + I18nLogger i18nLogger) { + return new I18nBundle(bundleName, i18nLogger); } private final String bundleName; private final ResourceBundle resources; private final String notFoundMessage; + private final I18nLogger i18nLogger; - private I18nBundle(String bundleName) { - this(bundleName, new EmptyResourceBundle(), MESSAGE_BUNDLE_NOT_FOUND); + private I18nBundle(String bundleName, I18nLogger i18nLogger) { + this(bundleName, new EmptyResourceBundle(), MESSAGE_BUNDLE_NOT_FOUND, + i18nLogger); } - public I18nBundle(String bundleName, ResourceBundle resources) { - this(bundleName, resources, MESSAGE_KEY_NOT_FOUND); + public I18nBundle(String bundleName, ResourceBundle resources, + I18nLogger i18nLogger) { + this(bundleName, resources, MESSAGE_KEY_NOT_FOUND, i18nLogger); } private I18nBundle(String bundleName, ResourceBundle resources, - String notFoundMessage) { + String notFoundMessage, I18nLogger i18nLogger) { if (bundleName == null) { throw new IllegalArgumentException("bundleName may not be null"); } @@ -57,22 +61,27 @@ public class I18nBundle { this.bundleName = bundleName; this.resources = resources; this.notFoundMessage = notFoundMessage; + this.i18nLogger = i18nLogger; } public String text(String key, Object... parameters) { - String textString; if (resources.containsKey(key)) { textString = resources.getString(key); log.debug("In '" + bundleName + "', " + key + "='" + textString + "')"); - return formatString(textString, parameters); } else { String message = MessageFormat.format(notFoundMessage, bundleName, key); log.warn(message); - return "ERROR: " + message; + textString = "ERROR: " + message; } + String result = formatString(textString, parameters); + + if (i18nLogger != null) { + i18nLogger.log(bundleName, key, parameters, textString, result); + } + return result; } private static String formatString(String textString, Object... parameters) { diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nLogger.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nLogger.java new file mode 100644 index 000000000..8889195b6 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nLogger.java @@ -0,0 +1,47 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.i18n; + +import java.util.Arrays; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings.Keys; + +/** + * If enabled in developer mode, write a message to the log each time someone + * asks for a language string. + * + * The I18nBundle has a life span of one HTTP request, and so does this. + */ +public class I18nLogger { + private static final Log log = LogFactory.getLog(I18nLogger.class); + + private final boolean isLogging; + + public I18nLogger(HttpServletRequest req) { + DeveloperSettings settings = DeveloperSettings.getBean(req); + this.isLogging = settings.getBoolean(Keys.ENABLED) + && settings.getBoolean(Keys.I18N_LOG_STRINGS) + && log.isInfoEnabled(); + } + + public void log(String bundleName, String key, Object[] parameters, + String rawText, String formattedText) { + if (isLogging) { + String message = String.format( + "Retrieved from %s.%s with %s: '%s'", bundleName, key, + Arrays.toString(parameters), rawText); + + if (!rawText.equals(formattedText)) { + message += String.format(" --> '%s'", formattedText); + } + + log.info(message); + } + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/logging/RDFServiceLogger.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/logging/RDFServiceLogger.java index aecdffdec..8428f7aee 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/logging/RDFServiceLogger.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/logging/RDFServiceLogger.java @@ -15,7 +15,8 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings.Keys; /** * Writes the log message for the LoggingRDFService. @@ -41,10 +42,6 @@ import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; public class RDFServiceLogger implements AutoCloseable { private static final Log log = LogFactory.getLog(RDFServiceLogger.class); - private static final String PROPERTY_ENABLED = "developer.loggingRDFService.enable"; - private static final String PROPERTY_STACK_TRACE = "developer.loggingRDFService.stackTrace"; - private static final String PROPERTY_RESTRICTION = "developer.loggingRDFService.restriction"; - private final ServletContext ctx; private final Object[] args; @@ -72,18 +69,21 @@ public class RDFServiceLogger implements AutoCloseable { } private void getProperties() { - ConfigurationProperties props = ConfigurationProperties.getBean(ctx); - isEnabled = Boolean.valueOf(props.getProperty(PROPERTY_ENABLED)); - traceRequested = Boolean.valueOf(props - .getProperty(PROPERTY_STACK_TRACE)); + DeveloperSettings settings = DeveloperSettings.getBean(ctx); + isEnabled = settings.getBoolean(Keys.LOGGING_RDF_ENABLE); + traceRequested = settings.getBoolean(Keys.LOGGING_RDF_STACK_TRACE); - String restrictionString = props.getProperty(PROPERTY_RESTRICTION); - if (StringUtils.isNotBlank(restrictionString)) { + String restrictionString = settings + .getString(Keys.LOGGING_RDF_RESTRICTION); + if (StringUtils.isBlank(restrictionString)) { + restriction = null; + } else { try { restriction = Pattern.compile(restrictionString); } catch (Exception e) { log.error("Failed to compile the pattern for " - + PROPERTY_RESTRICTION + " = " + restriction + " " + e); + + Keys.LOGGING_RDF_RESTRICTION.key() + " = " + + restriction + " " + e); isEnabled = false; } } 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..5ab41ef6a --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettings.java @@ -0,0 +1,251 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.developer; + +import java.io.File; +import java.io.FileReader; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; + +/** + * Hold the global developer settings. Render to JSON when requested. + * + * On first request, the "developer.properties" file is loaded from the Vitro + * home directory. If the file doesn't exist, or doesn't contain values for + * certain properties, those propertiew will keep their default values. + * + * An AJAX request can be used to update the properties. If the request has + * multiple values for a property, the first value will be used. If the request + * does not contain a value for a property, that property will keep its current + * value. + */ +public class DeveloperSettings { + private static final Log log = LogFactory.getLog(DeveloperSettings.class); + + public enum Keys { + /** + * Developer mode and developer panel is enabled. + */ + ENABLED("developer.enabled", true), + + /** + * Users don't need authority to use the developer panel. But they still + * can't enable it without authority. + */ + PERMIT_ANONYMOUS_CONTROL("developer.permitAnonymousControl", true), + + /** + * Load Freemarker templates every time they are requested. + */ + DEFEAT_FREEMARKER_CACHE("developer.defeatFreemarkerCache", true), + + /** + * Show where each Freemarker template starts and stops. + */ + INSERT_FREEMARKER_DELIMITERS("developer.insertFreemarkerDelimiters", + true), + + /** + * Load language property files every time they are requested. + */ + I18N_DEFEAT_CACHE("developer.i18n.defeatCache", true), + + /** + * Enable the I18nLogger to log each string request. + */ + I18N_LOG_STRINGS("developer.i18n.logStringRequests", true), + + /** + * Enable the LoggingRDFService + */ + LOGGING_RDF_ENABLE("developer.loggingRDFService.enable", true), + + /** + * When logging with the LoggingRDFService, include a stack trace + */ + LOGGING_RDF_STACK_TRACE("developer.loggingRDFService.stackTrace", true), + + /** + * Don't log with the LoggingRDFService unless the calling stack meets + * this restriction. + */ + LOGGING_RDF_RESTRICTION("developer.loggingRDFService.restriction", + false); + + private final String key; + private final boolean bool; + + Keys(String key, boolean bool) { + this.key = key; + this.bool = bool; + } + + public String key() { + return key; + } + + boolean isBoolean() { + return bool; + } + } + + // ---------------------------------------------------------------------- + // 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(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(key, parameterMap.get(key)[0]); + } + update(fromRequest); + } + + private void update(Map changedSettings) { + for (Keys key : Keys.values()) { + String s = changedSettings.get(key.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.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.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.key(), get(key)); + } + return map; + } + + @Override + public String toString() { + return "DeveloperSettings" + settings; + } + + /* For debugging. */ + private void dumpParameterMap(Map parameterMap) { + Map> map = new HashMap<>(); + for (String key : parameterMap.keySet()) { + map.put(key, Arrays.asList(parameterMap.get(key))); + } + log.debug("Parameter map: " + map); + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettingsServlet.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettingsServlet.java new file mode 100644 index 000000000..7422137ab --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/developer/DeveloperSettingsServlet.java @@ -0,0 +1,93 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.developer; + +import static edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings.Keys.PERMIT_ANONYMOUS_CONTROL; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission; +import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.ajax.VitroAjaxController; +import edu.cornell.mannlib.vitro.webapp.services.freemarker.FreemarkerProcessingService.TemplateProcessingException; +import edu.cornell.mannlib.vitro.webapp.services.freemarker.FreemarkerProcessingServiceSetup; + +/** + * Accept an AJAX request to update the developer settings. Return an HTML + * representation of the developer panel from the settings and a Freemarker + * template. + * + * If developer mode is not enabled, the HTML response is empty. + * + * You may only control the panel if you are logged in with sufficient + * authorization, or if anonymous control is permitted by the settings. + * + * If you are not allowed to control the panel, then the HTML response + * is only a statement that developer mode is enabled. Otherwise, it + * is a full panel (collapsed at first). + */ +public class DeveloperSettingsServlet extends VitroAjaxController { + private static final Log log = LogFactory + .getLog(DeveloperSettingsServlet.class); + + @Override + protected void doRequest(VitroRequest vreq, HttpServletResponse resp) + throws ServletException, IOException { + DeveloperSettings settings = DeveloperSettings.getBean(vreq); + + /* + * Are they allowed to control the panel? + */ + if (isAuthorized(vreq)) { + // Update the settings. + settings.updateFromRequest(vreq.getParameterMap()); + } else { + log.debug("Not authorized to update settings."); + } + + /* + * Build the response. + */ + try { + Map bodyMap = buildBodyMap(isAuthorized(vreq), + settings); + String rendered = renderTemplate(vreq, bodyMap); + resp.getWriter().write(rendered); + } catch (Exception e) { + doError(resp, e.toString(), 500); + } + } + + private Map buildBodyMap(boolean authorized, + DeveloperSettings settings) { + Map settingsMap = new HashMap<>(); + settingsMap.putAll(settings.getSettingsMap()); + settingsMap.put("mayControl", authorized); + Map bodyMap = new HashMap<>(); + bodyMap.put("settings", settingsMap); + return bodyMap; + } + + private String renderTemplate(VitroRequest vreq, Map bodyMap) + throws TemplateProcessingException { + return FreemarkerProcessingServiceSetup.getService(getServletContext()) + .renderTemplate("developerPanel.ftl", bodyMap, vreq); + } + + private boolean isAuthorized(VitroRequest vreq) { + boolean authBySetting = DeveloperSettings.getBean(vreq).getBoolean( + PERMIT_ANONYMOUS_CONTROL); + boolean authByPolicy = PolicyHelper.isAuthorizedForActions(vreq, + SimplePermission.ENABLE_DEVELOPER_PANEL.ACTION); + return authBySetting || authByPolicy; + } +} diff --git a/webapp/test/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"> +