Child pages
  • Advanced plugin sample
Skip to end of metadata
Go to start of metadata

Code Mostly Present, Text Not Yet Finished

Here's a sample plugin setup demonstrating several advanced features. It is based on the Quartz scheduler. The plugin is separated into 3 components:

  • The Quartz scheduler integration
  • Deployment integration so you can deploy a JAR with Quartz job classes and execution schedules, and start and stop jobs with the Geronimo deployment tools
  • Console integration so you can use a Quartz management screen in the console to edit, schedule, run, pause, and resume Quartz jobs.

Note that once the Quartz plugin is deployed, any J2EE application can get a JNDI reference to the Quartz scheduler, making it easy for applications to work with.

Before you try this process, you should be familiar with basic GBeans. There's a much more straightforward Quartz article available at http://www-128.ibm.com/developerworks/opensource/library/os-ag-thirdparty/ if you need some background (though that covers Geronimo 1.0 syntax for the XML files, which is slightly different).

For more on Quartz, see

Quartz Integration

The goal of the basic Quartz integration is to:

  • Wrap a Quartz scheduler in a GBean, so it's started and stopped with its Geronimo module
  • Make Quartz use a Geronimo thread pool instead of its own
  • Wrap Quartz jobs as GBeans so a job can be represented as a Geronimo component

The next sections will talk about deploying and managing Quartz jobs, and managing the Quartz scheduler and jobs through the Geronimo console.

As described here, this does not expose the full power of Quartz. This package does not let you configure database persistence, or deploy jobs on schedules other than Cron schedules, etc. This may or may not be sufficient for your needs, but it's certainly enough for this example.

The steps described here are:

  1. Create a scheduler GBean (and managment interface)
  2. Create a job GBean (and management interface)
  3. Create some glue between a Geronimo thread pool and a Quartz thread pool
  4. Create a Geronimo deployment plan for the Quartz integration module
  5. Create the plugin metadata for the Quartz plugin

Quartz Scheduler GBean

Here's a sample Scheduler GBean:

javasolid

Here are some things to note:

  • The GBean has a reference to a Geronimo ThreadPool, and during doStart, it overwrites the default Quartz thread pool settings with this. The mechanism is a little weird since Quartz expects to create a new thread pool instead of accessing an existing one in the server environment, so we have to stuff the Geronimo thread pool into a map and the pass the index of the map entry to the new thread pool class that Quartz instantiates, whereupon it can look up the original pool again.
  • The GBean takes a kernel reference, which is used to look up Job GBeans on the fly (in the getJob method)
  • All the methods deal only with jobs in the "Geronimo Quartz" group – we don't try to manage other jobs that may have been deployed to Quartz without using this GBean plumbing.
  • Again, this GBean only exposes a small fraction of the methods available in the Quartz scheduler, but it's enough for our purposes

The management interface for the Scheduler GBean looks like this. This interface will be used by callers to interact with the GBean (and it's what an application will get if the app maps the QuartzScheduler into its JNDI space). While an interface isn't required, it's definitely recommended.

javasolid

Quartz Job GBean

javasolid javasolid

Quartz/Geronimo Thread Pool

javasolid

Geronimo Deployment Plan

If you compile the previous classes and put them in a JAR, you can create the following deployment plan and either keep it outside the JAR or save it to META-INF/geronimo-service.xml in the JAR. For example, the JAR might look like this:

solid

The deployment plan is:

xmlsolid gplugins quartz 0.1 car geronimo rmi-naming car opensymphony quartz jar DefaultThreadPool ]]>

Note that in order to deploy this, you must have Quartz in your Geronimo repository (e.g. repository/opensymphony/quartz/1.5.2/quartz-1.5.2.jar). If you install the Quartz integration as a plugin, this will be installed for you automatically.

Quartz Plugin Metadata

TODO: need to write this

javasolid

Quartz Deployment Integration

Quartz Job Deployment Plan Format

xmlsolid Schema for a Quartz scheduled job. A list of jobs to schedule with Quartz A job to schedule with Quartz A unique name used to identify the Quartz job The fully-qualified class name of the Quartz job A CRON-formatted expression for when the job should run ]]>

Sample:

xmlsolid TestJob Test 1 org.gplugins.quartz.jobs.TestJob 0/15 * * * * ? ]]>

