Author: jtravis Date: 2007-03-21 11:22:00 -0800 (Wed, 21 Mar 2007) New Revision: 3802 URL: http://svn.hyperic.org/?view=rev&root=Hyperic+HQ&revision=3802 Added: trunk/src/org/hyperic/hq/ui/rendit/ trunk/src/org/hyperic/hq/ui/rendit/InvocationBindings.java trunk/src/org/hyperic/hq/ui/rendit/PluginWrapper.java trunk/src/org/hyperic/hq/ui/rendit/RenditServer.java trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/ trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/org/ trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/org/hyperic/ trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/org/hyperic/hq/ trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/org/hyperic/hq/rendit/ trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/org/hyperic/hq/rendit/BaseController.groovy trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/org/hyperic/hq/rendit/dispatcher.groovy trunk/src/org/hyperic/hq/ui/rendit/servlet/ trunk/src/org/hyperic/hq/ui/rendit/servlet/DirWatcher.java trunk/src/org/hyperic/hq/ui/rendit/servlet/RenditServlet.java trunk/thirdparty/lib/groovy-all-1.0.jar Modified: trunk/build.xml trunk/web/WEB-INF/web.xml Log: Add groovy, and the 'rendit' engine which does provides a simple way of plugin-based rendering of custom UI Modified: trunk/build.xml =================================================================== --- trunk/build.xml 2007-03-21 00:27:05 UTC (rev 3801) +++ trunk/build.xml 2007-03-21 19:22:00 UTC (rev 3802) @@ -72,6 +72,9 @@ <property name="installer.dir" value="${build.dir}/installer"/> + <property name="rendit_sys.dir" + value="${basedir}/src/org/hyperic/hq/ui/rendit/rendit_sys"/> + <path id="pdknative"> <fileset dir="${sigar.lib}" includes="*.jar" /> <fileset dir="${db2monitor.dir}" includes="lib/db2monitor.jar" /> @@ -382,6 +385,7 @@ <include name="json.jar" /> <include name="jug-asl-2.0.0.jar" /> <include name="commons-httpclient-3.0.1.jar" /> + <include name="groovy-all-1.0.jar" /> <!-- replacement j2ee compile required for instantj since our installer doesn't install tools.jar --> @@ -429,9 +433,20 @@ <copy tofile="${ear.dir}/lib/jdomb8.jar" file="${thirdparty.lib}/jdom_b8.jar" /> + <copy todir="${ear.dir}/rendit_sys"> + <fileset dir="${rendit_sys.dir}"/> + </copy> + <touch file="${ear.dir}/META-INF/application.xml" /> </target> + <target name="rendit-deploy" depends="init"> + <!-- Just a quick way to copy rendit_sys into JBoss --> + <copy todir="${jboss.home}/server/default/deploy/hq.ear/rendit_sys"> + <fileset dir="${rendit_sys.dir}"/> + </copy> + </target> + <target name="pack-ear" depends="pack-ear-files" unless="generate-manifest.notrequired"> <!-- generate EAR MANIFEST file --> <pathconvert targetos="unix" pathSep="," property="Class-Path"> Added: trunk/src/org/hyperic/hq/ui/rendit/InvocationBindings.java =================================================================== --- trunk/src/org/hyperic/hq/ui/rendit/InvocationBindings.java (rev 0) +++ trunk/src/org/hyperic/hq/ui/rendit/InvocationBindings.java 2007-03-21 19:22:00 UTC (rev 3802) @@ -0,0 +1,44 @@ +package org.hyperic.hq.ui.rendit; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * This class encapsulates the data which is passed from RenditServer into + * the groovy dispatcher. + */ +public class InvocationBindings { + private List _requestPath; + private File _pluginDir; + private HttpServletRequest _request; + private HttpServletResponse _response; + + InvocationBindings(List requestPath, File pluginDir, + HttpServletRequest request, HttpServletResponse response) + { + _requestPath = requestPath; + _pluginDir = pluginDir; + _request = request; + _response = response; + } + + public List getRequestPath() { + return Collections.unmodifiableList(_requestPath); + } + + public File getPluginDir() { + return _pluginDir; + } + + public HttpServletRequest getRequest() { + return _request; + } + + public HttpServletResponse getResponse() { + return _response; + } +} Added: trunk/src/org/hyperic/hq/ui/rendit/PluginWrapper.java =================================================================== --- trunk/src/org/hyperic/hq/ui/rendit/PluginWrapper.java (rev 0) +++ trunk/src/org/hyperic/hq/ui/rendit/PluginWrapper.java 2007-03-21 19:22:00 UTC (rev 3802) @@ -0,0 +1,56 @@ +package org.hyperic.hq.ui.rendit; + +import groovy.lang.Binding; +import groovy.util.GroovyScriptEngine; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; + +/** + * Basically a wrapper around the classloader and associated groovy + * artifacts. + */ +public class PluginWrapper { + private final File _pluginDir; + private final GroovyScriptEngine _engine; + + PluginWrapper(File pluginDir, File sysDir, ClassLoader parentLoader) { + URLClassLoader urlLoader = new URLClassLoader(new URL[0], + parentLoader); + URL[] u; + + _pluginDir = pluginDir; + + try { + u = new URL[] { + sysDir.toURL(), + _pluginDir.toURL(), + }; + } catch(MalformedURLException e) { + throw new RuntimeException(e); + } + _engine = new GroovyScriptEngine(u, urlLoader); + } + + File getPluginDir() { + return _pluginDir; + } + + Object run(String script, Binding b) throws Exception { + Thread curThread = Thread.currentThread(); + ClassLoader oldLoader = curThread.getContextClassLoader(); + + try { + curThread.setContextClassLoader(_engine.getParentClassLoader()); + return _engine.run(script, b); + } finally { + curThread.setContextClassLoader(oldLoader); + } + } + + public static boolean isValidPlugin(File f) { + return f.getName().startsWith("hqu_"); + } +} Added: trunk/src/org/hyperic/hq/ui/rendit/RenditServer.java =================================================================== --- trunk/src/org/hyperic/hq/ui/rendit/RenditServer.java (rev 0) +++ trunk/src/org/hyperic/hq/ui/rendit/RenditServer.java 2007-03-21 19:22:00 UTC (rev 3802) @@ -0,0 +1,104 @@ +package org.hyperic.hq.ui.rendit; + +import java.io.File; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import groovy.lang.Binding; + +public class RenditServer { + private static final RenditServer INSTANCE = new RenditServer(); + private static final Log _log = LogFactory.getLog(RenditServer.class); + private final Object CFG_LOCK = new Object(); + private Map _plugins = new HashMap(); + private File _sysDir; + + private RenditServer() {} + + private ClassLoader getUseLoader() { + return RenditServer.class.getClassLoader(); + } + + public File getSysDir() { + synchronized (CFG_LOCK) { + return _sysDir; + } + } + + /** + * Set the system directory which contains groovy support classes. + * Since this object is a singleton, we rely on someone to call this + * before they start adding plugins. + */ + public void setSysDir(File sysDir) { + synchronized (CFG_LOCK) { + Map oldPlugins = new HashMap(_plugins); + + _sysDir = sysDir; + // Re-create all the plugins with the new system directory + _plugins.clear(); + for (Iterator i=oldPlugins.entrySet().iterator(); i.hasNext(); ) { + Map.Entry ent = (Map.Entry)i.next(); + String pluginName = (String)ent.getKey(); + PluginWrapper plugin = (PluginWrapper)ent.getValue(); + PluginWrapper newWrapper; + + newWrapper = new PluginWrapper(plugin.getPluginDir(), _sysDir, + getUseLoader()); + + _plugins.put(pluginName, newWrapper); + } + } + } + + public void addPluginDir(String pluginName, File path) { + _log.info("Adding plugin [" + pluginName + "] as [" + path + "]"); + synchronized (CFG_LOCK) { + _plugins.put(pluginName, + new PluginWrapper(path, _sysDir, getUseLoader())); + + } + } + + public void removePluginDir(String pluginName) { + _log.info("Removing plugin [" + pluginName + "]"); + synchronized (CFG_LOCK) { + _plugins.remove(pluginName); + } + } + + public void handleRequest(List path, HttpServletRequest req, + HttpServletResponse resp) + throws Exception + { + String pluginName = (String)path.get(0); + PluginWrapper plugin; + + synchronized (CFG_LOCK) { + plugin = (PluginWrapper)_plugins.get(pluginName); + } + + if (plugin == null) { + throw new IllegalArgumentException("Unknown plugin [" + + pluginName + "]"); + } + + Binding b = new Binding(); + b.setVariable("invokeArgs", + new InvocationBindings(path, plugin.getPluginDir(), + req, resp)); + plugin.run("org/hyperic/hq/rendit/dispatcher.groovy", b); + } + + public static final RenditServer getInstance() { + return INSTANCE; + } +} Added: trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/org/hyperic/hq/rendit/BaseController.groovy =================================================================== --- trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/org/hyperic/hq/rendit/BaseController.groovy (rev 0) +++ trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/org/hyperic/hq/rendit/BaseController.groovy 2007-03-21 19:22:00 UTC (rev 3802) @@ -0,0 +1,59 @@ +package org.hyperic.hq.rendit + +import java.io.OutputStreamWriter + +import org.apache.commons.logging.Log +import org.apache.commons.logging.LogFactory + +import groovy.text.SimpleTemplateEngine +import java.io.File + +public abstract class BaseController + extends Expando +{ + Log log = LogFactory.getLog(this.getClass()) + String action + File pluginDir + def invokeArgs + + private void setAction(String action) { + this.action = action + } + + def setInvokeArgs(def args) { + this.invokeArgs = args + } + + def setPluginDir(File pluginDir) { + this.pluginDir = pluginDir + } + + /** + * Render a .gsp. + * + * This method takes a map of arguments. Valid arguments include: + * file: The file to render. If not specified, the name of the + * current action will be used + * args: A map of key/value pairs to send to the .gsp to use when + * rendering + */ + protected def render(args) { + def gspArgs = args.remove("args") + def gspFile = args.remove("file") + def useAction + + if (gspFile == null) + useAction = action + else + useAction = gspFile + + def targFile = new File(pluginDir, useAction + ".gsp") + targFile.withReader { reader -> + def eng = new SimpleTemplateEngine(false) + def template = eng.createTemplate(reader) + def outStream = invokeArgs.response.outputStream + def outWriter = new OutputStreamWriter(outStream) + template.make(gspArgs).writeTo(outWriter) + } + } +} Added: trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/org/hyperic/hq/rendit/dispatcher.groovy =================================================================== --- trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/org/hyperic/hq/rendit/dispatcher.groovy (rev 0) +++ trunk/src/org/hyperic/hq/ui/rendit/rendit_sys/org/hyperic/hq/rendit/dispatcher.groovy 2007-03-21 19:22:00 UTC (rev 3802) @@ -0,0 +1,63 @@ +package org.hyperic.hq.rendit + +import org.apache.commons.logging.Log +import org.apache.commons.logging.LogFactory + +/** + * The Dispatcher is the direct invocation target called from the HQ + * RenditServer. It has the responsibility of locating the controllers, + * setting up the environment, and invoking the request. + */ +public class Dispatcher { + private Log log = LogFactory.getLog(Dispatcher.class); + + private File pluginDir + private String controllerName + private String action + private def invokeArgs + + private String capitalize(String s) { + if (s.length() == 0) + return s; + return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + } + + def Dispatcher(def invokeArgs) { + def path = invokeArgs.requestPath + + if (path.size() < 3) { + throw new IllegalArgumentException("Path must have at least 3 " + + "components"); + } + + pluginDir = invokeArgs.pluginDir + controllerName = capitalize(path[1]) + "Controller" + action = path[2] + this.invokeArgs = invokeArgs + } + + def invoke() { + def controller + + try { + controller = Class.forName(controllerName, true, + this.class.classLoader).newInstance() + } catch(Exception e) { + throw new IllegalArgumentException("Unknown controller " + + "[$controller]") + } + + controller.setAction(action) + controller.setPluginDir(pluginDir) + controller.setInvokeArgs(invokeArgs) + + def runner = controller."$action" + if (runner == null) { + throw new IllegalArgumentException("Unknown action [$action]") + } + + runner(invokeArgs.request.parameterMap) + } +} + +new Dispatcher(invokeArgs).invoke() Added: trunk/src/org/hyperic/hq/ui/rendit/servlet/DirWatcher.java =================================================================== --- trunk/src/org/hyperic/hq/ui/rendit/servlet/DirWatcher.java (rev 0) +++ trunk/src/org/hyperic/hq/ui/rendit/servlet/DirWatcher.java 2007-03-21 19:22:00 UTC (rev 3802) @@ -0,0 +1,67 @@ +package org.hyperic.hq.ui.rendit.servlet; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Watches a directory for changes, issuing callbacks when things change + * + * XXX: JBoss has similar functionality that we should re-use + */ +class DirWatcher + implements Runnable +{ + private final Log _log = LogFactory.getLog(DirWatcher.class); + + private final File _dir; + private final DirWatcherCallback _cback; + + private final List _lastList = new ArrayList(); + + DirWatcher(File dir, DirWatcherCallback callback) { + _dir = dir; + _cback = callback; + } + + public void run() { + _log.info("Watching: " + _dir); + while (true) { + try { + List curList = Arrays.asList(_dir.listFiles()); + + for (Iterator i=curList.iterator(); i.hasNext(); ) { + File f = (File)i.next(); + + if (!_lastList.contains(f)) { + _cback.fileAdded(f); + } + } + + for (Iterator i=_lastList.iterator(); i.hasNext(); ) { + File f = (File)i.next(); + + if (!curList.contains(f)) { + _cback.fileRemoved(f); + } + } + + _lastList.clear(); + _lastList.addAll(curList); + Thread.sleep(1000); + } catch(Exception e) { + _log.warn("Error while processing directory listing", e); + } + } + } + + public interface DirWatcherCallback { + void fileAdded(File f); + void fileRemoved(File f); + } +} Added: trunk/src/org/hyperic/hq/ui/rendit/servlet/RenditServlet.java =================================================================== --- trunk/src/org/hyperic/hq/ui/rendit/servlet/RenditServlet.java (rev 0) +++ trunk/src/org/hyperic/hq/ui/rendit/servlet/RenditServlet.java 2007-03-21 19:22:00 UTC (rev 3802) @@ -0,0 +1,117 @@ +package org.hyperic.hq.ui.rendit.servlet; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hyperic.hq.ui.rendit.PluginWrapper; +import org.hyperic.hq.ui.rendit.RenditServer; +import org.hyperic.hq.ui.rendit.servlet.DirWatcher.DirWatcherCallback; +import org.hyperic.util.StringUtil; + +public class RenditServlet + extends HttpServlet +{ + private static final Log _log = LogFactory.getLog(RenditServlet.class); + private static final Object INIT_LOCK = new Object(); + private static boolean INITIALIZED; + + private static DirWatcher _watcher; + private static Thread _watcherThread; + + protected void doGet(HttpServletRequest req, HttpServletResponse response) + throws ServletException, IOException + { + String servPath = req.getServletPath(); + String reqUri = req.getRequestURI(); + + initPlugins(); + + if (!reqUri.startsWith(servPath)) { + _log.warn("Request path [" + reqUri + "] does not start with " + + "servlet [" + servPath + "]"); + return; + } + + // XXX: Make sure the following is sane -- needs to be escaped? + // any weird attacks? + reqUri = reqUri.substring(servPath.length()); + List path = StringUtil.explode(reqUri, "/"); + + if (path.size() != 3) { + throw new ServletException("Illegal request path"); + } + + _log.info("Request: " + req.getRequestURI() + "?" + + req.getQueryString()); + try { + RenditServer.getInstance().handleRequest(path, req, response); + } catch(Exception e) { + throw new ServletException(e); + } + } + + public void init() throws ServletException { + super.init(); + initPlugins(); + } + + private void initPlugins() { + synchronized(INIT_LOCK) { + if (INITIALIZED) + return; + + String home = System.getProperty("jboss.home.url"); + + if (home == null) { + _log.info("Can't find JBOSS Home"); + return; + } + + URL url; + try { + url = new URL(home); + } catch (MalformedURLException e) { + _log.error("Malformed jboss.home.url=" + home); + return; + } + + File homeDir = new File(url.getFile()); + + // XXX: Hardcoded sysdir for now + String sysPath = System.getProperty("jboss.server.home.dir") + + "/deploy/hq.ear/rendit_sys"; + File sysDir = new File(sysPath); + RenditServer.getInstance().setSysDir(sysDir); + + _watcher = new DirWatcher(homeDir, new DirWatcherCallback() { + public void fileAdded(File f) { + if (PluginWrapper.isValidPlugin(f)) { + RenditServer.getInstance().addPluginDir(f.getName(), f); + } + } + + public void fileRemoved(File f) { + if (PluginWrapper.isValidPlugin(f)) { + RenditServer.getInstance().removePluginDir(f.getName()); + } + } + }); + + _watcherThread = new Thread(_watcher); + _watcherThread.setDaemon(true); + _watcherThread.start(); + + INITIALIZED = true; + } + } +} Added: trunk/thirdparty/lib/groovy-all-1.0.jar =================================================================== (Binary files differ) Property changes on: trunk/thirdparty/lib/groovy-all-1.0.jar ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Modified: trunk/web/WEB-INF/web.xml =================================================================== --- trunk/web/WEB-INF/web.xml 2007-03-21 00:27:05 UTC (rev 3801) +++ trunk/web/WEB-INF/web.xml 2007-03-21 19:22:00 UTC (rev 3802) @@ -44,6 +44,10 @@ <filter-mapping> <filter-name>Authentication-Filter</filter-name> + <url-pattern>/rendit/*</url-pattern> + </filter-mapping> + <filter-mapping> + <filter-name>Authentication-Filter</filter-name> <url-pattern>*.do</url-pattern> </filter-mapping> <filter-mapping> @@ -53,7 +57,14 @@ <listener> <listener-class>org.hyperic.hq.ui.Configurator</listener-class> </listener> + <servlet> + <servlet-name>rendit</servlet-name> + <servlet-class>org.hyperic.hq.ui.rendit.servlet.RenditServlet</servlet-class> + <load-on-startup>2</load-on-startup> + </servlet> + + <servlet> <servlet-name>metricdata</servlet-name> <servlet-class>org.hyperic.hq.ui.servlet.MetricDataServlet</servlet-class> <load-on-startup>2</load-on-startup> @@ -158,6 +169,10 @@ <!-- end cactus redirector mapping --> <servlet-mapping> + <servlet-name>rendit</servlet-name> + <url-pattern>/rendit/*</url-pattern> + </servlet-mapping> + <servlet-mapping> <servlet-name>metricdata</servlet-name> <url-pattern>/resource/MetricData</url-pattern> </servlet-mapping> |