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