View Javadoc
1   package org.apache.maven.doxia.siterenderer;
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.BufferedReader;
23  import java.io.File;
24  import java.io.FileNotFoundException;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.LineNumberReader;
29  import java.io.OutputStream;
30  import java.io.Reader;
31  import java.io.StringReader;
32  import java.io.StringWriter;
33  import java.io.UnsupportedEncodingException;
34  import java.io.Writer;
35  import java.net.MalformedURLException;
36  import java.net.URL;
37  import java.net.URLClassLoader;
38  import java.text.DateFormat;
39  import java.text.SimpleDateFormat;
40  import java.util.Arrays;
41  import java.util.Collection;
42  import java.util.Collections;
43  import java.util.Date;
44  import java.util.Enumeration;
45  import java.util.Iterator;
46  import java.util.LinkedHashMap;
47  import java.util.LinkedList;
48  import java.util.List;
49  import java.util.Locale;
50  import java.util.Map;
51  import java.util.Properties;
52  import java.util.zip.ZipEntry;
53  import java.util.zip.ZipException;
54  import java.util.zip.ZipFile;
55  
56  import org.apache.commons.lang.ArrayUtils;
57  import org.apache.maven.artifact.versioning.ArtifactVersion;
58  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
59  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
60  import org.apache.maven.artifact.versioning.Restriction;
61  import org.apache.maven.artifact.versioning.VersionRange;
62  import org.apache.maven.doxia.Doxia;
63  import org.apache.maven.doxia.logging.PlexusLoggerWrapper;
64  import org.apache.maven.doxia.parser.ParseException;
65  import org.apache.maven.doxia.parser.Parser;
66  import org.apache.maven.doxia.parser.manager.ParserNotFoundException;
67  import org.apache.maven.doxia.site.decoration.DecorationModel;
68  import org.apache.maven.doxia.site.decoration.PublishDate;
69  import org.apache.maven.doxia.site.skin.SkinModel;
70  import org.apache.maven.doxia.site.skin.io.xpp3.SkinXpp3Reader;
71  import org.apache.maven.doxia.parser.module.ParserModule;
72  import org.apache.maven.doxia.parser.module.ParserModuleManager;
73  import org.apache.maven.doxia.parser.module.ParserModuleNotFoundException;
74  import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
75  import org.apache.maven.doxia.util.XmlValidator;
76  import org.apache.velocity.Template;
77  import org.apache.velocity.context.Context;
78  import org.apache.velocity.tools.Scope;
79  import org.apache.velocity.tools.ToolManager;
80  import org.apache.velocity.tools.config.EasyFactoryConfiguration;
81  import org.apache.velocity.tools.generic.AlternatorTool;
82  import org.apache.velocity.tools.generic.ClassTool;
83  import org.apache.velocity.tools.generic.ComparisonDateTool;
84  import org.apache.velocity.tools.generic.ContextTool;
85  import org.apache.velocity.tools.generic.ConversionTool;
86  import org.apache.velocity.tools.generic.DisplayTool;
87  import org.apache.velocity.tools.generic.EscapeTool;
88  import org.apache.velocity.tools.generic.FieldTool;
89  import org.apache.velocity.tools.generic.LinkTool;
90  import org.apache.velocity.tools.generic.LoopTool;
91  import org.apache.velocity.tools.generic.MathTool;
92  import org.apache.velocity.tools.generic.NumberTool;
93  import org.apache.velocity.tools.generic.RenderTool;
94  import org.apache.velocity.tools.generic.ResourceTool;
95  import org.apache.velocity.tools.generic.SortTool;
96  import org.apache.velocity.tools.generic.XmlTool;
97  import org.codehaus.plexus.PlexusContainer;
98  import org.codehaus.plexus.component.annotations.Component;
99  import org.codehaus.plexus.component.annotations.Requirement;
100 import org.codehaus.plexus.i18n.I18N;
101 import org.codehaus.plexus.logging.AbstractLogEnabled;
102 import org.codehaus.plexus.util.DirectoryScanner;
103 import org.codehaus.plexus.util.FileUtils;
104 import org.codehaus.plexus.util.IOUtil;
105 import org.codehaus.plexus.util.Os;
106 import org.codehaus.plexus.util.PathTool;
107 import org.codehaus.plexus.util.PropertyUtils;
108 import org.codehaus.plexus.util.ReaderFactory;
109 import org.codehaus.plexus.util.StringUtils;
110 import org.codehaus.plexus.util.WriterFactory;
111 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
112 import org.codehaus.plexus.velocity.SiteResourceLoader;
113 import org.codehaus.plexus.velocity.VelocityComponent;
114 
115 /**
116  * <p>DefaultSiteRenderer class.</p>
117  *
118  * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
119  * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
120  * @version $Id: DefaultSiteRenderer.java 1729686 2016-02-10 18:38:24Z michaelo $
121  * @since 1.0
122  */
123 @Component( role = Renderer.class )
124 public class DefaultSiteRenderer
125     extends AbstractLogEnabled
126     implements Renderer
127 {
128     // ----------------------------------------------------------------------
129     // Requirements
130     // ----------------------------------------------------------------------
131 
132     @Requirement( hint = "doxia-site-renderer" ) // DOXIA-147: dedicated component configuration hand-written
133                                                  // in component.xml
134     private VelocityComponent velocity;
135 
136     @Requirement
137     private ParserModuleManager parserModuleManager;
138 
139     @Requirement
140     private Doxia doxia;
141 
142     @Requirement
143     private I18N i18n;
144 
145     @Requirement
146     private PlexusContainer plexus;
147 
148     private static final String RESOURCE_DIR = "org/apache/maven/doxia/siterenderer/resources";
149 
150     private static final String DEFAULT_TEMPLATE = RESOURCE_DIR + "/default-site.vm";
151 
152     private static final String SKIN_TEMPLATE_LOCATION = "META-INF/maven/site.vm";
153 
154     // ----------------------------------------------------------------------
155     // Renderer implementation
156     // ----------------------------------------------------------------------
157 
158     /** {@inheritDoc} */
159     public Map<String, DocumentRenderer> locateDocumentFiles( SiteRenderingContext siteRenderingContext )
160             throws IOException, RendererException
161     {
162         Map<String, DocumentRenderer> files = new LinkedHashMap<String, DocumentRenderer>();
163         Map<String, String> moduleExcludes = siteRenderingContext.getModuleExcludes();
164 
165         // look in every site directory (in general src/site or target/generated-site)
166         for ( File siteDirectory : siteRenderingContext.getSiteDirectories() )
167         {
168             if ( siteDirectory.exists() )
169             {
170                 Collection<ParserModule> modules = parserModuleManager.getParserModules();
171                 // use every Doxia parser module
172                 for ( ParserModule module : modules )
173                 {
174                     File moduleBasedir = new File( siteDirectory, module.getSourceDirectory() );
175 
176                     String excludes = ( moduleExcludes == null ) ? null : moduleExcludes.get( module.getParserId() );
177 
178                     addModuleFiles( moduleBasedir, module, excludes, files );
179                 }
180             }
181         }
182 
183         // look in specific modules directories (used for old Maven 1.x site layout: xdoc and fml docs in /xdocs)
184         for ( ExtraDoxiaModuleReference module : siteRenderingContext.getModules() )
185         {
186             try
187             {
188                 ParserModule parserModule = parserModuleManager.getParserModule( module.getParserId() );
189 
190                 String excludes = ( moduleExcludes == null ) ? null : moduleExcludes.get( module.getParserId() );
191 
192                 addModuleFiles( module.getBasedir(), parserModule, excludes, files );
193             }
194             catch ( ParserModuleNotFoundException e )
195             {
196                 throw new RendererException( "Unable to find module: " + e.getMessage(), e );
197             }
198         }
199         return files;
200     }
201 
202     private List<String> filterExtensionIgnoreCase( List<String> fileNames, String extension )
203     {
204         List<String> filtered = new LinkedList<String>( fileNames );
205         for ( Iterator<String> it = filtered.iterator(); it.hasNext(); )
206         {
207             String name = it.next();
208 
209             // Take care of extension case
210             if ( !endsWithIgnoreCase( name, extension ) )
211             {
212                 it.remove();
213             }
214         }
215         return filtered;
216     }
217 
218     private void addModuleFiles( File moduleBasedir, ParserModule module, String excludes,
219                                  Map<String, DocumentRenderer> files )
220             throws IOException, RendererException
221     {
222         if ( !moduleBasedir.exists() || ArrayUtils.isEmpty( module.getExtensions() ) )
223         {
224             return;
225         }
226 
227         List<String> allFiles = FileUtils.getFileNames( moduleBasedir, "**/*.*", excludes, false );
228 
229         for ( String extension : module.getExtensions() )
230         {
231             String fullExtension = "." + extension;
232 
233             List<String> docs = filterExtensionIgnoreCase( allFiles, fullExtension );
234 
235             // *.<extension>.vm
236             List<String> velocityFiles = filterExtensionIgnoreCase( allFiles, fullExtension + ".vm" );
237 
238             docs.addAll( velocityFiles );
239 
240             for ( String doc : docs )
241             {
242                 RenderingContext context =
243                         new RenderingContext( moduleBasedir, doc, module.getParserId(), extension );
244 
245                 // TODO: DOXIA-111: we need a general filter here that knows how to alter the context
246                 if ( endsWithIgnoreCase( doc, ".vm" ) )
247                 {
248                     context.setAttribute( "velocity", "true" );
249                 }
250 
251                 String key = context.getOutputName();
252                 key = StringUtils.replace( key, "\\", "/" );
253 
254                 if ( files.containsKey( key ) )
255                 {
256                     DocumentRenderer renderer = files.get( key );
257 
258                     RenderingContext originalContext = renderer.getRenderingContext();
259 
260                     File originalDoc = new File( originalContext.getBasedir(), originalContext.getInputName() );
261 
262                     throw new RendererException( "File '" + module.getSourceDirectory() + File.separator + doc
263                         + "' clashes with existing '" + originalDoc + "'." );
264                 }
265                 // -----------------------------------------------------------------------
266                 // Handle key without case differences
267                 // -----------------------------------------------------------------------
268                 for ( Map.Entry<String, DocumentRenderer> entry : files.entrySet() )
269                 {
270                     if ( entry.getKey().equalsIgnoreCase( key ) )
271                     {
272                         RenderingContext originalContext = entry.getValue().getRenderingContext();
273 
274                         File originalDoc = new File( originalContext.getBasedir(), originalContext.getInputName() );
275 
276                         if ( Os.isFamily( Os.FAMILY_WINDOWS ) )
277                         {
278                             throw new RendererException( "File '" + module.getSourceDirectory() + File.separator
279                                 + doc + "' clashes with existing '" + originalDoc + "'." );
280                         }
281 
282                         if ( getLogger().isWarnEnabled() )
283                         {
284                             getLogger().warn( "File '" + module.getSourceDirectory() + File.separator + doc
285                                 + "' could clash with existing '" + originalDoc + "'." );
286                         }
287                     }
288                 }
289 
290                 files.put( key, new DoxiaDocumentRenderer( context ) );
291             }
292         }
293     }
294 
295     /** {@inheritDoc} */
296     public void render( Collection<DocumentRenderer> documents, SiteRenderingContext siteRenderingContext,
297                         File outputDirectory )
298         throws RendererException, IOException
299     {
300         for ( DocumentRenderer docRenderer : documents )
301         {
302             RenderingContext renderingContext = docRenderer.getRenderingContext();
303 
304             File outputFile = new File( outputDirectory, docRenderer.getOutputName() );
305 
306             File inputFile = new File( renderingContext.getBasedir(), renderingContext.getInputName() );
307 
308             boolean modified = !outputFile.exists() || ( inputFile.lastModified() > outputFile.lastModified() )
309                 || ( siteRenderingContext.getDecoration().getLastModified() > outputFile.lastModified() );
310 
311             if ( modified || docRenderer.isOverwrite() )
312             {
313                 if ( !outputFile.getParentFile().exists() )
314                 {
315                     outputFile.getParentFile().mkdirs();
316                 }
317 
318                 if ( getLogger().isDebugEnabled() )
319                 {
320                     getLogger().debug( "Generating " + outputFile );
321                 }
322 
323                 Writer writer = null;
324                 try
325                 {
326                     if ( !docRenderer.isExternalReport() )
327                     {
328                         writer = WriterFactory.newWriter( outputFile, siteRenderingContext.getOutputEncoding() );
329                     }
330                     docRenderer.renderDocument( writer, this, siteRenderingContext );
331                 }
332                 finally
333                 {
334                     IOUtil.close( writer );
335                 }
336             }
337             else
338             {
339                 if ( getLogger().isDebugEnabled() )
340                 {
341                     getLogger().debug( inputFile + " unchanged, not regenerating..." );
342                 }
343             }
344         }
345     }
346 
347     /** {@inheritDoc} */
348     public void renderDocument( Writer writer, RenderingContext renderingContext, SiteRenderingContext siteContext )
349             throws RendererException, FileNotFoundException, UnsupportedEncodingException
350     {
351         SiteRendererSink sink = new SiteRendererSink( renderingContext );
352 
353         File doc = new File( renderingContext.getBasedir(), renderingContext.getInputName() );
354 
355         Reader reader = null;
356         try
357         {
358             String resource = doc.getAbsolutePath();
359 
360             Parser parser = doxia.getParser( renderingContext.getParserId() );
361             // DOXIASITETOOLS-146 don't render comments from source markup
362             parser.setEmitComments( false );
363 
364             // TODO: DOXIA-111: the filter used here must be checked generally.
365             if ( renderingContext.getAttribute( "velocity" ) != null )
366             {
367                 getLogger().debug( "Processing Velocity for " + renderingContext.getInputName() );
368                 try
369                 {
370                     SiteResourceLoader.setResource( resource );
371 
372                     Context vc = createDocumentVelocityContext( renderingContext, siteContext );
373 
374                     StringWriter sw = new StringWriter();
375 
376                     velocity.getEngine().mergeTemplate( resource, siteContext.getInputEncoding(), vc, sw );
377 
378                     String doxiaContent = sw.toString();
379 
380                     if ( siteContext.getProcessedContentOutput() != null )
381                     {
382                         // save Velocity processing result, ie the Doxia content that will be parsed after
383                         if ( !siteContext.getProcessedContentOutput().exists() )
384                         {
385                             siteContext.getProcessedContentOutput().mkdirs();
386                         }
387 
388                         String input = renderingContext.getInputName();
389                         File outputFile = new File( siteContext.getProcessedContentOutput(),
390                                                     input.substring( 0, input.length() - 3 ) );
391 
392                         File outputParent = outputFile.getParentFile();
393                         if ( !outputParent.exists() )
394                         {
395                             outputParent.mkdirs();
396                         }
397 
398                         FileUtils.fileWrite( outputFile, siteContext.getInputEncoding(), doxiaContent );
399                     }
400 
401                     reader = new StringReader( doxiaContent );
402                 }
403                 catch ( Exception e )
404                 {
405                     if ( getLogger().isDebugEnabled() )
406                     {
407                         getLogger().error( "Error parsing " + resource + " as a velocity template, using as text.", e );
408                     }
409                     else
410                     {
411                         getLogger().error( "Error parsing " + resource + " as a velocity template, using as text." );
412                     }
413                 }
414 
415                 if ( parser.getType() == Parser.XML_TYPE && siteContext.isValidate() )
416                 {
417                     reader = validate( reader, resource );
418                 }
419             }
420             else
421             {
422                 switch ( parser.getType() )
423                 {
424                     case Parser.XML_TYPE:
425                         reader = ReaderFactory.newXmlReader( doc );
426                         if ( siteContext.isValidate() )
427                         {
428                             reader = validate( reader, resource );
429                         }
430                         break;
431 
432                     case Parser.TXT_TYPE:
433                     case Parser.UNKNOWN_TYPE:
434                     default:
435                         reader = ReaderFactory.newReader( doc, siteContext.getInputEncoding() );
436                 }
437             }
438             sink.enableLogging( new PlexusLoggerWrapper( getLogger() ) );
439 
440             if ( reader == null ) // can happen if velocity throws above
441             {
442                 throw new RendererException( "Error getting a parser for '" + doc + "'" );
443             }
444             doxia.parse( reader, renderingContext.getParserId(), sink );
445         }
446         catch ( ParserNotFoundException e )
447         {
448             throw new RendererException( "Error getting a parser for '" + doc + "': " + e.getMessage(), e );
449         }
450         catch ( ParseException e )
451         {
452             throw new RendererException( "Error parsing '"
453                     + doc + "': line [" + e.getLineNumber() + "] " + e.getMessage(), e );
454         }
455         catch ( IOException e )
456         {
457             throw new RendererException( "IOException when processing '" + doc + "'", e );
458         }
459         finally
460         {
461             sink.flush();
462 
463             sink.close();
464 
465             IOUtil.close( reader );
466         }
467 
468         generateDocument( writer, sink, siteContext );
469     }
470 
471     /**
472      * Creates a Velocity Context with all generic tools configured wit the site rendering context.
473      *
474      * @param siteRenderingContext the site rendering context
475      * @return a Velocity tools managed context
476      */
477     protected Context createToolManagedVelocityContext( SiteRenderingContext siteRenderingContext )
478     {
479         Locale locale = siteRenderingContext.getLocale();
480         String dateFormat = siteRenderingContext.getDecoration().getPublishDate().getFormat();
481 
482         EasyFactoryConfiguration config = new EasyFactoryConfiguration( false );
483         config.property( "safeMode", Boolean.FALSE );
484         config.toolbox( Scope.REQUEST )
485             .tool( ContextTool.class )
486             .tool( LinkTool.class )
487             .tool( LoopTool.class )
488             .tool( RenderTool.class );
489         config.toolbox( Scope.APPLICATION ).property( "locale", locale )
490             .tool( AlternatorTool.class )
491             .tool( ClassTool.class )
492             .tool( ComparisonDateTool.class ).property( "format", dateFormat )
493             .tool( ConversionTool.class ).property( "dateFormat", dateFormat )
494             .tool( DisplayTool.class )
495             .tool( EscapeTool.class )
496             .tool( FieldTool.class )
497             .tool( MathTool.class )
498             .tool( NumberTool.class )
499             .tool( ResourceTool.class ).property( "bundles", new String[] { "site-renderer" } )
500             .tool( SortTool.class )
501             .tool( XmlTool.class );
502 
503         ToolManager manager = new ToolManager( false, false );
504         manager.configure( config );
505 
506         return manager.createContext();
507     }
508 
509     /**
510      * Create a Velocity Context for a Doxia document, containing every information about rendered document.
511      *
512      * @param sink the site renderer sink for the document
513      * @param siteRenderingContext the site rendering context
514      * @return
515      */
516     protected Context createDocumentVelocityContext( RenderingContext renderingContext,
517                                                      SiteRenderingContext siteRenderingContext )
518     {
519         Context context = createToolManagedVelocityContext( siteRenderingContext );
520         // ----------------------------------------------------------------------
521         // Data objects
522         // ----------------------------------------------------------------------
523 
524         context.put( "relativePath", renderingContext.getRelativePath() );
525 
526         String currentFileName = renderingContext.getOutputName().replace( '\\', '/' );
527         context.put( "currentFileName", currentFileName );
528 
529         context.put( "alignedFileName", PathTool.calculateLink( currentFileName, renderingContext.getRelativePath() ) );
530 
531         context.put( "decoration", siteRenderingContext.getDecoration() );
532 
533         Locale locale = siteRenderingContext.getLocale();
534         context.put( "locale", locale );
535         context.put( "supportedLocales", Collections.unmodifiableList( siteRenderingContext.getSiteLocales() ) );
536 
537         context.put( "currentDate", new Date() );
538         SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMdd" );
539         context.put( "dateRevision", sdf.format( new Date() ) );
540 
541         context.put( "publishDate", siteRenderingContext.getPublishDate() );
542 
543         PublishDate publishDate = siteRenderingContext.getDecoration().getPublishDate();
544         DateFormat dateFormat = new SimpleDateFormat( publishDate.getFormat(), locale );
545         context.put( "dateFormat", dateFormat );
546 
547         // doxiaSiteRendererVersion
548         InputStream inputStream = this.getClass().getResourceAsStream( "/META-INF/"
549             + "maven/org.apache.maven.doxia/doxia-site-renderer/pom.properties" );
550         Properties properties = PropertyUtils.loadProperties( inputStream );
551         if ( inputStream == null )
552         {
553             getLogger().debug( "pom.properties for doxia-site-renderer could not be found." );
554         }
555         else if ( properties == null )
556         {
557             getLogger().debug( "Failed to load pom.properties, so doxiaVersion is not available"
558                 + " in the Velocity context." );
559         }
560         else
561         {
562             context.put( "doxiaSiteRendererVersion", properties.getProperty( "version" ) );
563         }
564 
565         // Add user properties
566         Map<String, ?> templateProperties = siteRenderingContext.getTemplateProperties();
567 
568         if ( templateProperties != null )
569         {
570             for ( Map.Entry<String, ?> entry : templateProperties.entrySet() )
571             {
572                 context.put( entry.getKey(), entry.getValue() );
573             }
574         }
575 
576         // ----------------------------------------------------------------------
577         // Tools
578         // ----------------------------------------------------------------------
579 
580         context.put( "PathTool", new PathTool() );
581 
582         context.put( "FileUtils", new FileUtils() );
583 
584         context.put( "StringUtils", new StringUtils() );
585 
586         context.put( "i18n", i18n );
587 
588         context.put( "plexus", plexus );
589         return context;
590     }
591 
592     /**
593      * Create a Velocity Context for the site template decorating the document. In addition to all the informations
594      * from the document, this context contains data gathered in {@link SiteRendererSink} during document rendering.
595      *
596      * @param siteRendererSink the site renderer sink for the document
597      * @param siteRenderingContext the site rendering context
598      * @return
599      */
600     protected Context createSiteTemplateVelocityContext( SiteRendererSink siteRendererSink,
601                                                          SiteRenderingContext siteRenderingContext )
602     {
603         // first get the context from document
604         Context context = createDocumentVelocityContext( siteRendererSink.getRenderingContext(), siteRenderingContext );
605 
606         // then add data objects from rendered document
607 
608         // Add infos from document
609         context.put( "authors", siteRendererSink.getAuthors() );
610 
611         context.put( "shortTitle", siteRendererSink.getTitle() );
612 
613         // DOXIASITETOOLS-70: Prepend the project name to the title, if any
614         String title = "";
615         if ( siteRenderingContext.getDecoration() != null
616                 && siteRenderingContext.getDecoration().getName() != null )
617         {
618             title = siteRenderingContext.getDecoration().getName();
619         }
620         else if ( siteRenderingContext.getDefaultWindowTitle() != null )
621         {
622             title = siteRenderingContext.getDefaultWindowTitle();
623         }
624 
625         if ( title.length() > 0 )
626         {
627             title += " &#x2013; "; // Symbol Name: En Dash, Html Entity: &ndash;
628         }
629         title += siteRendererSink.getTitle();
630 
631         context.put( "title", title );
632 
633         context.put( "headContent", siteRendererSink.getHead() );
634 
635         context.put( "bodyContent", siteRendererSink.getBody() );
636 
637         SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMdd" );
638         if ( StringUtils.isNotEmpty( siteRendererSink.getDate() ) )
639         {
640             String documentDate = siteRendererSink.getDate();
641 
642             try
643             {
644                 // we support only ISO 8601 date
645                 Date creationDate = new SimpleDateFormat( "yyyy-MM-dd" ).parse( documentDate );
646 
647                 context.put( "creationDate", creationDate );
648                 context.put( "dateCreation", sdf.format( creationDate ) );
649             }
650             catch ( java.text.ParseException e )
651             {
652                 getLogger().warn( "Could not parse date '" + documentDate + "' from "
653                     + siteRendererSink.getRenderingContext().getInputName()
654                     + " (expected yyyy-MM-dd format), ignoring!" );
655             }
656         }
657 
658         return context;
659     }
660 
661     /** {@inheritDoc} */
662     public void generateDocument( Writer writer, SiteRendererSink sink, SiteRenderingContext siteRenderingContext )
663             throws RendererException
664     {
665         String templateName = siteRenderingContext.getTemplateName();
666 
667         getLogger().debug( "Processing Velocity for template " + templateName + " on "
668             + sink.getRenderingContext().getInputName() );
669 
670         Context context = createSiteTemplateVelocityContext( sink, siteRenderingContext );
671 
672         ClassLoader old = null;
673 
674         if ( siteRenderingContext.getTemplateClassLoader() != null )
675         {
676             // -------------------------------------------------------------------------
677             // If no template classloader was set we'll just use the context classloader
678             // -------------------------------------------------------------------------
679 
680             old = Thread.currentThread().getContextClassLoader();
681 
682             Thread.currentThread().setContextClassLoader( siteRenderingContext.getTemplateClassLoader() );
683         }
684 
685         try
686         {
687             Template template;
688 
689             try
690             {
691                 SkinModel skinModel = siteRenderingContext.getSkinModel();
692                 String encoding = ( skinModel == null ) ? null : skinModel.getEncoding();
693 
694                 template = ( encoding == null ) ? velocity.getEngine().getTemplate( templateName )
695                                 : velocity.getEngine().getTemplate( templateName, encoding );
696             }
697             catch ( Exception e )
698             {
699                 throw new RendererException( "Could not find the site decoration template '" + templateName + "'", e );
700             }
701 
702             try
703             {
704                 template.merge( context, writer );
705             }
706             catch ( Exception e )
707             {
708                 throw new RendererException( "Error while merging site decoration template.", e );
709             }
710         }
711         finally
712         {
713             IOUtil.close( writer );
714 
715             if ( old != null )
716             {
717                 Thread.currentThread().setContextClassLoader( old );
718             }
719         }
720     }
721 
722     private SiteRenderingContext createSiteRenderingContext( Map<String, ?> attributes, DecorationModel decoration,
723                                                              String defaultWindowTitle, Locale locale )
724     {
725         SiteRenderingContext context = new SiteRenderingContext();
726 
727         context.setTemplateProperties( attributes );
728         context.setLocale( locale );
729         context.setDecoration( decoration );
730         context.setDefaultWindowTitle( defaultWindowTitle );
731 
732         return context;
733     }
734 
735     /** {@inheritDoc} */
736     public SiteRenderingContext createContextForSkin( File skinFile, Map<String, ?> attributes,
737                                                       DecorationModel decoration, String defaultWindowTitle,
738                                                       Locale locale )
739             throws IOException, RendererException
740     {
741         SiteRenderingContext context = createSiteRenderingContext( attributes, decoration, defaultWindowTitle, locale );
742 
743         context.setSkinJarFile( skinFile );
744 
745         ZipFile zipFile = getZipFile( skinFile );
746         InputStream in = null;
747 
748         try
749         {
750             if ( zipFile.getEntry( SKIN_TEMPLATE_LOCATION ) != null )
751             {
752                 context.setTemplateName( SKIN_TEMPLATE_LOCATION );
753                 context.setTemplateClassLoader( new URLClassLoader( new URL[]{skinFile.toURI().toURL()} ) );
754             }
755             else
756             {
757                 context.setTemplateName( DEFAULT_TEMPLATE );
758                 context.setTemplateClassLoader( getClass().getClassLoader() );
759                 context.setUsingDefaultTemplate( true );
760             }
761 
762             ZipEntry skinDescriptorEntry = zipFile.getEntry( SkinModel.SKIN_DESCRIPTOR_LOCATION );
763             if ( skinDescriptorEntry != null )
764             {
765                 in = zipFile.getInputStream( skinDescriptorEntry );
766 
767                 SkinModel skinModel = new SkinXpp3Reader().read( in );
768                 context.setSkinModel( skinModel );
769 
770                 String toolsPrerequisite =
771                     skinModel.getPrerequisites() == null ? null : skinModel.getPrerequisites().getDoxiaSitetools();
772 
773                 Package p = DefaultSiteRenderer.class.getPackage();
774                 String current = ( p == null ) ? null : p.getSpecificationVersion();
775 
776                 if ( StringUtils.isNotBlank( toolsPrerequisite ) && ( current != null )
777                     && !matchVersion( current, toolsPrerequisite ) )
778                 {
779                     throw new RendererException( "Cannot use skin: has " + toolsPrerequisite
780                         + " Doxia Sitetools prerequisite, but current is " + current );
781                 }
782             }
783         }
784         catch ( XmlPullParserException e )
785         {
786             throw new RendererException( "Failed to parse " + SkinModel.SKIN_DESCRIPTOR_LOCATION
787                 + " skin descriptor from " + skinFile, e );
788         }
789         finally
790         {
791             IOUtil.close( in );
792             closeZipFile( zipFile );
793         }
794 
795         return context;
796     }
797 
798     boolean matchVersion( String current, String prerequisite )
799         throws RendererException
800     {
801         try
802         {
803             ArtifactVersion v = new DefaultArtifactVersion( current );
804             VersionRange vr = VersionRange.createFromVersionSpec( prerequisite );
805 
806             boolean matched = false;
807             ArtifactVersion recommendedVersion = vr.getRecommendedVersion();
808             if ( recommendedVersion == null )
809             {
810                 List<Restriction> restrictions = vr.getRestrictions();
811                 for ( Restriction restriction : restrictions )
812                 {
813                     if ( restriction.containsVersion( v ) )
814                     {
815                         matched = true;
816                         break;
817                     }
818                 }
819             }
820             else
821             {
822                 // only singular versions ever have a recommendedVersion
823                 @SuppressWarnings( "unchecked" )
824                 int compareTo = recommendedVersion.compareTo( v );
825                 matched = ( compareTo <= 0 );
826             }
827 
828             if ( getLogger().isDebugEnabled() )
829             {
830                 getLogger().debug( "Skin doxia-sitetools prerequisite: " + prerequisite + ", current: " + current
831                     + ", matched = " + matched );
832             }
833 
834             return matched;
835         }
836         catch ( InvalidVersionSpecificationException e )
837         {
838             throw new RendererException( "Invalid skin doxia-sitetools prerequisite: " + prerequisite, e );
839         }
840     }
841 
842     /** {@inheritDoc} */
843     @Deprecated
844     public SiteRenderingContext createContextForTemplate( File templateFile, Map<String, ?> attributes,
845                                                           DecorationModel decoration, String defaultWindowTitle,
846                                                           Locale locale )
847             throws MalformedURLException
848     {
849         SiteRenderingContext context = createSiteRenderingContext( attributes, decoration, defaultWindowTitle, locale );
850 
851         context.setTemplateName( templateFile.getName() );
852         context.setTemplateClassLoader( new URLClassLoader( new URL[]{templateFile.getParentFile().toURI().toURL()} ) );
853 
854         return context;
855     }
856 
857     /** {@inheritDoc} */
858     public void copyResources( SiteRenderingContext siteRenderingContext, File resourcesDirectory,
859                                File outputDirectory )
860         throws IOException
861     {
862         throw new AssertionError( "copyResources( SiteRenderingContext, File, File ) is deprecated." );
863     }
864 
865     /** {@inheritDoc} */
866     public void copyResources( SiteRenderingContext siteRenderingContext, File outputDirectory )
867         throws IOException
868     {
869         if ( siteRenderingContext.getSkinJarFile() != null )
870         {
871             ZipFile file = getZipFile( siteRenderingContext.getSkinJarFile() );
872 
873             try
874             {
875                 for ( Enumeration<? extends ZipEntry> e = file.entries(); e.hasMoreElements(); )
876                 {
877                     ZipEntry entry = e.nextElement();
878 
879                     if ( !entry.getName().startsWith( "META-INF/" ) )
880                     {
881                         File destFile = new File( outputDirectory, entry.getName() );
882                         if ( !entry.isDirectory() )
883                         {
884                             if ( destFile.exists() )
885                             {
886                                 // don't override existing content: avoids extra rewrite with same content or extra site
887                                 // resource
888                                 continue;
889                             }
890 
891                             destFile.getParentFile().mkdirs();
892 
893                             copyFileFromZip( file, entry, destFile );
894                         }
895                         else
896                         {
897                             destFile.mkdirs();
898                         }
899                     }
900                 }
901             }
902             finally
903             {
904                 closeZipFile( file );
905             }
906         }
907 
908         if ( siteRenderingContext.isUsingDefaultTemplate() )
909         {
910             InputStream resourceList = getClass().getClassLoader()
911                     .getResourceAsStream( RESOURCE_DIR + "/resources.txt" );
912 
913             if ( resourceList != null )
914             {
915                 Reader r = null;
916                 LineNumberReader reader = null;
917                 try
918                 {
919                     r = ReaderFactory.newReader( resourceList, ReaderFactory.UTF_8 );
920                     reader = new LineNumberReader( r );
921 
922                     String line;
923 
924                     while ( ( line = reader.readLine() ) != null )
925                     {
926                         if ( line.startsWith( "#" ) || line.trim().length() == 0 )
927                         {
928                             continue;
929                         }
930 
931                         InputStream is = getClass().getClassLoader().getResourceAsStream( RESOURCE_DIR + "/" + line );
932 
933                         if ( is == null )
934                         {
935                             throw new IOException( "The resource " + line + " doesn't exist." );
936                         }
937 
938                         File outputFile = new File( outputDirectory, line );
939 
940                         if ( outputFile.exists() )
941                         {
942                             // don't override existing content: avoids extra rewrite with same content or extra site
943                             // resource
944                             continue;
945                         }
946 
947                         if ( !outputFile.getParentFile().exists() )
948                         {
949                             outputFile.getParentFile().mkdirs();
950                         }
951 
952                         OutputStream os = null;
953                         try
954                         {
955                             // for the images
956                             os = new FileOutputStream( outputFile );
957                             IOUtil.copy( is, os );
958                         }
959                         finally
960                         {
961                             IOUtil.close( os );
962                         }
963 
964                         IOUtil.close( is );
965                     }
966                 }
967                 finally
968                 {
969                     IOUtil.close( reader );
970                     IOUtil.close( r );
971                 }
972             }
973         }
974 
975         // Copy extra site resources
976         for ( File siteDirectory : siteRenderingContext.getSiteDirectories() )
977         {
978             File resourcesDirectory = new File( siteDirectory, "resources" );
979 
980             if ( resourcesDirectory != null && resourcesDirectory.exists() )
981             {
982                 copyDirectory( resourcesDirectory, outputDirectory );
983             }
984         }
985 
986         // Check for the existence of /css/site.css
987         File siteCssFile = new File( outputDirectory, "/css/site.css" );
988         if ( !siteCssFile.exists() )
989         {
990             // Create the subdirectory css if it doesn't exist, DOXIA-151
991             File cssDirectory = new File( outputDirectory, "/css/" );
992             boolean created = cssDirectory.mkdirs();
993             if ( created && getLogger().isDebugEnabled() )
994             {
995                 getLogger().debug(
996                     "The directory '" + cssDirectory.getAbsolutePath() + "' did not exist. It was created." );
997             }
998 
999             // If the file is not there - create an empty file, DOXIA-86
1000             if ( getLogger().isDebugEnabled() )
1001             {
1002                 getLogger().debug(
1003                     "The file '" + siteCssFile.getAbsolutePath() + "' does not exist. Creating an empty file." );
1004             }
1005             Writer writer = null;
1006             try
1007             {
1008                 writer = WriterFactory.newWriter( siteCssFile, siteRenderingContext.getOutputEncoding() );
1009                 //DOXIA-290...the file should not be 0 bytes.
1010                 writer.write( "/* You can override this file with your own styles */"  );
1011             }
1012             finally
1013             {
1014                 IOUtil.close( writer );
1015             }
1016         }
1017     }
1018 
1019     private static void copyFileFromZip( ZipFile file, ZipEntry entry, File destFile )
1020             throws IOException
1021     {
1022         FileOutputStream fos = new FileOutputStream( destFile );
1023 
1024         try
1025         {
1026             IOUtil.copy( file.getInputStream( entry ), fos );
1027         }
1028         finally
1029         {
1030             IOUtil.close( fos );
1031         }
1032     }
1033 
1034     /**
1035      * Copy the directory
1036      *
1037      * @param source      source file to be copied
1038      * @param destination destination file
1039      * @throws java.io.IOException if any
1040      */
1041     protected void copyDirectory( File source, File destination )
1042             throws IOException
1043     {
1044         if ( source.exists() )
1045         {
1046             DirectoryScanner scanner = new DirectoryScanner();
1047 
1048             String[] includedResources = {"**/**"};
1049 
1050             scanner.setIncludes( includedResources );
1051 
1052             scanner.addDefaultExcludes();
1053 
1054             scanner.setBasedir( source );
1055 
1056             scanner.scan();
1057 
1058             List<String> includedFiles = Arrays.asList( scanner.getIncludedFiles() );
1059 
1060             for ( String name : includedFiles )
1061             {
1062                 File sourceFile = new File( source, name );
1063 
1064                 File destinationFile = new File( destination, name );
1065 
1066                 FileUtils.copyFile( sourceFile, destinationFile );
1067             }
1068         }
1069     }
1070 
1071     private Reader validate( Reader source, String resource )
1072             throws ParseException, IOException
1073     {
1074         getLogger().debug( "Validating: " + resource );
1075 
1076         try
1077         {
1078             String content = IOUtil.toString( new BufferedReader( source ) );
1079 
1080             new XmlValidator( new PlexusLoggerWrapper( getLogger() ) ).validate( content );
1081 
1082             return new StringReader( content );
1083         }
1084         finally
1085         {
1086             IOUtil.close( source );
1087         }
1088     }
1089 
1090     // TODO replace with StringUtils.endsWithIgnoreCase() from maven-shared-utils 0.7
1091     static boolean endsWithIgnoreCase( String str, String searchStr )
1092     {
1093         if ( str.length() < searchStr.length() )
1094         {
1095             return false;
1096         }
1097 
1098         return str.regionMatches( true, str.length() - searchStr.length(), searchStr, 0, searchStr.length() );
1099     }
1100 
1101     private static ZipFile getZipFile( File file )
1102         throws IOException
1103     {
1104         if ( file == null )
1105         {
1106             throw new IOException( "Error opening ZipFile: null" );
1107         }
1108 
1109         try
1110         {
1111             // TODO: plexus-archiver, if it could do the excludes
1112             return new ZipFile( file );
1113         }
1114         catch ( ZipException ex )
1115         {
1116             IOException ioe = new IOException( "Error opening ZipFile: " + file.getAbsolutePath() );
1117             ioe.initCause( ex );
1118             throw ioe;
1119         }
1120     }
1121 
1122     private static void closeZipFile( ZipFile zipFile )
1123     {
1124         // TODO: move to plexus utils
1125         try
1126         {
1127             zipFile.close();
1128         }
1129         catch ( IOException e )
1130         {
1131             // ignore
1132         }
1133     }
1134 }