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