/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.services.deployment; import java.io.BufferedWriter; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringBufferInputStream; import java.io.StringWriter; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import javax.management.ObjectName; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; import org.jboss.bootstrap.spi.ServerConfig; import org.jboss.deployment.DeploymentInfo; import org.jboss.logging.Logger; import org.jboss.mx.util.MBeanServerLocator; import org.jboss.services.deployment.metadata.ConfigInfo; import org.jboss.services.deployment.metadata.ConfigInfoBinding; import org.jboss.services.deployment.metadata.PropertyInfo; import org.jboss.services.deployment.metadata.TemplateInfo; import org.jboss.system.server.ServerConfigLocator; import org.jboss.util.file.Files; import org.jboss.varia.deployment.convertor.XslTransformer; import org.jboss.xb.binding.ObjectModelFactory; import org.jboss.xb.binding.Unmarshaller; import org.jboss.xb.binding.UnmarshallerFactory; /** * Class handling JBoss module generation. Uses apache velocity * for generating deployment descriptors. * * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a> * @author <a href="mailto:peter.johnson2@unisys.com">Peter Johnson</a> * * @version $Revision: 81038 $ */ public class DeploymentManager { // Constants ----------------------------------------------------- /** the filename to look for in template subdirectories */ public static final String TEMPLATE_CONFIG_FILE = "template-config.xml"; /** an object to pass back from the template to trigger an error */ public static final String TEMPLATE_ERROR_PARAM = "template-error"; /** a helper object to pass in to the template */ public static final String CONTEXT_HELPER = "helper"; /** jndi name of main deployer, used to locate configuration files for mbeans */ private static final String MAIN_DEPLOYER_OBJECT_NAME = "jboss.system:service=MainDeployer"; /** name of operation used to locate configuration files for mbeans */ private static final String MAIN_DEPLOYER_LIST_OPERATION_NAME = "listDeployed"; // Private Data -------------------------------------------------- /** Logger */ private Logger log; /** directory to hold the template subdirectories */ private File templateDir; /** the directory to output generated modules */ private File undeployDir; /** the directory to move modules for deployment */ private File deployDir; /** config name string -> ConfigInfo */ private Map configMap; /** the apache velocity engine */ VelocityEngine ve; /** * @param templateDir the root dir where templates are stored * @param packageDir the directory to store generated packages */ public DeploymentManager(String templateDir, String undeployDir, String deployDir, Logger log) throws Exception { this.log = log; // do the actuall initialization initialize(templateDir, undeployDir, deployDir); } // Public Interface ---------------------------------------------- /** * Return the list of available templates */ public Set listModuleTemplates() { Set keys = configMap.keySet(); synchronized(configMap) { // return a new sorted copy return new TreeSet(keys); } } /** * Get property metadata information for a particular template * * @param template * @return list with PropertyInfo objects associated with the template * @throws Exception if the template does not exist */ public List getTemplatePropertyInfo(String template) throws Exception { ConfigInfo ci = (ConfigInfo)configMap.get(template); if (ci == null) { throw new Exception("template does not exist: " + template); } else { // return a copy List propertyList = ci.getPropertyInfoList(); List newList = new ArrayList(propertyList.size()); for (Iterator i = propertyList.iterator(); i.hasNext();) { newList.add(new PropertyInfo((PropertyInfo)i.next())); } return newList; } } public String createModule(String module, String template, HashMap properties) throws Exception { if (module == null || template == null || properties == null) throw new Exception("Null argument: module=" + module + ", template=" + template + ", properties=" + properties); // make sure proposed module name is filesystem friendly if (!module.equals(Files.encodeFileName(module))) throw new Exception("not a filesystem friendly module name: " + module); log.info("createModule(module=" + module + ", template=" + template + ", properties=" + properties + ")"); ConfigInfo ci = (ConfigInfo)configMap.get(template); if (ci == null) throw new Exception("template does not exist: " + template); // get optional package extension (e.g. .sar) // and enforce it on the output package (file or directory) File outputModule; String extension = ci.getExtension(); if (extension == null || module.endsWith(extension)) outputModule = new File(this.undeployDir, module); else outputModule = new File(this.undeployDir, module + extension); // check if module already exists in output dir if (outputModule.exists()) throw new Exception("module already exist: " + outputModule); String vmTemplate = ci.getTemplate(); // make sure we clean-up in case something goes wrong try { // simple case - single descriptor package (e.g. xxx-service.xml) if (vmTemplate != null ) { VelocityContext ctx = createTemplateContext(ci, properties); BufferedWriter out = new BufferedWriter(new FileWriter(outputModule)); try { boolean success = ve.mergeTemplate(template + '/' + vmTemplate, ctx, out); if (success == true) { String errorMsg = (String)ctx.get(TEMPLATE_ERROR_PARAM); if (errorMsg.length() > 0) throw new Exception("Template error: " + errorMsg); else log.debug("created module '" + outputModule.getName() + "' based on template '" + template + "'"); } else throw new Exception("Failed to create module '" + outputModule.getName()); } finally { out.close(); } } else { // complex case - many descriptors and possibly files to copy // now output will be a directory instead of a plain descriptor (e.g. xxx.sar) VelocityContext ctx = createTemplateContext(ci, properties); // deep copy files if copydir specified String copydir = ci.getCopydir(); File sourceDir = new File(this.templateDir, template + '/' + copydir); deepCopy(sourceDir, outputModule); // go through all declared templates List templateList = ci.getTemplateInfoList(); for (Iterator i = templateList.iterator(); i.hasNext(); ) { TemplateInfo ti = (TemplateInfo)i.next(); File outputFile = new File(outputModule, ti.getOutput()); File outputPath = outputFile.getParentFile(); if (!outputPath.exists()) if (!outputPath.mkdirs()) throw new IOException("cannot create directory: " + outputPath); BufferedWriter out = new BufferedWriter(new FileWriter(outputFile)); try { boolean success = ve.mergeTemplate(template + '/' + ti.getInput(), ctx, out); if (success == true) { String errorMsg = (String)ctx.get(TEMPLATE_ERROR_PARAM); if (errorMsg.length() > 0) throw new Exception("Template error: " + errorMsg); else log.debug("created module '" + outputModule.getName() + "' based on template '" + template + "'"); } else throw new Exception("Failed to create package '" + outputModule.getName()); } finally { out.close(); } } } } catch (Exception e) { if (outputModule.exists()) { boolean deleted = Files.delete(outputModule); if (!deleted) log.warn("Failed to clean-up erroneous module: " + outputModule); } throw e; } return outputModule.getName(); } /** * Remove a module if exists * * @param module the module to remove * @return true if removed, false if module does not exist or an error occurs */ public boolean removeModule(String module) { File target = new File(this.undeployDir, module); return Files.delete(target); } /** * @see org.jboss.services.deployment.DeploymentServiceMBean#updateMBean */ public boolean updateMBean(MBeanData data) throws Exception { // Verify necessary parameters passed. if (data == null) throw new Exception("Null argument: data=" + data); if (data.getName() == null || data.getTemplateName() == null || data.getName().length() == 0 || data.getTemplateName().length() == 0 ) throw new Exception("Null required data: name=" + data.getName() + ", templateName=" + data.getTemplateName()); boolean result = true; // Log the data passed. if (log.isDebugEnabled()) { log.debug("updateMBean(" + data + ")"); log.debug(" template=" + data.getTemplateName()); log.debug(" depends=" + data.getDepends()); log.debug(" attributes=" + data.getAttributes()); log.debug(" xpath=" + data.getXpath()); } // Get the template to use, and ensure we have it: String template = data.getTemplateName(); ConfigInfo ci = (ConfigInfo)configMap.get(template); if (ci == null) throw new Exception("template does not exist: " + template); // Determine which configuration XML file holds the data for the // specified MBean: String configPath = configPathFor(data.getName()); log.debug("configPath=" + configPath); if (configPath == null) throw new Exception("No configuration file found for mbean " + data); HashMap map = new HashMap(); map.put("mbean", data); updateConfigFile(data.getName(), template, map, ci, configPath); return result; } /** * @see org.jboss.services.deployment.DeploymentServiceMBean#updateDataSource */ public String updateDataSource(String module, String template, HashMap properties) throws Exception { if (module == null || template == null || properties == null) throw new Exception("Null argument: module=" + module + ", template=" + template + ", properties=" + properties); log.info("updateDataSource(module=" + module + ", template=" + template + ", properties=" + properties + ")"); // Append the suffix '-update' to the template if necessary if (!template.endsWith("-update")) template += "-update"; module = processDataSourceChanges(module, template, properties); return module; } /** * @see org.jboss.services.deployment.DeploymentServiceMBean#removeDataSource */ public String removeDataSource(String module, String template, HashMap properties) throws Exception { if (module == null || template == null || properties == null) throw new Exception("Null argument: module=" + module + ", template=" + template + ", properties=" + properties); log.info("removeDataSource(module=" + module + ", template=" + template + ", properties=" + properties + ")"); // Append the suffix '-remove' to the template if necessary if (!template.endsWith("-remove")) template += "-remove"; module = processDataSourceChanges(module, template, properties); return module; } /** * Search for the configuration file that contains the specified MBean. * * @param name The MBean name, or a pattern that can be used to match an * mbean name. * @return The full path name for the configuration file. */ public String configPathFor(String name) throws Exception { if (log.isDebugEnabled()) log.debug("configPathFor(" + name + ")"); String result = null; // Get the collection of mbean deployment info objects: Collection deploymentColl = (Collection) MBeanServerLocator.locateJBoss().invoke( new ObjectName(MAIN_DEPLOYER_OBJECT_NAME), MAIN_DEPLOYER_LIST_OPERATION_NAME, null, null); // Iterate through that collection, looking for the one that contains an // mbean whose name matches the name (or pattern) provided: ObjectName pattern = new ObjectName(name); outer: for (Iterator iter = deploymentColl.iterator(); iter.hasNext();) { DeploymentInfo deploymentInfo = (DeploymentInfo) iter.next(); List associatedMBeanObjectNames = deploymentInfo.mbeans; for (Iterator iterator = associatedMBeanObjectNames.iterator(); iterator.hasNext();) { ObjectName beanName = (ObjectName) iterator.next(); if (log.isDebugEnabled()) log.debug("beanName=" + beanName); if (pattern.apply(beanName)) { result = deploymentInfo.watch.getFile(); break outer; } } } if (log.isDebugEnabled()) log.debug("configPathFor()=" + result); return result; } public void moveToDeployDir(String module) throws Exception { File source = new File(this.undeployDir, module); File target = new File(this.deployDir, module); if (source.exists()) { boolean moved = source.renameTo(target); if (!moved) throw new Exception("cannot move module: " + module); } else throw new Exception("module does not exist: " + module); } public void moveToModuleDir(String module) throws Exception { File source = new File(this.deployDir, module); File target = new File(this.undeployDir, module); if (source.exists()) { boolean moved = source.renameTo(target); if (!moved) throw new Exception("cannot move module: " + module); } else throw new Exception("module does not exist: " + module); } public URL getDeployedURL(String module) throws Exception { File target = new File(this.deployDir, module); if (!target.exists()) throw new Exception("module does not exist: " + target); return target.toURL(); } public URL getUndeployedURL(String module) throws Exception { File target = new File(this.undeployDir, module); if (!target.exists()) throw new Exception("module does not exist: " + target); return target.toURL(); } // Private Methods ----------------------------------------------- /** * Performs the actual initialization */ private void initialize(String templateDir, String undeployDir, String deployDir) throws Exception { log.debug("DeploymentManager.initialize()"); // Find out template dir this.templateDir = initDir(templateDir, false); log.debug("template dir=" + this.templateDir); // Initialize output dir this.undeployDir = initDir(undeployDir, true); log.debug("undeployDir dir=" + this.undeployDir); this.deployDir = initDir(deployDir, false); log.debug("deploy dir=" + this.deployDir); // Discover all template config files List configFiles = findTemplateConfigFiles(this.templateDir); log.debug("template config files=" + configFiles); Map map = Collections.synchronizedMap(new TreeMap()); // Parse each template config file and store metadata in configMap for (Iterator i = configFiles.iterator(); i.hasNext(); ) { File file = (File)i.next(); ConfigInfo ci = parseXMLconfig(file); // derive template name from subdirectory name ci.setName(file.getParentFile().getName()); if (log.isTraceEnabled()) log.trace("file: " + file + " ConfigInfo: " + ci); Object existingValue = map.put(ci.getName(), ci); // make sure not two configuration templates with the same name if (existingValue != null) throw new Exception("Duplicate template configuration entry: " + ci.getName()); } this.configMap = map; // Initialise velocity engine this.ve = new VelocityEngine(); this.ve.setProperty("runtime.log.logsystem.class", "org.apache.velocity.runtime.log.SimpleLog4JLogSystem"); this.ve.setProperty("runtime.log.logsystem.log4j.category", log.getName() + ".VelocityEngine"); this.ve.setProperty("file.resource.loader.path", this.templateDir.getCanonicalPath()); this.ve.setProperty("stringliterals.interpolate", "false"); this.ve.init(); } /** * Check if directory exists as an absolute path, * otherwise, try to find it under the jboss server * directories (and optionally create it, if the * create flag has been set) */ private File initDir(String targetDir, boolean create) throws Exception { File dir = null; // Check if this is an existing absolute path try { URL fileURL = new URL(targetDir); File file = new File(fileURL.getFile()); if(file.isDirectory() && file.canRead() && file.canWrite()) { dir = file; } } catch(Exception e) { // Otherwise, try to see inside the jboss directory hierarchy File homeDir = ServerConfigLocator.locate().getServerHomeDir(); dir = new File(homeDir, targetDir); if (create == true) dir.mkdirs(); if (!dir.isDirectory()) throw new Exception("The target directory is not valid: " + dir.getCanonicalPath()); } return dir; } /** * Find all files named TEMPLATE_CONFIG_FILE * one level below basedir, i.e. * * basedir/YYY/template-config.xml * ... * * @param basedir * @return */ private List findTemplateConfigFiles(File basedir) { // return val List files = new ArrayList(); // anonymous class FileFilter dirFilter = new FileFilter() { public boolean accept(File file) { return file.isDirectory() && !file.getName().startsWith("."); } }; // return all dirs not starting with "." File[] dirs = basedir.listFiles(dirFilter); for (int i = 0; i < dirs.length; i++) { File file = new File(dirs[i], TEMPLATE_CONFIG_FILE); if (file.isFile() && file.canRead()) files.add(file); } return files; } /** * Parse an XML template config file into * a ConfigInfo POJO model. * * @param file * @return * @throws Exception */ private ConfigInfo parseXMLconfig(File file) throws Exception { // get the XML stream InputStream is = new FileInputStream(file); // create unmarshaller Unmarshaller unmarshaller = UnmarshallerFactory.newInstance().newUnmarshaller(); // create an instance of ObjectModelFactory ObjectModelFactory factory = new ConfigInfoBinding(); // let the object model factory to create an instance of Book and populate it with data from XML ConfigInfo ci = (ConfigInfo)unmarshaller.unmarshal(is, factory, null); // close the XML stream is.close(); return ci; } /** * Copy values from HashMap to VelocityContext, following the * metadata definition. Make sure types are correct, while * required properties are all there. Throw an exception * otherwise * * @param ci * @param map * @return * @throws Exception */ private VelocityContext createTemplateContext(ConfigInfo ci, HashMap map) throws Exception { VelocityContext vc; List propertyList = ci.getPropertyInfoList(); if (propertyList.size() > 0) { vc = new VelocityContext(); for (Iterator i = propertyList.iterator(); i.hasNext(); ) { PropertyInfo pi = (PropertyInfo)i.next(); String name = pi.getName(); String type = pi.getType(); boolean optional = pi.isOptional(); Object defaultValue = pi.getDefaultValue(); if (name == null || name.length() == 0 || type == null || type.length() == 0) throw new Exception("Null or empty name/type property metadata for template: " + ci.getName()); Object sentValue = map.get(name); // a value was sent - pass it over after checking its type if (sentValue != null) { if (!type.equals(sentValue.getClass().getName())) throw new Exception("Expected type '" + type + "' for property '" + name + "', got '" + sentValue.getClass().getName()); vc.put(name, sentValue); } else if (optional == false) { // a value was not sent - property is required // so use the default value (if exists) or throw an exception if (defaultValue != null) { vc.put(name, defaultValue); } else { throw new Exception("Required property missing: '" + name + "' of type '" + type + "'"); } } // property is optional and value was not sent // do nothing even if a default is set } } else { // property list empty, allow everything // just embed the Hashmap vc = new VelocityContext(map); } // add a parameter to allow the templates to report errors vc.put(TEMPLATE_ERROR_PARAM, ""); // add a context helper vc.put(CONTEXT_HELPER, new ContextHelper()); return vc; } /** * Make sure sourceDir exist, then deep copy * all files/dirs from sourceDir to targetDir * * @param sourceDir * @param targetDir */ private void deepCopy(File sourceDir, File targetDir) throws IOException { if (!sourceDir.isDirectory()) throw new IOException("sourceDir not a directory: " + sourceDir); if (!targetDir.mkdir()) throw new IOException("could not create directory: " + targetDir); File[] files = sourceDir.listFiles(); for (int i = 0; i < files.length; i++) { File source = files[i]; if (!source.canRead()) throw new IOException("cannot read: " + source); if (source.isFile()) Files.copy(source, new File(targetDir, source.getName())); else deepCopy(source, new File(targetDir, source.getName())); } } /** * Process the data source change request based on the specified template * and the input properties. * * @param module * @param template * @param properties * @return The full module name, with the suffix. * @throws Exception */ private String processDataSourceChanges (String module, String template, HashMap properties) throws Exception { ConfigInfo ci = (ConfigInfo) configMap.get(template); if (ci == null) throw new Exception("template does not exist: " + template); // Append the extension if the module name does not contain it String extension = ci.getExtension(); if (extension != null && !module.endsWith(extension)) module += extension; // Build the mbean name from the jndi name. Note that we are actually building // a pattern, any mbean whose name matches this pattern will do. String path = configPathFor("jboss.jca:name=" + (String)properties.get("jndi-name") + ",*"); updateConfigFile(module, template, properties, ci, path); return module; } /** * Constructs the XSLT from the Velocity template, performas the tranformation, * and then copies the resulting configuration file to the proper location, * overwriting the existing configuration file. * @param name Identifier used in error message should the deploy fail * @param template The name of the template to use. This should be a Velocity * template that generates an XSTL script. * @param map The properties to use with Velocity. * @param ci The configuration information for the template being used. * @param path Full path name of the current configuration file. This file will * be run through the XSL transform and be overwritten by the result. */ private void updateConfigFile(String name, String template, HashMap map, ConfigInfo ci, String path) throws Exception { String vmTemplate = ci.getTemplate(); // Generate the XSLT based on the properties and the template: StringWriter sw = new StringWriter(); PrintWriter xslt = new PrintWriter(sw); VelocityContext ctx = createTemplateContext(ci, map); String tp = template + File.separator + vmTemplate; ve.mergeTemplate(tp, ctx, xslt); StringBuffer buf = sw.getBuffer(); xslt.close(); // Update the configuration XML file using the generated XSLT. For now, // place the generated file into the undeploy directory: File configFile = new File(path); File outputFile = new File(this.undeployDir, configFile.getName()); InputStream configStream = new FileInputStream(configFile); InputStream xsltStream = new StringBufferInputStream(buf.toString()); OutputStream outputStream = new FileOutputStream(outputFile); XslTransformer.applyTransformation(configStream, outputStream, xsltStream, null); configStream.close(); xsltStream.close(); outputStream.close(); // Now that we have the generated file, move it to its proper location if (!configFile.delete()) throw new Exception("Update failed for '" + name + "', unable to delete old configuration file: " + path); if (!outputFile.renameTo(configFile)) throw new Exception("Update failed for '" + name + "', unable to move configuration file to deploy directory: " + path); } }