We use Maven 2 and XMLBeans to code-generate classes corresponding to this Schema, using a Maven POM like this. (Note the extra dependency because the schema above imports the Geronimo schema, whose classes are in the geronimo-service-builder module.)

xmlsolid 4.0.0 gplugins quartz-deployer 0.1 Geronimo Quartz Deployer org.codehaus.mojo xmlbeans-maven-plugin 2.0 xmlbeans true geronimo-quartz-0.1.xsd src/schema src/schema/xmlconfig.xml src/java src/test src/resources true src/test-resources gplugins quartz 0.1 xmlbeans xbean 2.0.0 stax stax-api 1.0 org.apache.geronimo.modules geronimo-kernel 1.1-SNAPSHOT org.apache.geronimo.modules geronimo-deployment 1.1-SNAPSHOT org.apache.geronimo.modules geronimo-service-builder 1.1-SNAPSHOT org.apache.geronimo.modules geronimo-common 1.1-SNAPSHOT opensymphony quartz 1.5.2 test junit junit 3.8.1 test ]]>

Quartz Job Deployer

The deployer has a couple main responsibilites:

  • Check whether this deployer can deploy the requested module
  • Confirm that the deployment plan is valid
  • Generate a Module ID for the deployment
  • Create GBeans that will be the "deployed" form of the module

Generally, this can be reduced to "input XML, output either null or Module ID plus GBeans".

Here's the deployer:

javasolid -1) { name = name.substring(0, pos); } moduleDoc.getJobs().getEnvironment().getModuleId().setArtifactId(name); } return moduleDoc.getJobs(); } catch (XmlException e) { throw new DeploymentException("Could not parse xml in plan", e); } catch (IOException e) { throw new DeploymentException("no plan at " + planFile, e); } } public Artifact getConfigurationID(Object plan, JarFile module, ModuleIDBuilder idBuilder) throws IOException, DeploymentException { JobsType configType = (JobsType) plan; EnvironmentType environmentType = configType.getEnvironment(); Environment environment = EnvironmentBuilder.buildEnvironment(environmentType, defaultEnvironment); idBuilder.resolve(environment, module == null ? "" : new File(module.getName()).getName(), "car"); if(!environment.getConfigId().isResolved()) { throw new IllegalStateException("Module ID is not fully populated ("+environment.getConfigId()+")"); } return environment.getConfigId(); } public DeploymentContext buildConfiguration(boolean inPlaceDeployment, Artifact configId, Object plan, JarFile jar, Collection configurationStores, ArtifactResolver artifactResolver, ConfigurationStore targetConfigurationStore) throws IOException, DeploymentException { JobsType configType = (JobsType) plan; return buildConfiguration(inPlaceDeployment, configId, configType, jar, configurationStores, artifactResolver, targetConfigurationStore); } public DeploymentContext buildConfiguration(boolean inPlaceDeployment, Artifact configId, JobsType moduleType, JarFile jar, Collection configurationStores, ArtifactResolver artifactResolver, ConfigurationStore targetConfigurationStore) throws DeploymentException, IOException { ArtifactType type = moduleType.getEnvironment().isSetModuleId() ? moduleType.getEnvironment().getModuleId() : moduleType.getEnvironment().addNewModuleId(); type.setArtifactId(configId.getArtifactId()); type.setGroupId(configId.getGroupId()); type.setType(configId.getType()); type.setVersion(configId.getVersion().toString()); Environment environment = EnvironmentBuilder.buildEnvironment(moduleType.getEnvironment(), defaultEnvironment); if(!environment.getConfigId().isResolved()) { throw new IllegalStateException("Module ID should be fully resolved by now (not "+environment.getConfigId()+")"); } File outfile; try { outfile = targetConfigurationStore.createNewConfigurationDir(configId); } catch (ConfigurationAlreadyExistsException e) { throw new DeploymentException(e); } ConfigurationManager configurationManager = this.configurationManager; if (configurationManager == null) { configurationManager = new SimpleConfigurationManager(configurationStores, artifactResolver, repositories); } DeploymentContext context = new DeploymentContext(outfile, inPlaceDeployment && null != jar ? DeploymentUtil.toFile(jar) : null, environment, ConfigurationModuleType.SERVICE, naming, configurationManager, repositories ); if(jar != null) { File file = new File(jar.getName()); context.addIncludeAsPackedJar(URI.create(file.getName()), jar); } try { ClassLoader cl = context.getClassLoader(); AbstractName moduleName = naming.createRootName(configId, configId.toString(), "ServiceModule"); addJobs(moduleType.getJobArray(), cl, moduleName, context); return context; } catch (RuntimeException t) { context.close(); throw t; } catch (Error e) { context.close(); throw e; } } private void addJobs(JobType[] jobs, ClassLoader cl, AbstractName moduleName, DeploymentContext context) throws DeploymentException { for (int i = 0; i < jobs.length; i++) { JobType job = jobs[i]; GBeanInfo gBeanInfo = GBeanInfo.getGBeanInfo(QuartzJobGBean.class.getName(), cl); String namePart = job.getJobName(); AbstractName abstractName = context.getNaming().createChildName(moduleName, namePart, "GBean"); GBeanData data = new GBeanData(abstractName, gBeanInfo); data.setReferencePattern("QuartzScheduler", schedulerName); data.setAttribute("name", job.getJobName()); data.setAttribute("jobClass", job.getJobClass()); data.setAttribute("cronExpression", job.getCronExpression()); try { context.addGBean(data); } catch (GBeanAlreadyExistsException e) { throw new DeploymentException("Cannot add Quartz job '"+job.getJobName()+"' for module "+moduleName+"; a job with that name already exists."); } } } public static final GBeanInfo GBEAN_INFO; static { GBeanInfoBuilder infoFactory = GBeanInfoBuilder.createStatic("Quartz Deployer", QuartzJobDeployer.class, "ConfigBuilder"); infoFactory.addInterface(ConfigurationBuilder.class); infoFactory.addAttribute("defaultEnvironment", Environment.class, true); infoFactory.addAttribute("schedulerName", AbstractNameQuery.class, true, true); infoFactory.addAttribute("kernel", Kernel.class, false, false); infoFactory.addReference("Repository", Repository.class, "Repository"); infoFactory.setConstructor(new String[]{"defaultEnvironment", "Repository", "schedulerName", "kernel"}); GBEAN_INFO = infoFactory.getBeanInfo(); } public static GBeanInfo getGBeanInfo() { return GBEAN_INFO; } } ]]>

