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

@ -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");
// ----------------------------------------------------------------------

View file

@ -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<String, Object> 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<String, Object> 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<TemplateLoader> loaders = new ArrayList<TemplateLoader>();
@ -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;

View file

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

View file

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

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

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.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;
}
}

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