001: package org.apache.velocity.anakia;
002:
003: /*
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021:
022: import java.io.BufferedWriter;
023: import java.io.File;
024: import java.io.FileOutputStream;
025: import java.io.IOException;
026: import java.io.OutputStreamWriter;
027: import java.io.Writer;
028: import java.util.Iterator;
029: import java.util.LinkedList;
030: import java.util.List;
031: import java.util.StringTokenizer;
032:
033: import org.apache.commons.collections.ExtendedProperties;
034: import org.apache.tools.ant.BuildException;
035: import org.apache.tools.ant.DirectoryScanner;
036: import org.apache.tools.ant.Project;
037: import org.apache.tools.ant.taskdefs.MatchingTask;
038: import org.apache.velocity.Template;
039: import org.apache.velocity.VelocityContext;
040: import org.apache.velocity.app.VelocityEngine;
041: import org.apache.velocity.runtime.RuntimeConstants;
042: import org.apache.velocity.util.StringUtils;
043:
044: import org.jdom.Document;
045: import org.jdom.JDOMException;
046: import org.jdom.input.SAXBuilder;
047: import org.jdom.output.Format;
048: import org.xml.sax.SAXParseException;
049:
050: /**
051: * The purpose of this Ant Task is to allow you to use
052: * Velocity as an XML transformation tool like XSLT is.
053: * So, instead of using XSLT, you will be able to use this
054: * class instead to do your transformations. It works very
055: * similar in concept to Ant's <style> task.
056: * <p>
057: * You can find more documentation about this class on the
058: * Velocity
059: * <a href="http://velocity.apache.org/engine/devel/docs/anakia.html">Website</a>.
060: *
061: * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
062: * @author <a href="mailto:szegedia@freemail.hu">Attila Szegedi</a>
063: * @version $Id: AnakiaTask.java 500772 2007-01-28 10:28:09Z henning $
064: */
065: public class AnakiaTask extends MatchingTask {
066: /** <code>{@link SAXBuilder}</code> instance to use */
067: SAXBuilder builder;
068:
069: /** the destination directory */
070: private File destDir = null;
071:
072: /** the base directory */
073: File baseDir = null;
074:
075: /** the style= attribute */
076: private String style = null;
077:
078: /** last modified of the style sheet */
079: private long styleSheetLastModified = 0;
080:
081: /** the projectFile= attribute */
082: private String projectAttribute = null;
083:
084: /** the File for the project.xml file */
085: private File projectFile = null;
086:
087: /** last modified of the project file if it exists */
088: private long projectFileLastModified = 0;
089:
090: /** check the last modified date on files. defaults to true */
091: private boolean lastModifiedCheck = true;
092:
093: /** the default output extension is .html */
094: private String extension = ".html";
095:
096: /** the template path */
097: private String templatePath = null;
098:
099: /** the file to get the velocity properties file */
100: private File velocityPropertiesFile = null;
101:
102: /** the VelocityEngine instance to use */
103: private VelocityEngine ve = new VelocityEngine();
104:
105: /** the Velocity subcontexts */
106: private List contexts = new LinkedList();
107:
108: /**
109: * Constructor creates the SAXBuilder.
110: */
111: public AnakiaTask() {
112: builder = new SAXBuilder();
113: builder.setFactory(new AnakiaJDOMFactory());
114: }
115:
116: /**
117: * Set the base directory.
118: * @param dir
119: */
120: public void setBasedir(File dir) {
121: baseDir = dir;
122: }
123:
124: /**
125: * Set the destination directory into which the VSL result
126: * files should be copied to
127: * @param dir the name of the destination directory
128: */
129: public void setDestdir(File dir) {
130: destDir = dir;
131: }
132:
133: /**
134: * Allow people to set the default output file extension
135: * @param extension
136: */
137: public void setExtension(String extension) {
138: this .extension = extension;
139: }
140:
141: /**
142: * Allow people to set the path to the .vsl file
143: * @param style
144: */
145: public void setStyle(String style) {
146: this .style = style;
147: }
148:
149: /**
150: * Allow people to set the path to the project.xml file
151: * @param projectAttribute
152: */
153: public void setProjectFile(String projectAttribute) {
154: this .projectAttribute = projectAttribute;
155: }
156:
157: /**
158: * Set the path to the templates.
159: * The way it works is this:
160: * If you have a Velocity.properties file defined, this method
161: * will <strong>override</strong> whatever is set in the
162: * Velocity.properties file. This allows one to not have to define
163: * a Velocity.properties file, therefore using Velocity's defaults
164: * only.
165: * @param templatePath
166: */
167:
168: public void setTemplatePath(File templatePath) {
169: try {
170: this .templatePath = templatePath.getCanonicalPath();
171: } catch (java.io.IOException ioe) {
172: throw new BuildException(ioe);
173: }
174: }
175:
176: /**
177: * Allow people to set the path to the velocity.properties file
178: * This file is found relative to the path where the JVM was run.
179: * For example, if build.sh was executed in the ./build directory,
180: * then the path would be relative to this directory.
181: * This is optional based on the setting of setTemplatePath().
182: * @param velocityPropertiesFile
183: */
184: public void setVelocityPropertiesFile(File velocityPropertiesFile) {
185: this .velocityPropertiesFile = velocityPropertiesFile;
186: }
187:
188: /**
189: * Turn on/off last modified checking. by default, it is on.
190: * @param lastmod
191: */
192: public void setLastModifiedCheck(String lastmod) {
193: if (lastmod.equalsIgnoreCase("false")
194: || lastmod.equalsIgnoreCase("no")
195: || lastmod.equalsIgnoreCase("off")) {
196: this .lastModifiedCheck = false;
197: }
198: }
199:
200: /**
201: * Main body of the application
202: * @throws BuildException
203: */
204: public void execute() throws BuildException {
205: DirectoryScanner scanner;
206: String[] list;
207:
208: if (baseDir == null) {
209: baseDir = project.resolveFile(".");
210: }
211: if (destDir == null) {
212: String msg = "destdir attribute must be set!";
213: throw new BuildException(msg);
214: }
215: if (style == null) {
216: throw new BuildException("style attribute must be set!");
217: }
218:
219: if (velocityPropertiesFile == null) {
220: velocityPropertiesFile = new File("velocity.properties");
221: }
222:
223: /*
224: * If the props file doesn't exist AND a templatePath hasn't
225: * been defined, then throw the exception.
226: */
227: if (!velocityPropertiesFile.exists() && templatePath == null) {
228: throw new BuildException("No template path and could not "
229: + "locate velocity.properties file: "
230: + velocityPropertiesFile.getAbsolutePath());
231: }
232:
233: log("Transforming into: " + destDir.getAbsolutePath(),
234: Project.MSG_INFO);
235:
236: // projectFile relative to baseDir
237: if (projectAttribute != null && projectAttribute.length() > 0) {
238: projectFile = new File(baseDir, projectAttribute);
239: if (projectFile.exists()) {
240: projectFileLastModified = projectFile.lastModified();
241: } else {
242: log(
243: "Project file is defined, but could not be located: "
244: + projectFile.getAbsolutePath(),
245: Project.MSG_INFO);
246: projectFile = null;
247: }
248: }
249:
250: Document projectDocument = null;
251: try {
252: if (velocityPropertiesFile.exists()) {
253: String file = velocityPropertiesFile.getAbsolutePath();
254: ExtendedProperties config = new ExtendedProperties(file);
255: ve.setExtendedProperties(config);
256: }
257:
258: // override the templatePath if it exists
259: if (templatePath != null && templatePath.length() > 0) {
260: ve.setProperty(
261: RuntimeConstants.FILE_RESOURCE_LOADER_PATH,
262: templatePath);
263: }
264:
265: ve.init();
266:
267: // get the last modification of the VSL stylesheet
268: styleSheetLastModified = ve.getTemplate(style)
269: .getLastModified();
270:
271: // Build the Project file document
272: if (projectFile != null) {
273: projectDocument = builder.build(projectFile);
274: }
275: } catch (Exception e) {
276: log("Error: " + e.toString(), Project.MSG_INFO);
277: throw new BuildException(e);
278: }
279:
280: // find the files/directories
281: scanner = getDirectoryScanner(baseDir);
282:
283: // get a list of files to work on
284: list = scanner.getIncludedFiles();
285: for (int i = 0; i < list.length; ++i) {
286: process(list[i], projectDocument);
287: }
288:
289: }
290:
291: /**
292: * Process an XML file using Velocity
293: */
294: private void process(String xmlFile, Document projectDocument)
295: throws BuildException {
296: File outFile = null;
297: File inFile = null;
298: Writer writer = null;
299: try {
300: // the current input file relative to the baseDir
301: inFile = new File(baseDir, xmlFile);
302: // the output file relative to basedir
303: outFile = new File(destDir, xmlFile.substring(0, xmlFile
304: .lastIndexOf('.'))
305: + extension);
306:
307: // only process files that have changed
308: if (lastModifiedCheck == false
309: || (inFile.lastModified() > outFile.lastModified()
310: || styleSheetLastModified > outFile
311: .lastModified()
312: || projectFileLastModified > outFile
313: .lastModified() || userContextsModifed(outFile
314: .lastModified()))) {
315: ensureDirectoryFor(outFile);
316:
317: //-- command line status
318: log("Input: " + xmlFile, Project.MSG_INFO);
319:
320: // Build the JDOM Document
321: Document root = builder.build(inFile);
322:
323: // Shove things into the Context
324: VelocityContext context = new VelocityContext();
325:
326: /*
327: * get the property TEMPLATE_ENCODING
328: * we know it's a string...
329: */
330: String encoding = (String) ve
331: .getProperty(RuntimeConstants.OUTPUT_ENCODING);
332: if (encoding == null || encoding.length() == 0
333: || encoding.equals("8859-1")
334: || encoding.equals("8859_1")) {
335: encoding = "ISO-8859-1";
336: }
337:
338: Format f = Format.getRawFormat();
339: f.setEncoding(encoding);
340:
341: OutputWrapper ow = new OutputWrapper(f);
342:
343: context.put("root", root.getRootElement());
344: context.put("xmlout", ow);
345: context.put("relativePath", getRelativePath(xmlFile));
346: context.put("treeWalk", new TreeWalker());
347: context.put("xpath", new XPathTool());
348: context.put("escape", new Escape());
349: context.put("date", new java.util.Date());
350:
351: /**
352: * only put this into the context if it exists.
353: */
354: if (projectDocument != null) {
355: context.put("project", projectDocument
356: .getRootElement());
357: }
358:
359: /**
360: * Add the user subcontexts to the to context
361: */
362: for (Iterator iter = contexts.iterator(); iter
363: .hasNext();) {
364: Context subContext = (Context) iter.next();
365: if (subContext == null) {
366: throw new BuildException(
367: "Found an undefined SubContext!");
368: }
369:
370: if (subContext.getContextDocument() == null) {
371: throw new BuildException(
372: "Could not build a subContext for "
373: + subContext.getName());
374: }
375:
376: context.put(subContext.getName(), subContext
377: .getContextDocument().getRootElement());
378: }
379:
380: /**
381: * Process the VSL template with the context and write out
382: * the result as the outFile.
383: */
384: writer = new BufferedWriter(new OutputStreamWriter(
385: new FileOutputStream(outFile), encoding));
386:
387: /**
388: * get the template to process
389: */
390: Template template = ve.getTemplate(style);
391: template.merge(context, writer);
392:
393: log("Output: " + outFile, Project.MSG_INFO);
394: }
395: } catch (JDOMException e) {
396: outFile.delete();
397:
398: if (e.getCause() != null) {
399: Throwable rootCause = e.getCause();
400: if (rootCause instanceof SAXParseException) {
401: System.out.println("");
402: System.out.println("Error: "
403: + rootCause.getMessage());
404: System.out.println(" Line: "
405: + ((SAXParseException) rootCause)
406: .getLineNumber()
407: + " Column: "
408: + ((SAXParseException) rootCause)
409: .getColumnNumber());
410: System.out.println("");
411: } else {
412: rootCause.printStackTrace();
413: }
414: } else {
415: e.printStackTrace();
416: }
417: } catch (Throwable e) {
418: if (outFile != null) {
419: outFile.delete();
420: }
421: e.printStackTrace();
422: } finally {
423: if (writer != null) {
424: try {
425: writer.flush();
426: } catch (IOException e) {
427: // Do nothing
428: }
429:
430: try {
431: writer.close();
432: } catch (IOException e) {
433: // Do nothing
434: }
435: }
436: }
437: }
438:
439: /**
440: * Hacky method to figure out the relative path
441: * that we are currently in. This is good for getting
442: * the relative path for images and anchor's.
443: */
444: private String getRelativePath(String file) {
445: if (file == null || file.length() == 0)
446: return "";
447: StringTokenizer st = new StringTokenizer(file, "/\\");
448: // needs to be -1 cause ST returns 1 even if there are no matches. huh?
449: int slashCount = st.countTokens() - 1;
450: StringBuffer sb = new StringBuffer();
451: for (int i = 0; i < slashCount; i++) {
452: sb.append("../");
453: }
454:
455: if (sb.toString().length() > 0) {
456: return StringUtils.chop(sb.toString(), 1);
457: }
458:
459: return ".";
460: }
461:
462: /**
463: * create directories as needed
464: */
465: private void ensureDirectoryFor(File targetFile)
466: throws BuildException {
467: File directory = new File(targetFile.getParent());
468: if (!directory.exists()) {
469: if (!directory.mkdirs()) {
470: throw new BuildException("Unable to create directory: "
471: + directory.getAbsolutePath());
472: }
473: }
474: }
475:
476: /**
477: * Check to see if user context is modified.
478: */
479: private boolean userContextsModifed(long lastModified) {
480: for (Iterator iter = contexts.iterator(); iter.hasNext();) {
481: AnakiaTask.Context ctx = (AnakiaTask.Context) iter.next();
482: if (ctx.getLastModified() > lastModified) {
483: return true;
484: }
485: }
486: return false;
487: }
488:
489: /**
490: * Create a new context.
491: * @return A new context.
492: */
493: public Context createContext() {
494: Context context = new Context();
495: contexts.add(context);
496: return context;
497: }
498:
499: /**
500: * A context implementation that loads all values from an XML file.
501: */
502: public class Context {
503:
504: private String name;
505: private Document contextDoc = null;
506: private String file;
507:
508: /**
509: * Public constructor.
510: */
511: public Context() {
512: }
513:
514: /**
515: * Get the name of the context.
516: * @return The name of the context.
517: */
518: public String getName() {
519: return name;
520: }
521:
522: /**
523: * Set the name of the context.
524: * @param name
525: *
526: * @throws IllegalArgumentException if a reserved word is used as a
527: * name, specifically any of "relativePath", "treeWalk", "xpath",
528: * "escape", "date", or "project"
529: */
530: public void setName(String name) {
531: if (name.equals("relativePath") || name.equals("treeWalk")
532: || name.equals("xpath") || name.equals("escape")
533: || name.equals("date") || name.equals("project")) {
534:
535: throw new IllegalArgumentException("Context name '"
536: + name + "' is reserved by Anakia");
537: }
538:
539: this .name = name;
540: }
541:
542: /**
543: * Build the context based on a file path.
544: * @param file
545: */
546: public void setFile(String file) {
547: this .file = file;
548: }
549:
550: /**
551: * Retrieve the time the source file was last modified.
552: * @return The time the source file was last modified.
553: */
554: public long getLastModified() {
555: return new File(baseDir, file).lastModified();
556: }
557:
558: /**
559: * Retrieve the context document object.
560: * @return The context document object.
561: */
562: public Document getContextDocument() {
563: if (contextDoc == null) {
564: File contextFile = new File(baseDir, file);
565:
566: try {
567: contextDoc = builder.build(contextFile);
568: } catch (Exception e) {
569: throw new BuildException(e);
570: }
571: }
572: return contextDoc;
573: }
574: }
575:
576: }
|