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