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