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