One thing to notice is how the Quartz Job Deployment Plan is connected to the Quartz Job Deployer. It is based on the schema namespace (http://geronimo.apache.org/xml/ns/plugins/quartz-0.1), and the fact that the plan was in the right place for us to find to begin with. For all the deployers we've done so far, we use XMLBeans to create JavaBeans connected to the schema. Then we use XMLBeans to read in the deployment plan, and check whether it's the type this deployer expects. Here's an excerpt from the deployer above:

javasolid

This part establishes that we can load a plan at all. If not, it either means no plan was provided, or the plan is in the module at a different location (e.g. WEB-INF/geronimo-web.xml, meaning it's definitely not a Quartz job). Either way, this deployer can't handle the archive so we return null.

If we get past that, it means that we found a plan. So we go on to check the type:

javasolid

The constant JOBS_QNAME is a reference to the schema namespace of the first element in the file. If it's the one we're looking for, great. Otherwise, even though we found a plan, it was not the right type of plan (e.g. someone passed a web plan on the command line), so this deployer can't handle it.

If we get past those two checks (plan present and plan has correct namespace) then we assume that it really was meant for this deployer to handle, and for other kinds of errors (syntax error in plan, etc.) we throw a deployment exception. Some of the deployers have additional logic to silently upgrade old-format plans to current-format plans, but this one does not.

Geronimo Plan for Quartz Deployer

This can be packaged into a JAR with the deployer code like this:

solid

The plan is:

xmlsolid gplugins quartz-builder 0.1 car gplugins quartz 0.1 car geronimo geronimo-gbean-deployer car ?name=QuartzScheduler geronimo rmi-naming car gplugins quartz car ]]>

Plugin Metadata for Quartz Deployer

TODO: need to write this

javasolid

Quartz Console Integration

Quartz Console Portlet

javasolid

Note that this portlet can access the QuartzScheduler in JNDI at java:comp/env/Scheduler by adding the following reference to geronimo-web.xml:

xmlsolid Scheduler org.gplugins.quartz.QuartzScheduler QuartzScheduler ]]>

