001: /*
002: * JBoss, Home of Professional Open Source.
003: * Copyright 2006, Red Hat Middleware LLC, and individual contributors
004: * as indicated by the @author tags. See the copyright.txt file in the
005: * distribution for a full listing of individual contributors.
006: *
007: * This is free software; you can redistribute it and/or modify it
008: * under the terms of the GNU Lesser General Public License as
009: * published by the Free Software Foundation; either version 2.1 of
010: * the License, or (at your option) any later version.
011: *
012: * This software is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this software; if not, write to the Free
019: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021: */
022: package org.jboss.services.deployment;
023:
024: import java.io.BufferedWriter;
025: import java.io.File;
026: import java.io.FileFilter;
027: import java.io.FileInputStream;
028: import java.io.FileWriter;
029: import java.io.IOException;
030: import java.io.InputStream;
031: import java.net.URL;
032: import java.util.ArrayList;
033: import java.util.Collections;
034: import java.util.HashMap;
035: import java.util.Iterator;
036: import java.util.List;
037: import java.util.Map;
038: import java.util.Set;
039: import java.util.TreeMap;
040: import java.util.TreeSet;
041:
042: import org.apache.velocity.VelocityContext;
043: import org.apache.velocity.app.VelocityEngine;
044: import org.jboss.logging.Logger;
045: import org.jboss.services.deployment.metadata.ConfigInfo;
046: import org.jboss.services.deployment.metadata.ConfigInfoBinding;
047: import org.jboss.services.deployment.metadata.PropertyInfo;
048: import org.jboss.services.deployment.metadata.TemplateInfo;
049: import org.jboss.system.server.ServerConfigLocator;
050: import org.jboss.util.file.Files;
051: import org.jboss.xb.binding.ObjectModelFactory;
052: import org.jboss.xb.binding.Unmarshaller;
053: import org.jboss.xb.binding.UnmarshallerFactory;
054:
055: /**
056: * Class handling JBoss module generation. Uses apache velocity
057: * for generating deployment descriptors.
058: *
059: * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
060: *
061: * @version $Revision: 57210 $
062: */
063: public class DeploymentManager {
064: // Constants -----------------------------------------------------
065:
066: /** the filename to look for in template subdirectories */
067: public static final String TEMPLATE_CONFIG_FILE = "template-config.xml";
068:
069: /** an object to pass back from the template to trigger an error */
070: public static final String TEMPLATE_ERROR_PARAM = "template-error";
071:
072: /** a helper object to pass in to the template */
073: public static final String CONTEXT_HELPER = "helper";
074:
075: // Private Data --------------------------------------------------
076:
077: /** Logger */
078: private Logger log;
079:
080: /** directory to hold the template subdirectories */
081: private File templateDir;
082:
083: /** the directory to output generated modules */
084: private File undeployDir;
085:
086: /** the directory to move modules for deployment */
087: private File deployDir;
088:
089: /** config name string -> ConfigInfo */
090: private Map configMap;
091:
092: /** the apache velocity engine */
093: VelocityEngine ve;
094:
095: /**
096: * @param templateDir the root dir where templates are stored
097: * @param packageDir the directory to store generated packages
098: */
099: public DeploymentManager(String templateDir, String undeployDir,
100: String deployDir, Logger log) throws Exception {
101: this .log = log;
102:
103: // do the actuall initialization
104: initialize(templateDir, undeployDir, deployDir);
105: }
106:
107: // Public Interface ----------------------------------------------
108:
109: /**
110: * Return the list of available templates
111: */
112: public Set listModuleTemplates() {
113: Set keys = configMap.keySet();
114:
115: synchronized (configMap) {
116: // return a new sorted copy
117: return new TreeSet(keys);
118: }
119: }
120:
121: /**
122: * Get property metadata information for a particular template
123: *
124: * @param template
125: * @return list with PropertyInfo objects associated with the template
126: * @throws Exception if the template does not exist
127: */
128: public List getTemplatePropertyInfo(String template)
129: throws Exception {
130: ConfigInfo ci = (ConfigInfo) configMap.get(template);
131:
132: if (ci == null) {
133: throw new Exception("template does not exist: " + template);
134: } else { // return a copy
135: List propertyList = ci.getPropertyInfoList();
136: List newList = new ArrayList(propertyList.size());
137:
138: for (Iterator i = propertyList.iterator(); i.hasNext();) {
139: newList.add(new PropertyInfo((PropertyInfo) i.next()));
140: }
141: return newList;
142: }
143: }
144:
145: public String createModule(String module, String template,
146: HashMap properties) throws Exception {
147: if (module == null || template == null || properties == null)
148: throw new Exception("Null argument: module=" + module
149: + ", template=" + template + ", properties="
150: + properties);
151:
152: // make sure proposed module name is filesystem friendly
153: if (!module.equals(Files.encodeFileName(module)))
154: throw new Exception(
155: "not a filesystem friendly module name: " + module);
156:
157: ConfigInfo ci = (ConfigInfo) configMap.get(template);
158:
159: if (ci == null)
160: throw new Exception("template does not exist: " + template);
161:
162: // get optional package extension (e.g. .sar)
163: // and enforce it on the output package (file or directory)
164: File outputModule;
165:
166: String extension = ci.getExtension();
167: if (extension == null || module.endsWith(extension))
168: outputModule = new File(this .undeployDir, module);
169: else
170: outputModule = new File(this .undeployDir, module
171: + extension);
172:
173: // check if module already exists in output dir
174: if (outputModule.exists())
175: throw new Exception("module already exist: " + outputModule);
176:
177: String vmTemplate = ci.getTemplate();
178:
179: // make sure we clean-up in case something goes wrong
180: try {
181: // simple case - single descriptor package (e.g. xxx-service.xml)
182: if (vmTemplate != null) {
183: VelocityContext ctx = createTemplateContext(ci,
184: properties);
185:
186: BufferedWriter out = new BufferedWriter(new FileWriter(
187: outputModule));
188:
189: try {
190: boolean success = ve.mergeTemplate(template + '/'
191: + vmTemplate, ctx, out);
192:
193: if (success == true) {
194: String errorMsg = (String) ctx
195: .get(TEMPLATE_ERROR_PARAM);
196:
197: if (errorMsg.length() > 0)
198: throw new Exception("Template error: "
199: + errorMsg);
200: else
201: log.debug("created module '"
202: + outputModule.getName()
203: + "' based on template '"
204: + template + "'");
205: } else
206: throw new Exception("Failed to create module '"
207: + outputModule.getName());
208: } finally {
209: out.close();
210: }
211: } else {
212: // complex case - many descriptors and possibly files to copy
213: // now output will be a directory instead of a plain descriptor (e.g. xxx.sar)
214: VelocityContext ctx = createTemplateContext(ci,
215: properties);
216:
217: // deep copy files if copydir specified
218: String copydir = ci.getCopydir();
219:
220: File sourceDir = new File(this .templateDir, template
221: + '/' + copydir);
222:
223: deepCopy(sourceDir, outputModule);
224:
225: // go through all declared templates
226: List templateList = ci.getTemplateInfoList();
227:
228: for (Iterator i = templateList.iterator(); i.hasNext();) {
229: TemplateInfo ti = (TemplateInfo) i.next();
230:
231: File outputFile = new File(outputModule, ti
232: .getOutput());
233: File outputPath = outputFile.getParentFile();
234:
235: if (!outputPath.exists())
236: if (!outputPath.mkdirs())
237: throw new IOException(
238: "cannot create directory: "
239: + outputPath);
240:
241: BufferedWriter out = new BufferedWriter(
242: new FileWriter(outputFile));
243:
244: try {
245: boolean success = ve.mergeTemplate(template
246: + '/' + ti.getInput(), ctx, out);
247:
248: if (success == true) {
249: String errorMsg = (String) ctx
250: .get(TEMPLATE_ERROR_PARAM);
251:
252: if (errorMsg.length() > 0)
253: throw new Exception("Template error: "
254: + errorMsg);
255: else
256: log.debug("created module '"
257: + outputModule.getName()
258: + "' based on template '"
259: + template + "'");
260: }
261:
262: else
263: throw new Exception(
264: "Failed to create package '"
265: + outputModule.getName());
266: } finally {
267: out.close();
268: }
269: }
270: }
271: } catch (Exception e) {
272: if (outputModule.exists()) {
273: boolean deleted = Files.delete(outputModule);
274: if (!deleted)
275: log.warn("Failed to clean-up erroneous module: "
276: + outputModule);
277: }
278: throw e;
279: }
280: return outputModule.getName();
281: }
282:
283: /**
284: * Remove a module if exists
285: *
286: * @param module the module to remove
287: * @return true if removed, false if module does not exist or an error occurs
288: */
289: public boolean removeModule(String module) {
290: File target = new File(this .undeployDir, module);
291: return Files.delete(target);
292: }
293:
294: public void moveToDeployDir(String module) throws Exception {
295: File source = new File(this .undeployDir, module);
296: File target = new File(this .deployDir, module);
297:
298: if (source.exists()) {
299: boolean moved = source.renameTo(target);
300: if (!moved)
301: throw new Exception("cannot move module: " + module);
302: } else
303: throw new Exception("module does not exist: " + module);
304: }
305:
306: public void moveToModuleDir(String module) throws Exception {
307: File source = new File(this .deployDir, module);
308: File target = new File(this .undeployDir, module);
309:
310: if (source.exists()) {
311: boolean moved = source.renameTo(target);
312: if (!moved)
313: throw new Exception("cannot move module: " + module);
314: } else
315: throw new Exception("module does not exist: " + module);
316: }
317:
318: public URL getDeployedURL(String module) throws Exception {
319: File target = new File(this .deployDir, module);
320:
321: if (!target.exists())
322: throw new Exception("module does not exist: " + target);
323:
324: return target.toURL();
325: }
326:
327: public URL getUndeployedURL(String module) throws Exception {
328: File target = new File(this .undeployDir, module);
329:
330: if (!target.exists())
331: throw new Exception("module does not exist: " + target);
332:
333: return target.toURL();
334: }
335:
336: // Private Methods -----------------------------------------------
337:
338: /**
339: * Performs the actual initialization
340: */
341: private void initialize(String templateDir, String undeployDir,
342: String deployDir) throws Exception {
343: boolean debug = log.isDebugEnabled();
344: boolean trace = log.isTraceEnabled();
345:
346: // Find out template dir
347: this .templateDir = initDir(templateDir, false);
348: if (debug)
349: log.debug("template dir=" + this .templateDir);
350:
351: // Initialize output dir
352: this .undeployDir = initDir(undeployDir, true);
353: if (debug)
354: log.debug("undeployDir dir=" + this .undeployDir);
355:
356: this .deployDir = initDir(deployDir, false);
357: if (debug)
358: log.debug("deploy dir=" + this .deployDir);
359:
360: // Discover all template config files
361: List configFiles = findTemplateConfigFiles(this .templateDir);
362:
363: if (debug)
364: log.debug("template config files=" + configFiles);
365:
366: Map map = Collections.synchronizedMap(new TreeMap());
367:
368: // Parse each template config file and store metadata in configMap
369: for (Iterator i = configFiles.iterator(); i.hasNext();) {
370: File file = (File) i.next();
371: ConfigInfo ci = parseXMLconfig(file);
372:
373: // derive template name from subdirectory name
374: ci.setName(file.getParentFile().getName());
375:
376: if (trace)
377: log.trace("file: " + file + " ConfigInfo: " + ci);
378:
379: Object existingValue = map.put(ci.getName(), ci);
380:
381: // make sure not two configuration templates with the same name
382: if (existingValue != null)
383: throw new Exception(
384: "Duplicate template configuration entry: "
385: + ci.getName());
386: }
387:
388: this .configMap = map;
389:
390: // Initialise velocity engine
391: this .ve = new VelocityEngine();
392:
393: this .ve.setProperty("runtime.log.logsystem.class",
394: "org.apache.velocity.runtime.log.SimpleLog4JLogSystem");
395: this .ve.setProperty("runtime.log.logsystem.log4j.category", log
396: .getName()
397: + ".VelocityEngine");
398: this .ve.setProperty("file.resource.loader.path",
399: this .templateDir.getCanonicalPath());
400:
401: this .ve.init();
402: }
403:
404: /**
405: * Check if directory exists as an absolute path,
406: * otherwise, try to find it under the jboss server
407: * directories (and optionally create it, if the
408: * create flag has been set)
409: */
410: private File initDir(String targetDir, boolean create)
411: throws Exception {
412: File dir = null;
413:
414: // Check if this is an existing absolute path
415: try {
416: URL fileURL = new URL(targetDir);
417:
418: File file = new File(fileURL.getFile());
419:
420: if (file.isDirectory() && file.canRead() && file.canWrite()) {
421: dir = file;
422: }
423: } catch (Exception e) {
424: // Otherwise, try to see inside the jboss directory hierarchy
425:
426: File homeDir = ServerConfigLocator.locate()
427: .getServerHomeDir();
428:
429: dir = new File(homeDir, targetDir);
430:
431: if (create == true)
432: dir.mkdirs();
433:
434: if (!dir.isDirectory())
435: throw new Exception(
436: "The target directory is not valid: "
437: + dir.getCanonicalPath());
438: }
439: return dir;
440: }
441:
442: /**
443: * Find all files named TEMPLATE_CONFIG_FILE
444: * one level below basedir, i.e.
445: *
446: * basedir/YYY/template-config.xml
447: * ...
448: *
449: * @param basedir
450: * @return
451: */
452: private List findTemplateConfigFiles(File basedir) {
453: // return val
454: List files = new ArrayList();
455:
456: // anonymous class
457: FileFilter dirFilter = new FileFilter() {
458: public boolean accept(File file) {
459: return file.isDirectory()
460: && !file.getName().startsWith(".");
461: }
462: };
463: // return all dirs not starting with "."
464: File[] dirs = basedir.listFiles(dirFilter);
465:
466: for (int i = 0; i < dirs.length; i++) {
467: File file = new File(dirs[i], TEMPLATE_CONFIG_FILE);
468:
469: if (file.isFile() && file.canRead())
470: files.add(file);
471: }
472: return files;
473: }
474:
475: /**
476: * Parse an XML template config file into
477: * a ConfigInfo POJO model.
478: *
479: * @param file
480: * @return
481: * @throws Exception
482: */
483: private ConfigInfo parseXMLconfig(File file) throws Exception {
484: // get the XML stream
485: InputStream is = new FileInputStream(file);
486:
487: // create unmarshaller
488: Unmarshaller unmarshaller = UnmarshallerFactory.newInstance()
489: .newUnmarshaller();
490:
491: // create an instance of ObjectModelFactory
492: ObjectModelFactory factory = new ConfigInfoBinding();
493:
494: // let the object model factory to create an instance of Book and populate it with data from XML
495: ConfigInfo ci = (ConfigInfo) unmarshaller.unmarshal(is,
496: factory, null);
497:
498: // close the XML stream
499: is.close();
500:
501: return ci;
502: }
503:
504: /**
505: * Copy values from HashMap to VelocityContext, following the
506: * metadata definition. Make sure types are correct, while
507: * required properties are all there. Throw an exception
508: * otherwise
509: *
510: * @param ci
511: * @param map
512: * @return
513: * @throws Exception
514: */
515: private VelocityContext createTemplateContext(ConfigInfo ci,
516: HashMap map) throws Exception {
517: VelocityContext vc;
518:
519: List propertyList = ci.getPropertyInfoList();
520:
521: if (propertyList.size() > 0) {
522: vc = new VelocityContext();
523:
524: for (Iterator i = propertyList.iterator(); i.hasNext();) {
525: PropertyInfo pi = (PropertyInfo) i.next();
526:
527: String name = pi.getName();
528: String type = pi.getType();
529: boolean optional = pi.isOptional();
530: Object defaultValue = pi.getDefaultValue();
531:
532: if (name == null || name.length() == 0 || type == null
533: || type.length() == 0)
534: throw new Exception(
535: "Null or empty name/type property metadata for template: "
536: + ci.getName());
537:
538: Object sentValue = map.get(name);
539:
540: // a value was sent - pass it over after checking its type
541: if (sentValue != null) {
542: if (!type.equals(sentValue.getClass().getName()))
543: throw new Exception("Expected type '" + type
544: + "' for property '" + name
545: + "', got '"
546: + sentValue.getClass().getName());
547:
548: vc.put(name, sentValue);
549: } else if (optional == false) {
550: // a value was not sent - property is required
551: // so use the default value (if exists) or throw an exception
552: if (defaultValue != null) {
553: vc.put(name, defaultValue);
554: } else {
555: throw new Exception(
556: "Required property missing: '" + name
557: + "' of type '" + type + "'");
558: }
559: }
560: // property is optional and value was not sent
561: // do nothing even if a default is set
562: }
563: } else {
564: // property list empty, allow everything
565: // just embed the Hashmap
566: vc = new VelocityContext(map);
567: }
568: // add a parameter to allow the templates to report errors
569: vc.put(TEMPLATE_ERROR_PARAM, "");
570: // add a context helper
571: vc.put(CONTEXT_HELPER, new ContextHelper());
572:
573: return vc;
574: }
575:
576: /**
577: * Make sure sourceDir exist, then deep copy
578: * all files/dirs from sourceDir to targetDir
579: *
580: * @param sourceDir
581: * @param targetDir
582: */
583: private void deepCopy(File sourceDir, File targetDir)
584: throws IOException {
585: if (!sourceDir.isDirectory())
586: throw new IOException("sourceDir not a directory: "
587: + sourceDir);
588:
589: if (!targetDir.mkdir())
590: throw new IOException("could not create directory: "
591: + targetDir);
592:
593: File[] files = sourceDir.listFiles();
594:
595: for (int i = 0; i < files.length; i++) {
596: File source = files[i];
597:
598: if (!source.canRead())
599: throw new IOException("cannot read: " + source);
600:
601: if (source.isFile())
602: Files.copy(source,
603: new File(targetDir, source.getName()));
604: else
605: deepCopy(source, new File(targetDir, source.getName()));
606: }
607: }
608: }
|