View Javadoc

1   package org.apache.anakia;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.    
20   */
21  
22  import java.io.BufferedWriter;
23  import java.io.File;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.OutputStreamWriter;
27  import java.io.Writer;
28  import java.util.Iterator;
29  import java.util.LinkedList;
30  import java.util.List;
31  import java.util.StringTokenizer;
32  
33  import org.apache.commons.collections.ExtendedProperties;
34  import org.apache.tools.ant.BuildException;
35  import org.apache.tools.ant.DirectoryScanner;
36  import org.apache.tools.ant.Project;
37  import org.apache.tools.ant.taskdefs.MatchingTask;
38  import org.apache.velocity.Template;
39  import org.apache.velocity.VelocityContext;
40  import org.apache.velocity.app.VelocityEngine;
41  import org.apache.velocity.runtime.RuntimeConstants;
42  import org.apache.velocity.util.StringUtils;
43  
44  import org.jdom.Document;
45  import org.jdom.JDOMException;
46  import org.jdom.input.SAXBuilder;
47  import org.jdom.output.Format;
48  import org.xml.sax.SAXParseException;
49  
50  /**
51   * The purpose of this Ant Task is to allow you to use
52   * Velocity as an XML transformation tool like XSLT is.
53   * So, instead of using XSLT, you will be able to use this
54   * class instead to do your transformations. It works very
55   * similar in concept to Ant's <style> task.
56   * <p>
57   * You can find more documentation about this class on the
58   * Velocity
59   * <a href="http://velocity.apache.org/engine/devel/docs/anakia.html">Website</a>.
60   *
61   * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
62   * @author <a href="mailto:szegedia@freemail.hu">Attila Szegedi</a>
63   * @version $Id: AnakiaTask.java 524478 2007-03-31 20:51:49Z wglass $
64   */
65  public class AnakiaTask extends MatchingTask
66  {
67      /** <code>{@link SAXBuilder}</code> instance to use */
68      SAXBuilder builder;
69  
70      /** the destination directory */
71      private File destDir = null;
72  
73      /** the base directory */
74      File baseDir = null;
75  
76      /** the style= attribute */
77      private String style = null;
78  
79      /** last modified of the style sheet */
80      private long styleSheetLastModified = 0;
81  
82      /** the projectFile= attribute */
83      private String projectAttribute = null;
84  
85      /** the File for the project.xml file */
86      private File projectFile = null;
87  
88      /** last modified of the project file if it exists */
89      private long projectFileLastModified = 0;
90  
91      /** check the last modified date on files. defaults to true */
92      private boolean lastModifiedCheck = true;
93  
94      /** the default output extension is .html */
95      private String extension = ".html";
96  
97      /** the template path */
98      private String templatePath = null;
99  
100     /** the file to get the velocity properties file */
101     private File velocityPropertiesFile = null;
102 
103     /** the VelocityEngine instance to use */
104     private VelocityEngine ve = new VelocityEngine();
105 
106     /** the Velocity subcontexts */
107     private List contexts = new LinkedList();
108 
109     /**
110      * Constructor creates the SAXBuilder.
111      */
112     public AnakiaTask()
113     {
114         builder = new SAXBuilder();
115         builder.setFactory(new AnakiaJDOMFactory());
116     }
117 
118     /**
119      * Set the base directory.
120      * @param dir
121      */
122     public void setBasedir(File dir)
123     {
124         baseDir = dir;
125     }
126 
127     /**
128      * Set the destination directory into which the VSL result
129      * files should be copied to
130      * @param dir the name of the destination directory
131      */
132     public void setDestdir(File dir)
133     {
134         destDir = dir;
135     }
136 
137     /**
138      * Allow people to set the default output file extension
139      * @param extension
140      */
141     public void setExtension(String extension)
142     {
143         this.extension = extension;
144     }
145 
146     /**
147      * Allow people to set the path to the .vsl file
148      * @param style
149      */
150     public void setStyle(String style)
151     {
152         this.style = style;
153     }
154 
155     /**
156      * Allow people to set the path to the project.xml file
157      * @param projectAttribute
158      */
159     public void setProjectFile(String projectAttribute)
160     {
161         this.projectAttribute = projectAttribute;
162     }
163 
164     /**
165      * Set the path to the templates.
166      * The way it works is this:
167      * If you have a Velocity.properties file defined, this method
168      * will <strong>override</strong> whatever is set in the
169      * Velocity.properties file. This allows one to not have to define
170      * a Velocity.properties file, therefore using Velocity's defaults
171      * only.
172      * @param templatePath
173      */
174 
175     public void setTemplatePath(File templatePath)
176      {
177          try
178          {
179              this.templatePath = templatePath.getCanonicalPath();
180          }
181          catch (java.io.IOException ioe)
182          {
183              throw new BuildException(ioe);
184          }
185      }
186 
187     /**
188      * Allow people to set the path to the velocity.properties file
189      * This file is found relative to the path where the JVM was run.
190      * For example, if build.sh was executed in the ./build directory,
191      * then the path would be relative to this directory.
192      * This is optional based on the setting of setTemplatePath().
193      * @param velocityPropertiesFile
194      */
195     public void setVelocityPropertiesFile(File velocityPropertiesFile)
196     {
197         this.velocityPropertiesFile = velocityPropertiesFile;
198     }
199 
200     /**
201      * Turn on/off last modified checking. by default, it is on.
202      * @param lastmod
203      */
204     public void setLastModifiedCheck(String lastmod)
205     {
206         if (lastmod.equalsIgnoreCase("false") || lastmod.equalsIgnoreCase("no")
207                 || lastmod.equalsIgnoreCase("off"))
208         {
209             this.lastModifiedCheck = false;
210         }
211     }
212 
213     /**
214      * Main body of the application
215      * @throws BuildException
216      */
217     public void execute () throws BuildException
218     {
219         DirectoryScanner scanner;
220         String[]         list;
221 
222         if (baseDir == null)
223         {
224             baseDir = project.resolveFile(".");
225         }
226         if (destDir == null )
227         {
228             String msg = "destdir attribute must be set!";
229             throw new BuildException(msg);
230         }
231         if (style == null)
232         {
233             throw new BuildException("style attribute must be set!");
234         }
235 
236         if (velocityPropertiesFile == null)
237         {
238             velocityPropertiesFile = new File("velocity.properties");
239         }
240 
241         /*
242          * If the props file doesn't exist AND a templatePath hasn't
243          * been defined, then throw the exception.
244          */
245         if ( !velocityPropertiesFile.exists() && templatePath == null )
246         {
247             throw new BuildException ("No template path and could not " +
248                 "locate velocity.properties file: " +
249                 velocityPropertiesFile.getAbsolutePath());
250         }
251 
252         log("Transforming into: " + destDir.getAbsolutePath(), Project.MSG_INFO);
253 
254         // projectFile relative to baseDir
255         if (projectAttribute != null && projectAttribute.length() > 0)
256         {
257             projectFile = new File(baseDir, projectAttribute);
258             if (projectFile.exists())
259             {
260                 projectFileLastModified = projectFile.lastModified();
261             }
262             else
263             {
264                 log ("Project file is defined, but could not be located: " +
265                     projectFile.getAbsolutePath(), Project.MSG_INFO );
266                 projectFile = null;
267             }
268         }
269 
270         Document projectDocument = null;
271         try
272         {
273             if ( velocityPropertiesFile.exists() )
274             {
275                 String file = velocityPropertiesFile.getAbsolutePath();
276                 ExtendedProperties config = new ExtendedProperties(file);
277                 ve.setExtendedProperties(config);
278             }
279 
280             // override the templatePath if it exists
281             if (templatePath != null && templatePath.length() > 0)
282             {
283                 ve.setProperty( RuntimeConstants.FILE_RESOURCE_LOADER_PATH,
284                     templatePath);
285             }
286 
287             ve.init();
288 
289             // get the last modification of the VSL stylesheet
290             styleSheetLastModified = ve.getTemplate( style ).getLastModified();
291 
292             // Build the Project file document
293             if (projectFile != null)
294             {
295                 projectDocument = builder.build(projectFile);
296             }
297         }
298         catch (Exception e)
299         {
300             log("Error: " + e.toString(), Project.MSG_INFO);
301             throw new BuildException(e);
302         }
303 
304         // find the files/directories
305         scanner = getDirectoryScanner(baseDir);
306 
307         // get a list of files to work on
308         list = scanner.getIncludedFiles();
309         for (int i = 0;i < list.length; ++i)
310         {
311             process(list[i], projectDocument );
312         }
313 
314     }
315 
316     /**
317      * Process an XML file using Velocity
318      */
319     private void process(String xmlFile, Document projectDocument)
320         throws BuildException
321     {
322         File   outFile=null;
323         File   inFile=null;
324         Writer writer = null;
325         try
326         {
327             // the current input file relative to the baseDir
328             inFile = new File(baseDir,xmlFile);
329             // the output file relative to basedir
330             outFile = new File(destDir,
331                             xmlFile.substring(0,
332                             xmlFile.lastIndexOf('.')) + extension);
333 
334             // only process files that have changed
335             if (lastModifiedCheck == false ||
336                     (inFile.lastModified() > outFile.lastModified() ||
337                     styleSheetLastModified > outFile.lastModified() ||
338                     projectFileLastModified > outFile.lastModified() ||
339                     userContextsModifed(outFile.lastModified())))
340             {
341                 ensureDirectoryFor( outFile );
342 
343                 //-- command line status
344                 log("Input:  " + xmlFile, Project.MSG_INFO );
345 
346                 // Build the JDOM Document
347                 Document root = builder.build(inFile);
348 
349                 // Shove things into the Context
350                 VelocityContext context = new VelocityContext();
351 
352                 /*
353                  *  get the property TEMPLATE_ENCODING
354                  *  we know it's a string...
355                  */
356                 String encoding = (String) ve.getProperty( RuntimeConstants.OUTPUT_ENCODING );
357                 if (encoding == null || encoding.length() == 0
358                     || encoding.equals("8859-1") || encoding.equals("8859_1"))
359                 {
360                     encoding = "ISO-8859-1";
361                 }
362 
363                 Format f = Format.getRawFormat();
364                 f.setEncoding(encoding);
365 
366                 OutputWrapper ow = new OutputWrapper(f);
367 
368                 context.put ("root", root.getRootElement());
369                 context.put ("xmlout", ow );
370                 context.put ("relativePath", getRelativePath(xmlFile));
371                 context.put ("treeWalk", new TreeWalker());
372                 context.put ("xpath", new XPathTool() );
373                 context.put ("escape", new Escape() );
374                 context.put ("date", new java.util.Date() );
375 
376                 /**
377                  * only put this into the context if it exists.
378                  */
379                 if (projectDocument != null)
380                 {
381                     context.put ("project", projectDocument.getRootElement());
382                 }
383 
384                 /**
385                  *  Add the user subcontexts to the to context
386                  */
387                 for (Iterator iter = contexts.iterator(); iter.hasNext();)
388                 {
389                     Context subContext = (Context) iter.next();
390                     if (subContext == null)
391                     {
392                         throw new BuildException("Found an undefined SubContext!");
393                     }
394 
395                     if (subContext.getContextDocument() == null)
396                     {
397                         throw new BuildException("Could not build a subContext for " + subContext.getName());
398                     }
399 
400                     context.put(subContext.getName(), subContext
401                             .getContextDocument().getRootElement());
402                 }
403 
404                 /**
405                  * Process the VSL template with the context and write out
406                  * the result as the outFile.
407                  */
408                 writer = new BufferedWriter(new OutputStreamWriter(
409                                             new FileOutputStream(outFile),
410                                                 encoding));
411 
412                 /**
413                  * get the template to process
414                  */
415                 Template template = ve.getTemplate(style);
416                 template.merge(context, writer);
417 
418                 log("Output: " + outFile, Project.MSG_INFO );
419             }
420         }
421         catch (JDOMException e)
422         {
423             outFile.delete();
424 
425             if (e.getCause() != null)
426             {
427                 Throwable rootCause = e.getCause();
428                 if (rootCause instanceof SAXParseException)
429                 {
430                     System.out.println("");
431                     System.out.println("Error: " + rootCause.getMessage());
432                     System.out.println(
433                         "       Line: " +
434                             ((SAXParseException)rootCause).getLineNumber() +
435                         " Column: " +
436                             ((SAXParseException)rootCause).getColumnNumber());
437                     System.out.println("");
438                 }
439                 else
440                 {
441                     rootCause.printStackTrace();
442                 }
443             }
444             else
445             {
446                 e.printStackTrace();
447             }
448         }
449         catch (Throwable e)
450         {
451             if (outFile != null)
452             {
453                 outFile.delete();
454             }
455             e.printStackTrace();
456         }
457         finally
458         {
459             if (writer != null)
460             {
461                 try
462                 {
463                     writer.flush();
464                 }
465                 catch (IOException e)
466                 {
467                     // Do nothing
468                 }
469 
470                 try
471                 {
472                     writer.close();
473                 }
474                 catch (IOException e)
475                 {
476                     // Do nothing
477                 }
478             }
479         }
480     }
481 
482     /**
483      * Hacky method to figure out the relative path
484      * that we are currently in. This is good for getting
485      * the relative path for images and anchor's.
486      */
487     private String getRelativePath(String file)
488     {
489         if (file == null || file.length()==0)
490             return "";
491         StringTokenizer st = new StringTokenizer(file, "/\\");
492         // needs to be -1 cause ST returns 1 even if there are no matches. huh?
493         int slashCount = st.countTokens() - 1;
494         StringBuffer sb = new StringBuffer();
495         for (int i=0;i<slashCount ;i++ )
496         {
497             sb.append ("../");
498         }
499 
500         if (sb.toString().length() > 0)
501         {
502             return StringUtils.chop(sb.toString(), 1);
503         }
504 
505         return ".";
506     }
507 
508     /**
509      * create directories as needed
510      */
511     private void ensureDirectoryFor( File targetFile ) throws BuildException
512     {
513         File directory = new File( targetFile.getParent() );
514         if (!directory.exists())
515         {
516             if (!directory.mkdirs())
517             {
518                 throw new BuildException("Unable to create directory: "
519                                          + directory.getAbsolutePath() );
520             }
521         }
522     }
523 
524 
525     /**
526      * Check to see if user context is modified.
527      */
528     private boolean userContextsModifed(long lastModified)
529     {
530         for (Iterator iter = contexts.iterator(); iter.hasNext();)
531         {
532             AnakiaTask.Context ctx = (AnakiaTask.Context) iter.next();
533             if(ctx.getLastModified() > lastModified)
534             {
535                 return true;
536             }
537         }
538         return false;
539     }
540 
541     /**
542      * Create a new context.
543      * @return A new context.
544      */
545     public Context createContext()
546     {
547         Context context = new Context();
548         contexts.add(context);
549         return context;
550     }
551 
552 
553     /**
554      * A context implementation that loads all values from an XML file.
555      */
556     public class Context
557     {
558 
559         private String name;
560         private Document contextDoc = null;
561         private String file;
562 
563         /**
564          * Public constructor.
565          */
566         public Context()
567         {
568         }
569 
570         /**
571          * Get the name of the context.
572          * @return The name of the context.
573          */
574         public String getName()
575         {
576             return name;
577         }
578 
579         /**
580          * Set the name of the context.
581          * @param name
582          *
583          * @throws IllegalArgumentException if a reserved word is used as a
584          * name, specifically any of "relativePath", "treeWalk", "xpath",
585          * "escape", "date", or "project"
586          */
587         public void setName(String name)
588         {
589             if (name.equals("relativePath") ||
590                     name.equals("treeWalk") ||
591                     name.equals("xpath") ||
592                     name.equals("escape") ||
593                     name.equals("date") ||
594                     name.equals("project"))
595             {
596 
597                     throw new IllegalArgumentException("Context name '" + name
598                             + "' is reserved by Anakia");
599             }
600 
601             this.name = name;
602         }
603 
604         /**
605          * Build the context based on a file path.
606          * @param file
607          */
608         public void setFile(String file)
609         {
610             this.file = file;
611         }
612 
613         /**
614          * Retrieve the time the source file was last modified.
615          * @return The time the source file was last modified.
616          */
617         public long getLastModified()
618         {
619             return new File(baseDir, file).lastModified();
620         }
621 
622         /**
623          * Retrieve the context document object.
624          * @return The context document object.
625          */
626         public Document getContextDocument()
627         {
628             if (contextDoc == null)
629             {
630                 File contextFile = new File(baseDir, file);
631 
632                 try
633                 {
634                     contextDoc = builder.build(contextFile);
635                 }
636                 catch (Exception e)
637                 {
638                     throw new BuildException(e);
639                 }
640             }
641             return contextDoc;
642         }
643     }
644 
645 }