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