VIVO-541 First cut at the developer panel.

This commit is contained in:
j2blake 2013-11-17 11:50:06 -05:00
parent 35251e89f4
commit 0fce9f6a7b
21 changed files with 762 additions and 139 deletions

View file

@ -780,65 +780,6 @@
</td> </td>
</tr> </tr>
<tr>
<td colspan="2">
<b>For developers only.</b>
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 <code>false</code>, which
means that a cached copy of each template will be used for 60 seconds
before the disk is checked for a new version.
<br/><b>Setting this option to "true" slows down Vitro performance.</b>
</td>
</tr>
<tr class="odd_row">
<td>
developer.defeatFreemarkerCache
</td>
<td>
false
</td>
</tr>
<tr>
<td colspan="2">
<b>For developers only.</b>
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 <code>false</code>, which means that the language file is
read when VIVO starts up, or when a new theme is selected.
<br/><b>Setting this option to "true" slows down Vitro performance.</b>
</td>
</tr>
<tr class="odd_row">
<td>
developer.defeatI18nCache = true
</td>
<td>
false
</td>
</tr>
<tr>
<td colspan="2">
<b>For developers only.</b>
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 <code>false</code>.
<br/><b>Setting this option to "true" slows down Vitro performance.</b>
</td>
</tr>
<tr class="odd_row blue">
<td>
developer.insertFreemarkerDelimiters = true
</td>
<td>
false
</td>
</tr>
</tbody> </tbody>
</table> </table>

View file

@ -263,7 +263,12 @@
<target name="prepareVitroHomeDir" depends="prepare"> <target name="prepareVitroHomeDir" depends="prepare">
<mkdir dir="${vitrohome.build.dir}" /> <mkdir dir="${vitrohome.build.dir}" />
<mkdir dir="${vitrohome.image.dir}" /> <mkdir dir="${vitrohome.image.dir}" />
<copy todir="${vitrohome.image.dir}" file="${appbase.dir}/config/example.runtime.properties" /> <copy todir="${vitrohome.image.dir}" >
<fileset dir="${appbase.dir}/config" >
<include name="example.runtime.properties" />
<include name="example.developer.properties" />
</fileset>
</copy>
<copy todir="${vitrohome.image.dir}" > <copy todir="${vitrohome.image.dir}" >
<fileset dir="${appbase.dir}" > <fileset dir="${appbase.dir}" >
<include name="rdf/**/*" /> <include name="rdf/**/*" />

View file

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

View file

@ -152,35 +152,3 @@ RDFService.languageFilter = true
# This should not be used with languages.forceLocale, which will override it. # This should not be used with languages.forceLocale, which will override it.
# #
# languages.selectableLocales = en, es, fr # 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 <code>false</code>, 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
# <code>false</code>, 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
# <code>false</code>.
#
# developer.insertFreemarkerDelimiters = true

View file

@ -24,6 +24,7 @@ auth:ADMIN
auth:hasPermission simplePermission:UseMiscellaneousAdminPages ; auth:hasPermission simplePermission:UseMiscellaneousAdminPages ;
auth:hasPermission simplePermission:UseSparqlQueryPage ; auth:hasPermission simplePermission:UseSparqlQueryPage ;
auth:hasPermission simplePermission:PageViewableAdmin ; auth:hasPermission simplePermission:PageViewableAdmin ;
auth:hasPermission simplePermission:EnableDeveloperPanel ;
# permissions for CURATOR and above. # permissions for CURATOR and above.
auth:hasPermission simplePermission:EditOntology ; auth:hasPermission simplePermission:EditOntology ;

View file

@ -76,6 +76,8 @@ public class SimplePermission extends Permission {
NAMESPACE + "UseAdvancedDataToolsPages"); NAMESPACE + "UseAdvancedDataToolsPages");
public static final SimplePermission USE_SPARQL_QUERY_PAGE = new SimplePermission( public static final SimplePermission USE_SPARQL_QUERY_PAGE = new SimplePermission(
NAMESPACE + "UseSparqlQueryPage"); NAMESPACE + "UseSparqlQueryPage");
public static final SimplePermission ENABLE_DEVELOPER_PANEL = new SimplePermission(
NAMESPACE + "EnableDeveloperPanel");
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

