1 package org.apache.velocity.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 501574 2007-01-30 21:32:26Z henning $
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 }