That assumes that the Quartz package is listed as a dependency higher up in geronimo-web.xml:

xmlsolid gplugins quartz 0.1 car ]]>

Console Update GBean

The secret sauce here is a GBean that rewrites the console config files. After that, you just have to hit http://localhost:8080/console/portal/welcome?hotDeploy=true (note that last bit) to force the console to reread its configuration files. I'm sure there's a better way, but hey.

This GBean can be reused to install any portlets into the console (though it's configured to add one new page with any/all the portlets on that single page).

javasolid -1) { framework = test; break; } } if (framework == null) { throw new IllegalStateException("Cannot locate framework web app within console EAR at " + file.getAbsolutePath()); } File dataDir = new File(new File(framework, "WEB-INF"), "data"); if (!dataDir.exists() || !dataDir.isDirectory() || !dataDir.canRead()) { throw new IllegalStateException("Unable to read console data dir at " + dataDir.getAbsolutePath()); } // Update the console portal config files File registryFile = new File(dataDir, "portletentityregistry.xml"); File contextFile = new File(dataDir, "portletcontexts.txt"); boolean changed = addContext(contextFile, "/"+webApp); if(changed) { Integer newPortletStartID = processRegistry(registryFile); File pageFile = new File(dataDir, "pageregistry.xml"); processPortlets(pageFile, registryApp, newPortletStartID.intValue()); FileOutputStream out = new FileOutputStream(new File(registryFile.getParentFile(), registryFile.getName()+".new")); TransformerFactory xfactory = TransformerFactory.newInstance(); Transformer xform = xfactory.newTransformer(); xform.setOutputProperty(OutputKeys.INDENT, "yes"); xform.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); xform.transform(new DOMSource(registryApp.getOwnerDocument()), new StreamResult(out)); out.flush(); out.close(); // Save off the old files, and replace them with the new ones registryFile.renameTo(new File(dataDir, registryFile.getName()+".old")); contextFile.renameTo(new File(dataDir, contextFile.getName()+".old")); pageFile.renameTo(new File(dataDir, pageFile.getName()+".old")); new File(dataDir, registryFile.getName()+".new").renameTo(registryFile); new File(dataDir, contextFile.getName()+".new").renameTo(contextFile); new File(dataDir, pageFile.getName()+".new").renameTo(pageFile); } kernel.setAttribute(abstractName, "installed", Boolean.TRUE); } private void processPortlets(File pageFile, Element registryApp, int newPortletStartID) throws ParserConfigurationException, IOException, SAXException, TransformerException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); FileInputStream in = new FileInputStream(pageFile); Document doc = builder.parse(in); in.close(); Element root = doc.getDocumentElement(); NodeList cats = root.getChildNodes(); for (int i = 0; i < cats.getLength(); i++) { Node node = cats.item(i); if(node.getNodeType() != Node.ELEMENT_NODE) { continue; } Element category = (Element) node; String title = category.getAttribute("name"); if(title.equals("plugins")) { NodeList pages = category.getChildNodes(); for (int j = 0; j < pages.getLength(); j++) { Node pageNode = pages.item(j); if(pageNode.getNodeType() != Node.ELEMENT_NODE) { continue; } Element page = (Element) pageNode; if(!page.getNodeName().equals("fragment")) { continue; } title = page.getAttribute("name"); portlets.remove(title); } if(portlets.size() > 0) { Element page = doc.createElement("fragment"); page.setAttribute("name", this.title); page.setAttribute("type", "page"); category.appendChild(page); Element nav = doc.createElement("navigation"); page.appendChild(nav); Element navTitle = doc.createElement("title"); nav.appendChild(navTitle); navTitle.appendChild(doc.createTextNode(this.title)); Element desc = doc.createElement("description"); nav.appendChild(desc); desc.appendChild(doc.createTextNode("ico_list_16x16.gif Configure this plugin")); for (int j = 0; j < portlets.size(); j++) { String name = (String) portlets.get(j); int id = newPortletStartID+j; // Write into registry Element regPortlet = registryApp.getOwnerDocument().createElement("portlet"); registryApp.appendChild(regPortlet); regPortlet.setAttribute("id", Integer.toString(id)); Element def = regPortlet.getOwnerDocument().createElement("definition-id"); regPortlet.appendChild(def); def.appendChild(def.getOwnerDocument().createTextNode(webApp+"."+name)); // Write into page Element row = doc.createElement("fragment"); page.appendChild(row); row.setAttribute("name", "row"+j); row.setAttribute("type", "row"); Element col = doc.createElement("fragment"); row.appendChild(col); col.setAttribute("name", "col1"); col.setAttribute("type", "column"); Element port = doc.createElement("fragment"); col.appendChild(port); port.setAttribute("name", "p1"); port.setAttribute("type", "portlet"); Element prop = doc.createElement("property"); port.appendChild(prop); prop.setAttribute("name", "portlet"); prop.setAttribute("value", "9."+id); } } } } FileOutputStream out = new FileOutputStream(new File(pageFile.getParentFile(), pageFile.getName()+".new")); TransformerFactory xfactory = TransformerFactory.newInstance(); Transformer xform = xfactory.newTransformer(); xform.setOutputProperty(OutputKeys.INDENT, "yes"); xform.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); xform.transform(new DOMSource(doc), new StreamResult(out)); out.flush(); out.close(); } private boolean addContext(File contextFile, String context) { try { List list = new ArrayList(); BufferedReader in = new BufferedReader(new FileReader(contextFile)); String line; while((line = in.readLine()) != null) { line = line.trim(); if(line.equals(context)) { in.close(); return false; } else if(!line.equals("")) { list.add(line); } } in.close(); PrintWriter out = new PrintWriter(new FileWriter(new File(contextFile.getParentFile(), contextFile.getName()+".new"))); for (int i = 0; i < list.size(); i++) { line = (String) list.get(i); out.println(line); } out.println(context); out.close(); return true; } catch (IOException e) { throw (IllegalStateException)new IllegalStateException("Unable to process portlet contexts").initCause(e); } } private Integer processRegistry(File file) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); FileInputStream in = new FileInputStream(file); Document doc = builder.parse(in); in.close(); Element root = doc.getDocumentElement(); NodeList apps = root.getElementsByTagName("application"); for (int i = 0; i < apps.getLength(); i++) { Element app = (Element) apps.item(i); if (app.getAttribute("id").equals("9")) { registryApp = app; NodeList portlets = app.getElementsByTagName("portlet"); int max = 0; for (int j = 0; j < portlets.getLength(); j++) { Element portlet = (Element) portlets.item(j); String id = portlet.getAttribute("id"); int value = Integer.parseInt(id); if (value > max) { max = value; } } return new Integer(max + 1); } } Element app = doc.createElement("application"); app.setAttribute("id", "9"); root.appendChild(app); Element def = doc.createElement("definition-id"); app.appendChild(def); def.appendChild(doc.createTextNode(webApp)); registryApp = app; return new Integer(1); } catch (Exception e) { throw (IllegalStateException)new IllegalStateException("Unable to process portlet registry").initCause(e); } } public void doStop() throws Exception { } public void doFail() { } public static final GBeanInfo GBEAN_INFO; static { GBeanInfoBuilder infoFactory = GBeanInfoBuilder.createStatic("Console Portlet Installer", AddToConsoleGBean.class, "GBean"); infoFactory.addAttribute("installed", boolean.class, true, true); infoFactory.addAttribute("title", String.class, true, true); infoFactory.addAttribute("webApp", String.class, true, true); infoFactory.addAttribute("portlets", List.class, true, true); infoFactory.addAttribute("kernel", Kernel.class, false, false); infoFactory.addAttribute("abstractName", AbstractName.class, false, false); infoFactory.setConstructor(new String[]{"webApp", "title", "portlets", "kernel", "abstractName"}); GBEAN_INFO = infoFactory.getBeanInfo(); } public static GBeanInfo getGBeanInfo() { return GBEAN_INFO; } } ]]>

The deployment plan block that configures this GBean is added to geronimo-web.xml for the web app, and looks like this:

xmlsolid Quartz quartz-console Quartz ]]>

The "title" is the name of the entry for the new page in the console, the "webApp" is the context root of the web application containing the portlets, and the "portlets" is a list of portlets by the name they declare in portlet.xml. Note that if there were multiple portlets, the portlets property would use a comma-separated list (but there's still only one page/title and web app).

Quartz Console Packaging

This ends up going into a WAR like this:

solid

Quartz Console Plugin Metadata

TODO: need to write this

javasolid
  • No labels