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