package edu.mit.simile.butterfly;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TimeZone;
import java.util.Timer;
import java.util.TimerTask;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.collections.ExtendedProperties;
import org.apache.log4j.PropertyConfigurator;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Script;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import edu.mit.simile.butterfly.velocity.ButterflyResourceLoader;
import edu.mit.simile.butterfly.velocity.Super;
/**
* This is the Butterfly servlet and the main entry point
* for a Butterfly-powered web application. This servlet is
* responsible for loading, configuring and wire together
* the various modules that compose your webapp and then
* manages the dispatching of requests to the modules that
* are supposed to handle them.
*/
public class Butterfly extends HttpServlet {
public static final String HOST_HEADER = "X-Forwarded-Host";
public static final String CONTEXT_HEADER = "X-Context-Path";
private static final long serialVersionUID = 1938797827088619577L;
private static final long watcherDelay = 1000;
public static final String NAME = "butterfly.name";
public static final String APPENGINE = "butterfly.appengine";
public static final String AUTORELOAD = "butterfly.autoreload";
public static final String HOME = "butterfly.home";
public static final String ZONE = "butterfly.zone";
public static final String BASE_URL = "butterfly.url";
public static final String DEFAULT_ZONE = "butterfly.default.zone";
public static final String DEFAULT_MOUNTPOINT = "butterfly.default.mountpoint";
public static final String MODULES_IGNORE = "butterfly.modules.ignore";
public static final String MODULES_PATH = "butterfly.modules.path";
public static final String MAIN_ZONE = "main";
final static List<String> CONTROLLER;
static {
CONTROLLER = new ArrayList<String>();
CONTROLLER.add("controller.js");
}
// --------------------- static ----------------------------------
public static String getTrueHost(HttpServletRequest request) {
String host = request.getHeader(HOST_HEADER);
if (host != null) {
String[] hosts = host.split(",");
host = hosts[hosts.length - 1];
}
return host;
}
public static String getTrueContextPath(HttpServletRequest request, boolean absolute) {
String context = request.getHeader(CONTEXT_HEADER);
if (context != null) {
if (context.charAt(context.length() - 1) == '/') context = context.substring(0, context.length() - 1);
} else {
context = request.getContextPath();
}
if (absolute) {
return getFullHost(request) + context;
} else {
return context;
}
}
public static String getTrueRequestURI(HttpServletRequest request, boolean absolute) {
return getTrueContextPath(request,absolute) + request.getPathInfo();
}
public static String getFullHost(HttpServletRequest request) {
StringBuffer prefix = new StringBuffer();
String protocol = request.getScheme();
prefix.append(protocol);
prefix.append("://");
String proxy = getTrueHost(request);
if (proxy != null) {
prefix.append(proxy);
} else {
prefix.append(request.getServerName());
int port = request.getServerPort();
if (!((protocol.equals("http") && port == 80) || (protocol.equals("https") && port == 443))) {
prefix.append(':');
prefix.append(port);
}
}
return prefix.toString();
}
public static boolean isGAE(ServletConfig config) {
return (config.getServletContext().getServerInfo().indexOf("Google App Engine") != -1);
}
// ---------------------------------------------------------------
transient private Logger _logger;
private boolean _autoreload;
private boolean _appengine;
private String _name;
private String _default_mountpoint;
private int _routingCookieMaxAge;
private String[] _ignores;
transient protected Timer _timer;
transient protected ButterflyClassLoader _classLoader;
transient protected ButterflyScriptWatcher _scriptWatcher;
transient protected ServletConfig _config;
transient protected ServletContext _context;
transient protected ButterflyMounter _mounter;
protected ExtendedProperties _properties;
protected File _contextDir;
protected File _homeDir;
protected File _webInfDir;
protected Exception _configurationException;
protected boolean _configured = false;
protected ContextFactory contextFactory;
class ButterflyContextFactory extends ContextFactory {
protected void onContextCreated(Context cx) {
cx.setOptimizationLevel(9);
super.onContextCreated(cx);
}
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
_config = config;
_appengine = isGAE(config);
_name = System.getProperty(NAME, "butterfly");
_context = config.getServletContext();
_context.setAttribute(NAME, _name);
_context.setAttribute(APPENGINE, _appengine);
_contextDir = new File(_context.getRealPath("/"));
_webInfDir = new File(_contextDir, "WEB-INF");
_properties = new ExtendedProperties();
_mounter = new ButterflyMounter();
// Load the butterfly properties
String props = System.getProperty("butterfly.properties");
File butterflyProperties = (props == null) ? new File(_webInfDir, "butterfly.properties") : new File(props);
BufferedInputStream is = null;
try {
is = new BufferedInputStream(new FileInputStream(butterflyProperties));
_properties.load(is);
} catch (FileNotFoundException e) {
throw new ServletException("Could not find butterfly properties file",e);
} catch (IOException e) {
throw new ServletException("Could not read butterfly properties file",e);
} finally {
try {
is.close();
} catch (Exception e) {
// ignore
}
}
// Process eventual properties includes
String includes = _properties.getString("butterfly.includes");
if (includes != null) {
for (String prop : includes.split(",")) {
File prop_file = (prop.startsWith("/")) ? new File(prop) : new File(_webInfDir, prop);
try {
is = new BufferedInputStream(new FileInputStream(prop_file));
ExtendedProperties p = new ExtendedProperties();
p.load(is);
_properties.combine(p);
} catch (Exception e) {
// ignore
} finally {
try {
is.close();
} catch (Exception e) {
// ignore
}
}
}
}
// Overload with properties set from the command line
// using the -Dkey=value parameters to the JVM
Properties systemProperties = System.getProperties();
for (Iterator<Object> i = systemProperties.keySet().iterator(); i.hasNext(); ) {
String key = (String) i.next();
String value = systemProperties.getProperty(key);
_properties.setProperty(key, value);
}
_default_mountpoint = _properties.getString(DEFAULT_MOUNTPOINT, "/modules");
_ignores = _properties.getString(MODULES_IGNORE, "").split(",");
_autoreload = _properties.getBoolean(AUTORELOAD, false);
if (!_appengine) {
String log4j = System.getProperty("butterfly.log4j");
File logProperties = (log4j == null) ? new File(_webInfDir, "log4j.properties") : new File(log4j);
if (logProperties.exists()) {
if (_autoreload) {
PropertyConfigurator.configureAndWatch(logProperties.getAbsolutePath(), watcherDelay);
} else {
PropertyConfigurator.configure(logProperties.getAbsolutePath());
}
}
}
_logger = LoggerFactory.getLogger(_name);
_logger.info("Starting {} ...", _name);
_logger.info("Properties loaded from {}", butterflyProperties);
if (_autoreload) _logger.info("Autoreloading is enabled");
if (_appengine) _logger.info("Running in Google App Engine");
_logger.debug("> init");
_logger.debug("> initialize classloader");
try {
_classLoader = AccessController.doPrivileged (
new PrivilegedAction<ButterflyClassLoader>() {
public ButterflyClassLoader run() {
return new ButterflyClassLoader(this.getClass().getClassLoader());
}
}
);
Thread.currentThread().setContextClassLoader(_classLoader);
_classLoader.watch(butterflyProperties); // reload if the butterfly properties change
contextFactory = new ButterflyContextFactory();
contextFactory.initApplicationClassLoader(_classLoader); // tell rhino to use this classloader as well
ContextFactory.initGlobal(contextFactory);
if (_autoreload && !_appengine) {
_timer = new Timer(true);
TimerTask classloaderWatcher = _classLoader.getClassLoaderWatcher(new Trigger(_contextDir));
_timer.schedule(classloaderWatcher, watcherDelay, watcherDelay);
}
} catch (Exception e) {
throw new ServletException("Failed to load butterfly classloader", e);
}
_logger.debug("< initialize classloader");
if (_autoreload && !_appengine) {
_logger.debug("> initialize script watcher");
_scriptWatcher = new ButterflyScriptWatcher();
_timer.schedule(_scriptWatcher, watcherDelay, watcherDelay);
_logger.debug("< initialize script watcher");
}
this.configure();
_logger.debug("< init");
}
@Override
public void destroy() {
_logger.info("Stopping Butterfly...");
for (ButterflyModule m : _modulesByName.values()) {
try {
_logger.debug("> destroying {}", m);
m.destroy();
_logger.debug("< destroying {}", m);
} catch (Exception e) {
_logger.error("Exception caught while destroying '" + m + "'", e);
}
}
if (_timer != null) {
_timer.cancel();
}
_logger.info("done.");
}
@SuppressWarnings("unchecked")
public void configure() {
_logger.debug("> configure");
_logger.info("> process properties");
try {
String homePath = _properties.getString(HOME);
if (homePath == null) {
_homeDir = _contextDir;
} else {
_homeDir = new File(homePath);
}
_logger.info("Butterfly home: {}", _homeDir);
Iterator<String> i = _properties.getKeys(ZONE);
while (i.hasNext()) {
String zone = i.next();
String path = _properties.getString(zone);
zone = zone.substring(ZONE.length() + 1);
_logger.info("Zone path: [{}] -> {}", zone, path);
_mounter.registerZone(zone, path);
}
String defaultZone = _properties.getString(DEFAULT_ZONE);
if (defaultZone != null) {
_logger.info("Default zone is: '{}'", defaultZone);
_mounter.setDefaultZone(defaultZone);
} else {
String baseURL = _properties.getString(BASE_URL,"/");
_mounter.registerZone(MAIN_ZONE, baseURL);
_mounter.setDefaultZone(MAIN_ZONE);
}
String language = _properties.getString("butterfly.locale.language");
String country = _properties.getString("butterfly.locale.country");
String variant = _properties.getString("butterfly.locale.variant");
if (language != null) {
if (country != null) {
if (variant != null) {
Locale.setDefault(new Locale(language, country, variant));
} else {
Locale.setDefault(new Locale(language, country));
}
} else {
Locale.setDefault(new Locale(language));
}
}
String timeZone = _properties.getString("butterfly.timeZone");
if (timeZone != null) {
TimeZone.setDefault(TimeZone.getTimeZone(timeZone));
}
_routingCookieMaxAge = _properties.getInt("butterfly.routing.cookie.maxage",-1);
} catch (Exception e) {
_configurationException = new Exception("Failed to load butterfly properties", e);
}
_logger.info("< process properties");
_logger.info("> load modules");
// load modules from the properties found in the butterfly.properties
List<String> paths = _properties.getList(MODULES_PATH);
for (String path : paths) {
findModulesIn(absolutize(_homeDir, path.trim()));
}
// load modules from the path found in the servlet init properties
String servlet_paths = this._config.getInitParameter(MODULES_PATH);
if (servlet_paths != null) {
for (String path : servlet_paths.split(",")) {
findModulesIn(absolutize(_homeDir, path.trim()));
}
}
_logger.info("< load modules");
_logger.info("> create modules");
for (String name : _moduleProperties.keySet()) {
createModule(name);
}
_logger.info("< create modules");
_logger.info("> load module wirings");
ExtendedProperties wirings = new ExtendedProperties();
try {
// Load the wiring properties
File moduleWirings = absolutize(_homeDir, _properties.getString("butterfly.modules.wirings","WEB-INF/modules.properties"));
_logger.info("Loaded module wirings from: {}", moduleWirings);
_classLoader.watch(moduleWirings); // reload if the module wirings change
FileInputStream fis = new FileInputStream(moduleWirings);
wirings.load(fis);
fis.close();
} catch (Exception e) {
_configurationException = new Exception("Failed to load module wirings", e);
}
_logger.info("< load module wirings");
_logger.info("> wire modules");
try {
wireModules(wirings);
} catch (Exception e) {
_configurationException = new Exception("Failed to wire modules", e);
}
_logger.info("< wire modules");
_logger.info("> configure modules");
try {
configureModules();
} catch (Exception e) {
_configurationException = new Exception("Failed to configure modules", e);
}
_logger.info("< configure modules");
_logger.info("> initialize modules");
Set<String> initialized = new HashSet<String>();
Set<String> initializing = new HashSet<String>();
for (String name : _modulesByName.keySet()) {
initializeModule(name, initialized, initializing);
}
_logger.info("< initialize modules");
_configured = true;
_logger.debug("< configure");
}
protected void initializeModule(String name, Set<String> initialized, Set<String> initializing) {
ButterflyModule m = _modulesByName.get(name);
if (m != null && !initialized.contains(name)) {
_logger.debug("> initialize " + m.getName());
if (initializing.contains(name)) {
_logger.warn("Circular dependencies detected involving module " + m);
} else {
initializing.add(name);
for (String depends : m.getDependencies().keySet()) {
initializeModule(depends, initialized, initializing);
}
initializing.remove(name);
}
try {
m.init(getServletConfig());
} catch (Exception e) {
_configurationException = new Exception("Failed to initialize module " + m, e);
}
_logger.debug("< initialize " + m.getName());
initialized.add(name);
}
}
@Override
@SuppressWarnings("unchecked")
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getMethod();
String path = request.getPathInfo();
String urlQuery = request.getQueryString();
if (_mounter != null) {
Zone zone = _mounter.getZone(request);
if (_logger.isDebugEnabled()) {
_logger.debug("> " + method + " [" + zone.getName() + "] " + path + ((urlQuery != null) ? "?" + urlQuery : ""));
Enumeration<String> en = request.getHeaderNames();
while (en.hasMoreElements()) {
String header = en.nextElement();
_logger.trace("{}: {}", header, request.getHeader(header));
}
} else if (_logger.isInfoEnabled()) {
String zoneName = (zone != null) ? zone.getName() : "";
_logger.info("{} {} [{}]", new String[] { method,path,zoneName });
}
setRoutingCookie(request, response);
try {
if (_configured) {
if (_configurationException == null) {
ButterflyModule module = _mounter.getModule(path,zone);
_logger.debug("Module '{}' will handle the request", module.getName());
String localPath = module.getRelativePath(request);
if (!module.process(localPath, request, response)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
} else {
error(response, "Butterfly Error", "Butterfly incurred in the following errors while initializing:", _configurationException);
}
} else {
delay(response, "Butterfly is still initializing...");
}
} catch (FileNotFoundException e) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
} catch (Exception e) {
error(response, "Butterfly Error", "Butterfly caught the following error while processing the request:", e);
}
response.flushBuffer();
if (_logger.isDebugEnabled()) _logger.debug("< " + method + " [" + zone.getName() + "] " + path + ((urlQuery != null) ? "?" + urlQuery : ""));
} else {
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
}
// ---------------------------- private -----------------------------------
final static private String dependencyPrefix = "requires";
final static private String implementsProperty = "implements";
final static private String extendsProperty = "extends";
protected Map<String,ButterflyModule> _modulesByName = new HashMap<String,ButterflyModule>();
protected Map<String,Map<String,ButterflyModule>> _modulesByInterface = new HashMap<String,Map<String,ButterflyModule>>();
protected Map<String,ExtendedProperties> _moduleProperties = new HashMap<String,ExtendedProperties>();
protected Map<String,Boolean> _created = new HashMap<String,Boolean>();
final static private String routingCookie = "host";
/*
* This method adds a cookie to the response that will be used by mod_proxy_balancer
* to know what server is supposed to be handling all the requests of this user agent.
*/
protected void setRoutingCookie(HttpServletRequest request, HttpServletResponse response) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (routingCookie.equals(cookie.getName())) {
return;
}
}
}
Cookie cookie = new Cookie(routingCookie, "." + _name); // IMPORTANT: the initial dot is required by mod_proxy_balancer!
cookie.setMaxAge(_routingCookieMaxAge); // delete at end of browser session
cookie.setPath("/");
response.addCookie(cookie);
}
protected File absolutize(File base, String location) {
if (location == null || location.length() == 0) { // we got an empty location
return base;
} else if (location.indexOf(':') > 0) { // we got an absolute windows location (ie c:\blah)
return new File(location);
} else if (location.charAt(0) == '/' || location.charAt(0) == '\\') { // we got an absolute location
return new File(location);
} else { // we got a relative location
return new File(base, location);
}
}
protected static final String PATH_PROP = "__path__";
protected void findModulesIn(File f) {
_logger.debug("look for modules in {}", f);
File modFile = new File(f,"MOD-INF");
if (modFile.exists()) {
_logger.trace("> findModulesIn({})", f);
try {
String name = f.getName();
ExtendedProperties p = new ExtendedProperties();
File propFile = new File(modFile,"module.properties");
if (propFile.exists()) {
_classLoader.watch(propFile); // reload if the the module properties change
BufferedInputStream stream = new BufferedInputStream(new FileInputStream(propFile));
p.load(stream);
stream.close();
}
p.addProperty(PATH_PROP, f.getAbsolutePath());
if (p.containsKey("name")) {
name = p.getString("name");
}
boolean load = true;
for (String s : _ignores) {
if (name.matches(s)) {
load = false;
break;
}
}
if (load) {
_moduleProperties.put(name, p);
}
} catch (Exception e) {
_logger.error("Error finding module wirings", e);
}
_logger.trace("< findModulesIn({})", f);
} else {
File[] files = f.listFiles();
if (files != null) {
for (int i = 0; i < files.length; i++) {
File file = files[i];
try {
if (file.isDirectory()) {
findModulesIn(file);
}
} catch (AccessControlException e) {
// skip
// NOTE: this is needed for Google App Engine that doesn't like us snooping around the internal file system
}
}
}
}
}
protected ButterflyModule createModule(String name) {
_logger.trace("> Creating module: {}", name);
if (_modulesByName.containsKey(name)) {
_logger.trace("< Module '{}' already exists", name);
return _modulesByName.get(name);
}
ExtendedProperties p = _moduleProperties.get(name);
File path = new File(p.getString(PATH_PROP));
_logger.debug("Module path: {}", path);
File classes = new File(path,"MOD-INF/classes");
if (classes.exists()) {
_classLoader.addRepository(classes);
}
File libs = new File(path,"MOD-INF/lib");
if (libs.exists()) {
_classLoader.addRepository(libs);
}
ButterflyModule m = new ButterflyModuleImpl();
// process module's controller
String manager = p.getString("module-impl");
if (manager != null && !manager.equals(m.getClass().getName())) {
try {
Class<?> c = _classLoader.loadClass(manager);
m = (ButterflyModule) c.newInstance();
} catch (Exception e) {
_logger.error("Error loading special module manager", e);
}
}
m.setName(name);
m.setPath(path);
m.setModules(_modulesByName);
m.setMounter(_mounter);
m.setClassLoader(_classLoader);
m.setTimer(_timer);
_modulesByName.put(name,m);
// process inheritance
ButterflyModule parentModule = null;
String parentName = p.getString(extendsProperty);
if (parentName != null) {
if (_moduleProperties.containsKey(parentName)) {
if (_modulesByName.containsKey(parentName)) {
parentModule = _modulesByName.get(parentName);
} else {
parentModule = createModule(parentName);
}
} else {
throw new RuntimeException("Cannot wire module '" + name + "' because the extended module '" + parentName + "' is not defined.");
}
}
if (parentModule != null) {
m.setExtended(parentModule);
parentModule.addExtendedBy(m);
}
_logger.trace("< Creating module: {}", name);
return m;
}
@SuppressWarnings("unchecked")
protected void wireModules(ExtendedProperties wirings) {
_logger.trace("> wireModules()");
_logger.info("mounting modules");
for (String name : _moduleProperties.keySet()) {
_logger.trace("> Mounting module: {}", name);
ButterflyModule m = _modulesByName.get(name);
String mountPointStr = wirings.getString(m.getName());
if (mountPointStr == null) {
String moduleName = m.getName();
String mountPoint = _default_mountpoint + "/" + m.getName();
_logger.info("No mount point defined for module '" + moduleName + "', mounting to '" + mountPoint + "'");
mountPointStr = mountPoint;
}
MountPoint mountPoint = new MountPoint(mountPointStr);
if (_mounter.isRegistered(mountPoint)) {
throw new RuntimeException("Cannot have two different modules with the same mount point '" + mountPoint + "'.");
} else {
_mounter.register(mountPoint, m);
}
_logger.trace("< Mounting module: {}", name);
}
for (String name : _moduleProperties.keySet()) {
_logger.trace("> Expanding properties for module: {}", name);
ButterflyModule m = _modulesByName.get(name);
ExtendedProperties p = _moduleProperties.get(name);
ButterflyModule extended = m.getExtendedModule();
while (extended != null) {
_logger.trace("> Merging properties from extended module: {}", name);
ExtendedProperties temp = p;
p = _moduleProperties.get(extended.getName());
p.combine(temp);
_logger.trace("< Merging properties from extended module: {} -> {}", name, p);
extended = extended.getExtendedModule();
}
_moduleProperties.put(name,p);
List<String> implementations = p.getList(implementsProperty);
if (implementations != null) {
for (String i : implementations) {
Map<String, ButterflyModule> map = _modulesByInterface.get(i);
if (map == null) {
map = new HashMap<String,ButterflyModule>();
_modulesByInterface.put(i, map);
}
map.put(name, m);
m.setImplementation(i);
}
}
_logger.trace("< Expanding properties for module: {}", name);
}
for (String name : _moduleProperties.keySet()) {
_logger.trace("> Inject dependencies in module: {}", name);
ExtendedProperties p = _moduleProperties.get(name);
ButterflyModule m = _modulesByName.get(name);
for (Object o : p.keySet()) {
String s = (String) o;
if (s.equals(dependencyPrefix)) {
for (Object oo : p.getList(s)) {
String dep = (String) oo;
_logger.trace("> Processing dependency: {}", dep);
dep = dep.trim();
Map<String,ButterflyModule> modules = _modulesByInterface.get(dep);
if (modules != null) {
if (modules.size() == 1) {
// if there's only one module implementing that interface, wiring is automatic
setDependency(m, dep, modules.values().iterator().next());
} else {
ButterflyModule parent = m.getExtendedModule();
do {
String wiredDependency = wirings.getString(name + "." + dep);
if (wiredDependency != null) {
setDependency(m, dep, _modulesByName.get(wiredDependency));
break;
} else {
if (parent != null) {
name = parent.getName();
}
}
} while (parent != null);
}
} else {
throw new RuntimeException("Cannot wire module '" + name + "' because no module implements the required interface '" + dep + "'");
}
_logger.trace("< Processing dependency: {}", dep);
}
}
}
_logger.trace("< Inject dependencies in module: {}", name);
}
ButterflyModule rootModule = _mounter.getRootModule();
// in case nothing defined the root mount point use the default one
if (rootModule == null) {
rootModule = _modulesByName.get("main");
}
// in case not even the 'main' module is available, give up
if (rootModule == null) {
throw new RuntimeException("Cannot initialize the modules because I can't guess which module to mount to '/'");
}
_logger.trace("< wireModules()");
}
@SuppressWarnings("unchecked")
protected void configureModules() {
_logger.trace("> configureModules()");
for (String name : _moduleProperties.keySet()) {
_logger.trace("> Configuring module: {}", name);
ExtendedProperties p = _moduleProperties.get(name);
ButterflyModule m = _modulesByName.get(name);
// make the system properties accessible to the modules
m.setProperties(_properties);
try {
if (p.getBoolean("templating", Boolean.TRUE)) {
_logger.trace("> enabling templating");
// load the default velocity properties
Properties properties = new Properties();
File velocityProperties = new File(_webInfDir, "velocity.properties");
_classLoader.watch(velocityProperties); // reload if the velocity properties change
FileInputStream fis = new FileInputStream(velocityProperties);
properties.load(fis);
fis.close();
// set properties for resource loading
properties.setProperty("resource.loader", "butterfly");
properties.setProperty("butterfly.resource.loader.class", ButterflyResourceLoader.class.getName());
properties.setProperty("butterfly.resource.loader.cache", "true");
properties.setProperty("butterfly.resource.loader.modificationCheckInterval", "1");
properties.setProperty("butterfly.resource.loader.description", "Butterfly Resource Loader");
// set properties for macros
properties.setProperty("velocimacro.library", p.getString("templating.macros", ""));
// Set our special parent injection directive
properties.setProperty("userdirective", Super.class.getName());
// Set logging properties
if (_appengine) {
properties.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, "org.apache.velocity.runtime.log.JdkLogChute");
} else {
properties.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, "org.apache.velocity.runtime.log.Log4JLogChute");
properties.setProperty("runtime.log.logsystem.log4j.logger", "velocity");
}
// create a module-specific velocity engine
VelocityEngine velocity = new VelocityEngine();
velocity.setApplicationAttribute("module", m); // this is how we pass the module to the resource loader
velocity.init(properties);
// inject the template engine in the module
m.setTemplateEngine(velocity);
_logger.trace("< enabling templating");
}
List<String> scriptables = p.getList("scriptables");
if (scriptables.size() > 0) {
Context context = Context.enter();
BufferedReader initializerReader = null;
for (String scriptable : scriptables) {
if (!scriptable.equals("")) {
try {
_logger.trace("> adding scriptable object: {}", scriptable);
@SuppressWarnings("rawtypes")
Class c = _classLoader.loadClass(scriptable);
ButterflyScriptableObject o = (ButterflyScriptableObject) c.newInstance();
setScriptable(m, o);
URL initializer = c.getResource("init.js");
if (initializer != null) {
initializerReader = new BufferedReader(new InputStreamReader(initializer.openStream()));
setScript(m, initializer, context.compileReader(initializerReader, "init.js", 1, null));
_scriptWatcher.watch(initializer,m);
_logger.trace("Parsed scriptable javascript initializer successfully");
}
_logger.trace("< adding scriptable object: {}", scriptable);
} catch (Exception e) {
_logger.trace("Error initializing scriptable object '{}': {}", scriptable, e);
} finally {
if (initializerReader != null) initializerReader.close();
}
}
}
Context.exit();
}
List<String> controllers = p.getList("controller", CONTROLLER);
Set<URL> controllerURLs = new HashSet<URL>(controllers.size());
for (String controller : controllers) {
URL controllerURL = m.getResource("MOD-INF/" + controller);
if (controllerURL != null) {
controllerURLs.add(controllerURL);
}
}
if (controllerURLs.size() > 0) {
_logger.trace("> enabling javascript control");
Context context = Context.enter();
BufferedReader initializerReader = null;
try {
URL initializer = this.getClass().getClassLoader().getResource("edu/mit/simile/butterfly/Butterfly.js");
initializerReader = new BufferedReader(new InputStreamReader(initializer.openStream()));
setScript(m, initializer, context.compileReader(initializerReader, "Butterfly.js", 1, null));
watch(initializer,m);
_logger.trace("Parsed javascript initializer successfully");
} finally {
if (initializerReader != null) initializerReader.close();
}
BufferedReader controllerReader = null;
for (URL controllerURL : controllerURLs) {
try{
controllerReader = new BufferedReader(new InputStreamReader(controllerURL.openStream()));
setScript(m, controllerURL, context.compileReader(controllerReader, controllerURL.toString(), 1, null));
watch(controllerURL,m);
_logger.trace("Parsed javascript controller successfully: {}", controllerURL);
} finally {
if (controllerReader != null) controllerReader.close();
}
}
Context.exit();
_logger.trace("< enabling javascript control");
}
} catch (Exception e) {
_logger.error("Error enabling javascript control",e);
}
_logger.trace("< Configuring module: {}", name);
}
_logger.trace("< configureModules()");
}
protected void setDependency(ButterflyModule subj, String dep, ButterflyModule obj) {
subj.setDependency(dep, obj);
ButterflyModule extended = subj.getExtendedModule();
if (extended != null) {
setDependency(extended, dep, obj);
}
}
protected void setScriptable(ButterflyModule mod, ButterflyScriptableObject scriptable) {
mod.setScriptable(scriptable);
ButterflyModule extended = mod.getExtendedModule();
if (extended != null) {
setScriptable(extended, scriptable);
}
}
protected void watch(URL script, ButterflyModule module) throws IOException {
if (_scriptWatcher != null) {
_scriptWatcher.watch(script, module);
}
}
/*
* NOTE(SM): I'm fully aware that these embedded HTML snippets are really ugly, but I don't
* want to depend on velocity for error reporting as that would prevent us from reporting
* errors about velocity's dependency itself.
*/
String header =
"<html>" +
" <head>" +
" </head>" +
" <body>";
String footer = "</body></html>";
protected void delay(HttpServletResponse response, String title) throws IOException {
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.println(header);
writer.println("<h1>" + title + "</h1>");
writer.println("<script>setTimeout(function() { window.location = '.' }, 3000);</script>");
writer.println(footer);
writer.close();
}
protected void error(HttpServletResponse response, String title, String msg, Exception e) throws IOException {
StringWriter stringWriter = new StringWriter();
PrintWriter writer = new PrintWriter(stringWriter);
writer.println(title);
writer.println(msg);
if (e != null) {
e.printStackTrace(writer);
}
writer.close();
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, stringWriter.toString());
}
static protected void setScript(ButterflyModule mod, URL location, Script script) {
mod.setScript(location, script);
ButterflyModule extended = mod.getExtendedModule();
if (extended != null) {
setScript(extended, location, script);
}
}
/*
* This is the trigger invoked by the butterfly classloader if any of the observed classes or files
* has changed. This trigger attempts to find the Butterfly.class on disk and changes its lastModified
* time if found. This has no effect in some servlet containers, but in others (for example the Jetty
* plugin for Maven) this triggers a context autoreload.
* NOTE: this is only invoked when files that were found when the application started are modified
* Adding new files to the classpath does not trigger a restart!
*/
private static class Trigger implements Runnable {
final static private Logger _logger = LoggerFactory.getLogger("butterfly.trigger");
private List<File> tries = new ArrayList<File>();
Trigger(File context) {
File web_inf = new File(context, "WEB-INF");
File classes = new File(web_inf, "classes");
if (classes.exists()) {
tries.add(findFile(classes, ".class"));
}
File libs = new File(web_inf, "lib");
if (libs.exists()) {
tries.add(findFile(libs, ".jar"));
}
}
public void run() {
_logger.info("classloader changed trigger invoked");
for (File f : tries) {
_logger.debug("trying: " + f.getAbsolutePath());
if (f.exists()) {
f.setLastModified((new Date()).getTime());
_logger.debug(" touched!!");
return;
}
}
_logger.warn("could not find anything to touch");
}
private File findFile(File start, String extension) {
for (File f : start.listFiles()) {
if (f.isDirectory()) {
return findFile(f, extension);
} else {
if (f.getName().endsWith(extension)) {
return f;
}
}
}
return null;
}
}
}
class ButterflyScriptWatcher extends TimerTask {
final static private Logger _logger = LoggerFactory.getLogger("butterfly.script_watcher");
private Map<URL,ButterflyModule> scripts = new HashMap<URL,ButterflyModule>();
private Map<URL,Long> lastModifieds = new HashMap<URL,Long>();
protected void watch(URL script, ButterflyModule module) throws IOException {
_logger.debug("Watching {}", script);
this.lastModifieds.put(script, script.openConnection().getLastModified());
this.scripts.put(script, module);
}
public void run() {
for (URL url : this.scripts.keySet()) {
try {
URLConnection connection = url.openConnection();
long lastModified = connection.getLastModified();
if (lastModified > this.lastModifieds.get(url)) {
_logger.debug("{} has changed, reparsing...", url);
this.lastModifieds.put(url, lastModified);
ButterflyModule module = this.scripts.get(url);
BufferedReader reader = null;
try {
Context context = Context.enter();
reader = new BufferedReader(new InputStreamReader(url.openStream()));
Butterfly.setScript(module, url, context.compileReader(reader, url.getFile(), 1, null));
_logger.info("{} reloaded", url);
Context.exit();
} finally {
if (reader != null) reader.close();
}
}
connection.getInputStream().close(); // NOTE(SM): this avoids leaking file descriptions in some JVMs
} catch (Exception e) {
_logger.error("", e);
}
}
}
}