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