View file

@ -18,7 +18,6 @@ import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; 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.config.RevisionInfoBean;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; 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.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.edit.n3editing.configuration.EditConfigurationConstants;
import edu.cornell.mannlib.vitro.webapp.i18n.freemarker.I18nMethodModel; import edu.cornell.mannlib.vitro.webapp.i18n.freemarker.I18nMethodModel;
import edu.cornell.mannlib.vitro.webapp.startup.StartupStatus; 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.IndividualShortViewDirective;
import edu.cornell.mannlib.vitro.webapp.web.directives.UrlDirective; import edu.cornell.mannlib.vitro.webapp.web.directives.UrlDirective;
import edu.cornell.mannlib.vitro.webapp.web.directives.WidgetDirective; import edu.cornell.mannlib.vitro.webapp.web.directives.WidgetDirective;
@ -55,18 +56,17 @@ import freemarker.template.TemplateModelException;
* own locale, etc. * own locale, etc.
* *
* Each time a request asks for the configuration, check to see whether the * 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 * cache is still valid, whether the theme has changed (needs a new
* TemplateLoader). Store the request info to the ThreadLocal. * TemplateLoader), and whether the DeveloperSettings have changed (might need a
* new TemplateLoader). Store the request info to the ThreadLocal.
*/ */
public abstract class FreemarkerConfiguration { public abstract class FreemarkerConfiguration {
private static final Log log = LogFactory private static final Log log = LogFactory
.getLog(FreemarkerConfiguration.class); .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 FreemarkerConfigurationImpl instance;
private static volatile String previousThemeDir; private static volatile String previousThemeDir;
private static volatile Map<String, Object> previousSettingsMap;
public static Configuration getConfig(HttpServletRequest req) { public static Configuration getConfig(HttpServletRequest req) {
confirmInstanceIsSet(); 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) { private static boolean isTemplateCacheInvalid(HttpServletRequest req) {
ConfigurationProperties props = ConfigurationProperties.getBean(req); DeveloperSettings settings = DeveloperSettings.getBean(req);
if (settings.getBoolean(Keys.DEFEAT_FREEMARKER_CACHE)) {
// If the developer doesn't want the cache, it's invalid.
if (Boolean.valueOf(props.getProperty(PROPERTY_DEFEAT_CACHE))) {
return true; return true;
} }
return false; return false;
} }
@ -113,7 +111,8 @@ public abstract class FreemarkerConfiguration {
private static void keepTemplateLoaderCurrentWithThemeDirectory( private static void keepTemplateLoaderCurrentWithThemeDirectory(
HttpServletRequest req) { HttpServletRequest req) {
String themeDir = getThemeDirectory(req); String themeDir = getThemeDirectory(req);
if (hasThemeDirectoryChanged(themeDir)) { if (hasThemeDirectoryChanged(themeDir)
|| haveDeveloperSettingsChanged(req)) {
TemplateLoader tl = createTemplateLoader(req, themeDir); TemplateLoader tl = createTemplateLoader(req, themeDir);
instance.setTemplateLoader(tl); instance.setTemplateLoader(tl);
} }
@ -134,10 +133,20 @@ public abstract class FreemarkerConfiguration {
} }
} }
private static boolean haveDeveloperSettingsChanged(HttpServletRequest req) {
Map<String, Object> settingsMap = DeveloperSettings.getBean(req)
.getSettingsMap();
if (settingsMap.equals(previousSettingsMap)) {
return false;
} else {
previousSettingsMap = settingsMap;
return true;
}
}
private static TemplateLoader createTemplateLoader(HttpServletRequest req, private static TemplateLoader createTemplateLoader(HttpServletRequest req,
String themeDir) { String themeDir) {
ServletContext ctx = req.getSession().getServletContext(); ServletContext ctx = req.getSession().getServletContext();
ConfigurationProperties props = ConfigurationProperties.getBean(ctx);
List<TemplateLoader> loaders = new ArrayList<TemplateLoader>(); List<TemplateLoader> loaders = new ArrayList<TemplateLoader>();
@ -167,7 +176,8 @@ public abstract class FreemarkerConfiguration {
MultiTemplateLoader mtl = new MultiTemplateLoader(loaderArray); MultiTemplateLoader mtl = new MultiTemplateLoader(loaderArray);
// If requested, add delimiters to the templates. // 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); return new DelimitingTemplateLoader(mtl);
} else { } else {
return mtl; return mtl;

View file

@ -287,6 +287,7 @@ public class FreemarkerConfigurationImpl extends Configuration {
urls.put("images", UrlBuilder.getUrl("/images")); urls.put("images", UrlBuilder.getUrl("/images"));
urls.put("theme", UrlBuilder.getUrl(themeDir)); urls.put("theme", UrlBuilder.getUrl(themeDir));
urls.put("index", UrlBuilder.getUrl("/browse")); urls.put("index", UrlBuilder.getUrl("/browse"));
urls.put("developerAjax", UrlBuilder.getUrl("/admin/developerAjax"));
return urls; return urls;
} }

View file

@ -15,8 +15,9 @@ import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; 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.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, * 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); private static final Log log = LogFactory.getLog(I18n.class);
public static final String DEFAULT_BUNDLE_NAME = "all"; 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 * 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) { protected I18nBundle getBundle(String bundleName, HttpServletRequest req) {
log.debug("Getting bundle '" + bundleName + "'"); log.debug("Getting bundle '" + bundleName + "'");
I18nLogger i18nLogger = new I18nLogger(req);
try { try {
checkDevelopmentMode(req); checkDevelopmentMode(req);
checkForChangeInThemeDirectory(req); checkForChangeInThemeDirectory(req);
@ -113,13 +114,13 @@ public class I18n {
ResourceBundle.Control control = new ThemeBasedControl(ctx, dir); ResourceBundle.Control control = new ThemeBasedControl(ctx, dir);
ResourceBundle rb = ResourceBundle.getBundle(bundleName, ResourceBundle rb = ResourceBundle.getBundle(bundleName,
req.getLocale(), control); req.getLocale(), control);
return new I18nBundle(bundleName, rb); return new I18nBundle(bundleName, rb, i18nLogger);
} catch (MissingResourceException e) { } catch (MissingResourceException e) {
log.warn("Didn't find text bundle '" + bundleName + "'"); log.warn("Didn't find text bundle '" + bundleName + "'");
return I18nBundle.emptyBundle(bundleName); return I18nBundle.emptyBundle(bundleName, i18nLogger);
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create text bundle '" + bundleName + "'", 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. * If we are in development mode, clear the cache on each request.
*/ */
private void checkDevelopmentMode(HttpServletRequest req) { private void checkDevelopmentMode(HttpServletRequest req) {
ConfigurationProperties bean = ConfigurationProperties.getBean(req); if (DeveloperSettings.getBean(req).getBoolean(Keys.I18N_DEFEAT_CACHE) ) {
String flag = bean
.getProperty(PROPERTY_DEVELOPER_DEFEAT_CACHE, "false");
if (Boolean.valueOf(flag.trim())) {
log.debug("In development mode - clearing the cache."); log.debug("In development mode - clearing the cache.");
clearCacheOnRequest(req); clearCacheOnRequest(req);
} }

View file

@ -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_BUNDLE_NOT_FOUND = "Text bundle ''{0}'' not found.";
private static final String MESSAGE_KEY_NOT_FOUND = "Text bundle ''{0}'' has no text for ''{1}''"; private static final String MESSAGE_KEY_NOT_FOUND = "Text bundle ''{0}'' has no text for ''{1}''";
public static I18nBundle emptyBundle(String bundleName) { public static I18nBundle emptyBundle(String bundleName,
return new I18nBundle(bundleName); I18nLogger i18nLogger) {
return new I18nBundle(bundleName, i18nLogger);
} }
private final String bundleName; private final String bundleName;
private final ResourceBundle resources; private final ResourceBundle resources;
private final String notFoundMessage; private final String notFoundMessage;
private final I18nLogger i18nLogger;
private I18nBundle(String bundleName) { private I18nBundle(String bundleName, I18nLogger i18nLogger) {
this(bundleName, new EmptyResourceBundle(), MESSAGE_BUNDLE_NOT_FOUND); this(bundleName, new EmptyResourceBundle(), MESSAGE_BUNDLE_NOT_FOUND,
i18nLogger);
} }
public I18nBundle(String bundleName, ResourceBundle resources) { public I18nBundle(String bundleName, ResourceBundle resources,
this(bundleName, resources, MESSAGE_KEY_NOT_FOUND); I18nLogger i18nLogger) {
this(bundleName, resources, MESSAGE_KEY_NOT_FOUND, i18nLogger);
} }
private I18nBundle(String bundleName, ResourceBundle resources, private I18nBundle(String bundleName, ResourceBundle resources,
String notFoundMessage) { String notFoundMessage, I18nLogger i18nLogger) {
if (bundleName == null) { if (bundleName == null) {
throw new IllegalArgumentException("bundleName may not be null"); throw new IllegalArgumentException("bundleName may not be null");
} }
@ -57,22 +61,27 @@ public class I18nBundle {
this.bundleName = bundleName; this.bundleName = bundleName;
this.resources = resources; this.resources = resources;
this.notFoundMessage = notFoundMessage; this.notFoundMessage = notFoundMessage;
this.i18nLogger = i18nLogger;
} }
public String text(String key, Object... parameters) { public String text(String key, Object... parameters) {
String textString; String textString;
if (resources.containsKey(key)) { if (resources.containsKey(key)) {
textString = resources.getString(key); textString = resources.getString(key);
log.debug("In '" + bundleName + "', " + key + "='" + textString log.debug("In '" + bundleName + "', " + key + "='" + textString
+ "')"); + "')");
return formatString(textString, parameters);
} else { } else {
String message = MessageFormat.format(notFoundMessage, bundleName, String message = MessageFormat.format(notFoundMessage, bundleName,
key); key);
log.warn(message); 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) { private static String formatString(String textString, Object... parameters) {

View file

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

View file

@ -15,7 +15,8 @@ import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; 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. * Writes the log message for the LoggingRDFService.
@ -41,10 +42,6 @@ import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties;
public class RDFServiceLogger implements AutoCloseable { public class RDFServiceLogger implements AutoCloseable {
private static final Log log = LogFactory.getLog(RDFServiceLogger.class); 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 ServletContext ctx;
private final Object[] args; private final Object[] args;
@ -72,18 +69,21 @@ public class RDFServiceLogger implements AutoCloseable {
} }
private void getProperties() { private void getProperties() {
ConfigurationProperties props = ConfigurationProperties.getBean(ctx); DeveloperSettings settings = DeveloperSettings.getBean(ctx);
isEnabled = Boolean.valueOf(props.getProperty(PROPERTY_ENABLED)); isEnabled = settings.getBoolean(Keys.LOGGING_RDF_ENABLE);
traceRequested = Boolean.valueOf(props traceRequested = settings.getBoolean(Keys.LOGGING_RDF_STACK_TRACE);
.getProperty(PROPERTY_STACK_TRACE));
String restrictionString = props.getProperty(PROPERTY_RESTRICTION); String restrictionString = settings
if (StringUtils.isNotBlank(restrictionString)) { .getString(Keys.LOGGING_RDF_RESTRICTION);
if (StringUtils.isBlank(restrictionString)) {
restriction = null;
} else {
try { try {
restriction = Pattern.compile(restrictionString); restriction = Pattern.compile(restrictionString);
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to compile the pattern for " log.error("Failed to compile the pattern for "
+ PROPERTY_RESTRICTION + " = " + restriction + " " + e); + Keys.LOGGING_RDF_RESTRICTION.key() + " = "
+ restriction + " " + e);
isEnabled = false; isEnabled = false;
} }
} }

View file

@ -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<Keys, Object> 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<String, String> 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<String, String[]> parameterMap) {
if (log.isDebugEnabled()) {
dumpParameterMap(parameterMap);
}
Map<String, String> fromRequest = new HashMap<>();
for (String key : parameterMap.keySet()) {
fromRequest.put(key, parameterMap.get(key)[0]);
}
update(fromRequest);
}
private void update(Map<String, String> 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<String, Object> getSettingsMap() {
Map<String, Object> 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<String, String[]> parameterMap) {
Map<String, List<String>> map = new HashMap<>();
for (String key : parameterMap.keySet()) {
map.put(key, Arrays.asList(parameterMap.get(key)));
}
log.debug("Parameter map: " + map);
}
}

View file

@ -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<String, Object> bodyMap = buildBodyMap(isAuthorized(vreq),
settings);
String rendered = renderTemplate(vreq, bodyMap);
resp.getWriter().write(rendered);
} catch (Exception e) {
doError(resp, e.toString(), 500);
}
}
private Map<String, Object> buildBodyMap(boolean authorized,
DeveloperSettings settings) {
Map<String, Object> settingsMap = new HashMap<>();
settingsMap.putAll(settings.getSettingsMap());
settingsMap.put("mayControl", authorized);
Map<String, Object> bodyMap = new HashMap<>();
bodyMap.put("settings", settingsMap);
return bodyMap;
}
private String renderTemplate(VitroRequest vreq, Map<String, Object> 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;
}
}

View file

@ -51,7 +51,7 @@ public class I18nStub extends I18n {
private class I18nBundleStub extends I18nBundle { private class I18nBundleStub extends I18nBundle {
public I18nBundleStub(String bundleName) { public I18nBundleStub(String bundleName) {
super(bundleName, new DummyResourceBundle()); super(bundleName, new DummyResourceBundle(), null);
} }
@Override @Override

View file

@ -2,6 +2,8 @@
</header> </header>
<#include "developer.ftl">
<nav role="navigation"> <nav role="navigation">
<ul id="main-nav" role="list"> <ul id="main-nav" role="list">
<#list menu.items as item> <#list menu.items as item>

View file

@ -814,6 +814,15 @@
<url-pattern>/searchHelp</url-pattern> <url-pattern>/searchHelp</url-pattern>
</servlet-mapping> </servlet-mapping>
<servlet>
<servlet-name>DeveloperAjax</servlet-name>
<servlet-class>edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettingsServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DeveloperAjax</servlet-name>
<url-pattern>/admin/developerAjax</url-pattern>
</servlet-mapping>
<!-- for now, need to make sure the links on CALS' site doesn't break --> <!-- for now, need to make sure the links on CALS' site doesn't break -->
<servlet-mapping> <servlet-mapping>
<servlet-name>SearchController</servlet-name> <servlet-name>SearchController</servlet-name>

View file

@ -0,0 +1,82 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
function DeveloperPanel(developerAjaxUrl) {
this.setupDeveloperPanel = updateDeveloperPanel;
function updateDeveloperPanel(data) {
$.ajax({
url: developerAjaxUrl,
dataType: "json",
data: data,
complete: function(xhr, status) {
updatePanelContents(xhr.responseText);
if (document.getElementById("developerPanelSaveButton")) {
enablePanelOpener();
addBehaviorToElements();
updateDisabledFields();
}
}
});
}
function updatePanelContents(contents) {
document.getElementById("developerPanel").innerHTML = contents;
}
function enablePanelOpener() {
document.getElementById("developerPanelClickMe").onclick = function() {
document.getElementById("developerPanelClickText").style.display = "none";
document.getElementById("developerPanelBody").style.display = "block";
};
}
function addBehaviorToElements() {
document.getElementById("developerPanelSaveButton").onclick = function() {
updateDeveloperPanel(collectFormData());
}
document.getElementById("developer.enabled").onchange = updateDisabledFields
document.getElementById("developer.loggingRDFService.enable").onchange = updateDisabledFields
}
function updateDisabledFields() {
var developerEnabled = document.getElementById("developer.enabled").checked;
document.getElementById("developer.defeatFreemarkerCache").disabled = !developerEnabled;
document.getElementById("developer.insertFreemarkerDelimiters").disabled = !developerEnabled;
document.getElementById("developer.i18n.defeatCache").disabled = !developerEnabled;
document.getElementById("developer.i18n.logStringRequests").disabled = !developerEnabled;
document.getElementById("developer.loggingRDFService.enable").disabled = !developerEnabled;
var rdfServiceEnabled = developerEnabled && document.getElementById("developer.loggingRDFService.enable").checked;
document.getElementById("developer.loggingRDFService.stackTrace").disabled = !rdfServiceEnabled;
document.getElementById("developer.loggingRDFService.restriction").disabled = !rdfServiceEnabled;
}
function collectFormData() {
var data = new Object();
data["developer.panelOpen"] = false;
getCheckbox("developer.enabled", data);
getCheckbox("developer.defeatFreemarkerCache", data);
getCheckbox("developer.insertFreemarkerDelimiters", data);
getCheckbox("developer.i18n.defeatCache", data);
getCheckbox("developer.i18n.logStringRequests", data);
getCheckbox("developer.loggingRDFService.enable", data);
getCheckbox("developer.loggingRDFService.stackTrace", data);
getText("developer.loggingRDFService.restriction", data);
return data;
}
function getCheckbox(key, dest) {
dest[key] = document.getElementById(key).checked;
}
function getText(key, dest) {
dest[key] = document.getElementById(key).value;
}
}
/*
* Relies on the global variable for the AJAX URL.
*/
$(document).ready(function() {
new DeveloperPanel(developerAjaxUrl).setupDeveloperPanel({});
});

View file

@ -0,0 +1,5 @@
<#-- $This file is distributed under the terms of the license in /doc/license.txt$ -->
<div id="developerPanel" > </div>
<script>developerAjaxUrl = '${urls.developerAjax}'</script>
${scripts.add('<script type="text/javascript" src="${urls.base}/js/developer/developerPanel.js"></script>')}

View file

@ -0,0 +1,91 @@
<#-- $This file is distributed under the terms of the license in /doc/license.txt$ -->
<#macro showCheckbox key>
<input type="checkbox" id="${key}" <#if settings[key]>checked</#if>>
</#macro>
<#macro showTextbox key>
<input type="text" id="${key}" size="40" value="${settings[key]}" >
</#macro>
<style>
div.developer {
background-color: red;
padding: 0px 10px 0px 10px;
font-size: small;
font-variant: small-caps;
}
div.developer #developerPanelBody {
display: none;
}
div.developer .container {
border: thin groove black
}
</style>
<#if !settings["developer.enabled"]>
<#elseif !settings["mayControl"]>
<div class="developer">
<h1>${siteName} is running in developer mode.</h1>
</div>
<#else>
<div class="developer">
<h1 id="developerPanelClickMe">${siteName} is running in developer mode.
<span id="developerPanelClickText">(click for Options)</span>
</h1>
<div id="developerPanelBody">
<form>
<label>
<@showCheckbox "developer.enabled" />
Enable developer mode
</label>
<div class="container">
Freemarker templates
<label>
<@showCheckbox "developer.defeatFreemarkerCache" />
Defeat the template cache
</label>
<label>
<@showCheckbox "developer.insertFreemarkerDelimiters" />
Insert HTML comments at start and end of templates
</label>
</div>
<div class="container">
SPARQL Queries
<label>
<@showCheckbox "developer.loggingRDFService.enable" />
Log each query
</label>
<label>
<@showCheckbox "developer.loggingRDFService.stackTrace" />
Add stack trace
</label>
<label>
Restrict by calling stack
<@showTextbox "developer.loggingRDFService.restriction" />
</label>
</div>
<div class="container">
Language support
<label>
<@showCheckbox "developer.i18n.defeatCache" />
Defeat the cache of language property files
</label>
<label>
<@showCheckbox "developer.i18n.logStringRequests" />
Log the retrieval of language strings
</label>
</div>
<input type="button" id="developerPanelSaveButton" value="Save Settings" name="foo" />
</form>
</div>
</div>
</#if>

View file

@ -1,5 +1,7 @@
<#-- $This file is distributed under the terms of the license in /doc/license.txt$ --> <#-- $This file is distributed under the terms of the license in /doc/license.txt$ -->
<#include "developer.ftl">
<nav role="navigation"> <nav role="navigation">
<ul id="main-nav" role="list"> <ul id="main-nav" role="list">
<#list menu.items as item> <#list menu.items as item>