View Javadoc

1   package org.apache.maven.doxia.docrenderer;
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.IOException;
25  import java.io.Reader;
26  import java.io.StringReader;
27  import java.io.StringWriter;
28  import java.util.Arrays;
29  import java.util.Collection;
30  import java.util.HashMap;
31  import java.util.Iterator;
32  import java.util.LinkedHashMap;
33  import java.util.LinkedList;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Map;
37  
38  import org.apache.maven.doxia.Doxia;
39  import org.apache.maven.doxia.document.DocumentModel;
40  import org.apache.maven.doxia.document.io.xpp3.DocumentXpp3Reader;
41  import org.apache.maven.doxia.sink.Sink;
42  import org.apache.maven.doxia.parser.ParseException;
43  import org.apache.maven.doxia.parser.Parser;
44  import org.apache.maven.doxia.parser.manager.ParserNotFoundException;
45  import org.apache.maven.doxia.logging.PlexusLoggerWrapper;
46  import org.apache.maven.doxia.module.site.SiteModule;
47  import org.apache.maven.doxia.module.site.manager.SiteModuleManager;
48  import org.apache.maven.doxia.util.XmlValidator;
49  
50  import org.apache.velocity.VelocityContext;
51  import org.apache.velocity.context.Context;
52  
53  import org.codehaus.plexus.logging.AbstractLogEnabled;
54  
55  import org.codehaus.plexus.util.DirectoryScanner;
56  import org.codehaus.plexus.util.FileUtils;
57  import org.codehaus.plexus.util.IOUtil;
58  import org.codehaus.plexus.util.ReaderFactory;
59  import org.codehaus.plexus.util.xml.XmlStreamReader;
60  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
61  import org.codehaus.plexus.velocity.SiteResourceLoader;
62  import org.codehaus.plexus.velocity.VelocityComponent;
63  
64  /**
65   * Abstract <code>document</code> renderer.
66   *
67   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
68   * @author ltheussl
69   * @version $Id: AbstractDocumentRenderer.java 1185508 2011-10-18 06:58:50Z ltheussl $
70   * @since 1.1
71   */
72  public abstract class AbstractDocumentRenderer
73      extends AbstractLogEnabled
74      implements DocumentRenderer
75  {
76      /** @plexus.requirement */
77      protected SiteModuleManager siteModuleManager;
78  
79      /** @plexus.requirement */
80      protected Doxia doxia;
81  
82      /** @plexus.requirement */
83      private VelocityComponent velocity;
84  
85      /**
86       * The common base directory of source files.
87       */
88      private String baseDir;
89  
90        //--------------------------------------------
91       //
92      //--------------------------------------------
93  
94      /**
95       * Render an aggregate document from the files found in a Map.
96       *
97       * @param filesToProcess the Map of Files to process. The Map should contain as keys the paths of the
98       *      source files (relative to {@link #getBaseDir() baseDir}), and the corresponding SiteModule as values.
99       * @param outputDirectory the output directory where the aggregate document should be generated.
100      * @param documentModel the document model, containing all the metadata, etc.
101      * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException if any
102      * @throws java.io.IOException if any
103      * @deprecated since 1.1.2, use {@link #render(Map, File, DocumentModel, DocumentRendererContext)}
104      */
105     public abstract void render( Map<String, SiteModule> filesToProcess, File outputDirectory,
106                                  DocumentModel documentModel )
107         throws DocumentRendererException, IOException;
108 
109       //--------------------------------------------
110      //
111     //--------------------------------------------
112 
113     /** {@inheritDoc} */
114     public void render( Collection<String> files, File outputDirectory, DocumentModel documentModel )
115         throws DocumentRendererException, IOException
116     {
117         render( getFilesToProcess( files ), outputDirectory, documentModel, null );
118     }
119 
120     /** {@inheritDoc} */
121     public void render( File baseDirectory, File outputDirectory, DocumentModel documentModel )
122         throws DocumentRendererException, IOException
123     {
124         render( baseDirectory, outputDirectory, documentModel, null );
125     }
126 
127     /**
128      * Render an aggregate document from the files found in a Map.
129      *
130      * @param filesToProcess the Map of Files to process. The Map should contain as keys the paths of the
131      *      source files (relative to {@link #getBaseDir() baseDir}), and the corresponding SiteModule as values.
132      * @param outputDirectory the output directory where the aggregate document should be generated.
133      * @param documentModel the document model, containing all the metadata, etc.
134      * @param context the rendering context when processing files.
135      * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException if any
136      * @throws java.io.IOException if any
137      */
138     public void render( Map<String, SiteModule> filesToProcess, File outputDirectory, DocumentModel documentModel,
139                         DocumentRendererContext context )
140         throws DocumentRendererException, IOException
141     {
142         // nop
143     }
144 
145     /**
146      * Render a document from the files found in a source directory, depending on a rendering context.
147      *
148      * @param baseDirectory the directory containing the source files.
149      *              This should follow the standard Maven convention, ie containing all the site modules.
150      * @param outputDirectory the output directory where the document should be generated.
151      * @param documentModel the document model, containing all the metadata, etc.
152      *              If the model contains a TOC, only the files found in this TOC are rendered,
153      *              otherwise all files found under baseDirectory will be processed.
154      *              If the model is null, render all files from baseDirectory individually.
155      * @param context the rendering context when processing files.
156      * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException if any
157      * @throws java.io.IOException if any
158      * @since 1.1.2
159      */
160     public void render( File baseDirectory, File outputDirectory, DocumentModel documentModel,
161                         DocumentRendererContext context )
162         throws DocumentRendererException, IOException
163     {
164         render( getFilesToProcess( baseDirectory ), outputDirectory, documentModel, context );
165     }
166 
167     /**
168      * Render a document from the files found in baseDirectory. This just forwards to
169      *              {@link #render(File,File,DocumentModel)} with a new DocumentModel.
170      *
171      * @param baseDirectory the directory containing the source files.
172      *              This should follow the standard Maven convention, ie containing all the site modules.
173      * @param outputDirectory the output directory where the document should be generated.
174      * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException if any
175      * @throws java.io.IOException if any
176      * @see #render(File, File, DocumentModel)
177      */
178     public void render( File baseDirectory, File outputDirectory )
179         throws DocumentRendererException, IOException
180     {
181         render( baseDirectory, outputDirectory, (DocumentModel) null );
182     }
183 
184     /**
185      * Render a document from the files found in baseDirectory.
186      *
187      * @param baseDirectory the directory containing the source files.
188      *              This should follow the standard Maven convention, ie containing all the site modules.
189      * @param outputDirectory the output directory where the document should be generated.
190      * @param documentDescriptor a file containing the document model.
191      *              If this file does not exist or is null, some default settings will be used.
192      * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException if any
193      * @throws java.io.IOException if any
194      * @see #render(File, File) if documentDescriptor does not exist or is null
195      * @see #render(Map, File, DocumentModel) otherwise
196      */
197     public void render( File baseDirectory, File outputDirectory, File documentDescriptor )
198         throws DocumentRendererException, IOException
199     {
200         if ( ( documentDescriptor == null ) || ( !documentDescriptor.exists() ) )
201         {
202             getLogger().warn( "No documentDescriptor found: using default settings!" );
203 
204             render( baseDirectory, outputDirectory );
205         }
206         else
207         {
208             render( getFilesToProcess( baseDirectory ), outputDirectory, readDocumentModel( documentDescriptor ), null );
209         }
210     }
211 
212     /**
213      * Render documents separately for each file found in a Map.
214      *
215      * @param filesToProcess the Map of Files to process. The Map should contain as keys the paths of the
216      *      source files (relative to {@link #getBaseDir() baseDir}), and the corresponding SiteModule as values.
217      * @param outputDirectory the output directory where the documents should be generated.
218      * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException if any
219      * @throws java.io.IOException if any
220      * @since 1.1.1
221      * @deprecated since 1.1.2, use {@link #renderIndividual(Map, File, DocumentRendererContext)}
222      */
223     public void renderIndividual( Map<String, SiteModule> filesToProcess, File outputDirectory )
224         throws DocumentRendererException, IOException
225     {
226         // nop
227     }
228 
229     /**
230      * Render documents separately for each file found in a Map.
231      *
232      * @param filesToProcess the Map of Files to process. The Map should contain as keys the paths of the
233      *      source files (relative to {@link #getBaseDir() baseDir}), and the corresponding SiteModule as values.
234      * @param outputDirectory the output directory where the documents should be generated.
235      * @param context the rendering context.
236      * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException if any
237      * @throws java.io.IOException if any
238      * @since 1.1.2
239      */
240     public void renderIndividual( Map<String, SiteModule> filesToProcess, File outputDirectory,
241                                   DocumentRendererContext context )
242         throws DocumentRendererException, IOException
243     {
244         // nop
245     }
246 
247     /**
248      * Returns a Map of files to process. The Map contains as keys the paths of the source files
249      *      (relative to {@link #getBaseDir() baseDir}), and the corresponding SiteModule as values.
250      *
251      * @param baseDirectory the directory containing the source files.
252      *              This should follow the standard Maven convention, ie containing all the site modules.
253      * @return a Map of files to process.
254      * @throws java.io.IOException in case of a problem reading the files under baseDirectory.
255      * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException if any
256      */
257     public Map<String, SiteModule> getFilesToProcess( File baseDirectory )
258         throws IOException, DocumentRendererException
259     {
260         if ( !baseDirectory.isDirectory() )
261         {
262             getLogger().warn( "No files found to process!" );
263 
264             return new HashMap<String, SiteModule>();
265         }
266 
267         setBaseDir( baseDirectory.getAbsolutePath() );
268 
269         Map<String, SiteModule> filesToProcess = new LinkedHashMap<String, SiteModule>();
270         Map<String, String> duplicatesFiles = new LinkedHashMap<String, String>();
271 
272         Collection<SiteModule> modules = siteModuleManager.getSiteModules();
273         for ( SiteModule module : modules )
274         {
275             File moduleBasedir = new File( baseDirectory, module.getSourceDirectory() );
276 
277             if ( moduleBasedir.exists() )
278             {
279                 // TODO: handle in/excludes
280                 @SuppressWarnings ( "unchecked" )
281                 List<String> allFiles = FileUtils.getFileNames( moduleBasedir, "**/*.*", null, false );
282 
283                 String lowerCaseExtension = module.getExtension().toLowerCase( Locale.ENGLISH );
284                 List<String> docs = new LinkedList<String>( allFiles );
285                 // Take care of extension case
286                 for ( Iterator<String> it = docs.iterator(); it.hasNext(); )
287                 {
288                     String name = it.next().trim();
289 
290                     if ( !name.toLowerCase( Locale.ENGLISH ).endsWith( "." + lowerCaseExtension ) )
291                     {
292                         it.remove();
293                     }
294                 }
295 
296                 List<String> velocityFiles = new LinkedList<String>( allFiles );
297                 // *.xml.vm
298                 for ( Iterator<String> it = velocityFiles.iterator(); it.hasNext(); )
299                 {
300                     String name = it.next().trim();
301 
302                     if ( !name.toLowerCase( Locale.ENGLISH ).endsWith( lowerCaseExtension + ".vm" ) )
303                     {
304                         it.remove();
305                     }
306                 }
307                 docs.addAll( velocityFiles );
308 
309                 for ( String filePath : docs )
310                 {
311                     filePath = filePath.trim();
312 
313                     if ( filePath.lastIndexOf( '.') > 0 )
314                     {
315                         String key = filePath.substring( 0, filePath.lastIndexOf( '.') );
316 
317                         if ( duplicatesFiles.containsKey( key ) )
318                         {
319                             throw new DocumentRendererException( "Files '" + module.getSourceDirectory()
320                                 + File.separator + filePath + "' clashes with existing '"
321                                 + duplicatesFiles.get( key ) + "'." );
322                         }
323 
324                         duplicatesFiles.put( key, module.getSourceDirectory() + File.separator + filePath );
325                     }
326 
327                     filesToProcess.put( filePath, module );
328                 }
329             }
330         }
331 
332         return filesToProcess;
333     }
334 
335     /**
336      * Returns a Map of files to process. The Map contains as keys the paths of the source files
337      *      (relative to {@link #getBaseDir() baseDir}), and the corresponding SiteModule as values.
338      *
339      * @param files The Collection of source files.
340      * @return a Map of files to process.
341      */
342     public Map<String, SiteModule> getFilesToProcess( Collection<String> files )
343     {
344         // ----------------------------------------------------------------------
345         // Map all the file names to parser ids
346         // ----------------------------------------------------------------------
347 
348         Map<String, SiteModule> filesToProcess = new HashMap<String, SiteModule>();
349 
350         Collection<SiteModule> modules = siteModuleManager.getSiteModules();
351         for ( SiteModule siteModule : modules )
352         {
353             String extension = "." + siteModule.getExtension();
354 
355             String sourceDirectory = File.separator + siteModule.getSourceDirectory() + File.separator;
356 
357             for ( String file : files )
358             {
359                 // first check if the file path contains one of the recognized source dir identifiers
360                 // (there's trouble if a pathname contains 2 identifiers), then match file extensions (not unique).
361 
362                 if ( file.indexOf( sourceDirectory ) != -1 )
363                 {
364                     filesToProcess.put( file, siteModule );
365                 }
366                 else if ( file.toLowerCase( Locale.ENGLISH ).endsWith( extension ) )
367                 {
368                     // don't overwrite if it's there already
369                     if ( !filesToProcess.containsKey( file ) )
370                     {
371                         filesToProcess.put( file, siteModule );
372                     }
373                 }
374             }
375         }
376 
377         return filesToProcess;
378     }
379 
380     /** {@inheritDoc} */
381     public DocumentModel readDocumentModel( File documentDescriptor )
382         throws DocumentRendererException, IOException
383     {
384         DocumentModel documentModel;
385 
386         Reader reader = null;
387         try
388         {
389             reader = ReaderFactory.newXmlReader( documentDescriptor );
390             documentModel = new DocumentXpp3Reader().read( reader );
391         }
392         catch ( XmlPullParserException e )
393         {
394             throw new DocumentRendererException( "Error parsing document descriptor", e );
395         }
396         finally
397         {
398             IOUtil.close( reader );
399         }
400 
401         return documentModel;
402     }
403 
404     /**
405      * Sets the current base directory.
406      *
407      * @param newDir the absolute path to the base directory to set.
408      */
409     public void setBaseDir( String newDir )
410     {
411         this.baseDir = newDir;
412     }
413 
414     /**
415      * Return the current base directory.
416      *
417      * @return the current base directory.
418      */
419     public String getBaseDir()
420     {
421         return this.baseDir;
422     }
423 
424       //--------------------------------------------
425      //
426     //--------------------------------------------
427 
428     /**
429      * Parse a source document into a sink.
430      *
431      * @param fullDocPath absolute path to the source document.
432      * @param parserId determines the parser to use.
433      * @param sink the sink to receive the events.
434      * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException in case of a parsing error.
435      * @throws java.io.IOException if the source document cannot be opened.
436      * @deprecated since 1.1.2, use {@link #parse(String, String, Sink, DocumentRendererContext)}
437      */
438     protected void parse( String fullDocPath, String parserId, Sink sink )
439         throws DocumentRendererException, IOException
440     {
441         parse( fullDocPath, parserId, sink, null );
442     }
443 
444     /**
445      * Parse a source document into a sink.
446      *
447      * @param fullDocPath absolute path to the source document.
448      * @param parserId determines the parser to use.
449      * @param sink the sink to receive the events.
450      * @param context the rendering context.
451      * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException in case of a parsing error.
452      * @throws java.io.IOException if the source document cannot be opened.
453      */
454     protected void parse( String fullDocPath, String parserId, Sink sink, DocumentRendererContext context )
455         throws DocumentRendererException, IOException
456     {
457         if ( getLogger().isDebugEnabled() )
458         {
459             getLogger().debug( "Parsing file " + fullDocPath );
460         }
461 
462         Reader reader = null;
463         try
464         {
465             File f = new File( fullDocPath );
466 
467             Parser parser = doxia.getParser( parserId );
468             switch ( parser.getType() )
469             {
470                 case Parser.XML_TYPE:
471                     reader = ReaderFactory.newXmlReader( f );
472 
473                     if ( isVelocityFile( f ) )
474                     {
475                         reader = getVelocityReader( f, ( (XmlStreamReader) reader ).getEncoding(), context );
476                     }
477                     if ( context != null && Boolean.TRUE.equals( (Boolean) context.get( "validate" ) ) )
478                     {
479                         reader = validate( reader, fullDocPath );
480                     }
481                     break;
482 
483                 case Parser.TXT_TYPE:
484                 case Parser.UNKNOWN_TYPE:
485                 default:
486                     if ( isVelocityFile( f ) )
487                     {
488                         reader =
489                             getVelocityReader( f, ( context == null ? ReaderFactory.FILE_ENCODING
490                                             : context.getInputEncoding() ), context );
491                     }
492                     else
493                     {
494                         if ( context == null )
495                         {
496                             reader = ReaderFactory.newPlatformReader( f );
497                         }
498                         else
499                         {
500                             reader = ReaderFactory.newReader( f, context.getInputEncoding() );
501                         }
502                     }
503             }
504 
505             sink.enableLogging( new PlexusLoggerWrapper( getLogger() ) );
506 
507             doxia.parse( reader, parserId, sink );
508         }
509         catch ( ParserNotFoundException e )
510         {
511             throw new DocumentRendererException( "No parser '" + parserId
512                         + "' found for " + fullDocPath + ": " + e.getMessage(), e );
513         }
514         catch ( ParseException e )
515         {
516             throw new DocumentRendererException( "Error parsing " + fullDocPath + ": " + e.getMessage(), e );
517         }
518         finally
519         {
520             IOUtil.close( reader );
521 
522             sink.flush();
523         }
524     }
525 
526     /**
527      * Copies the contents of the resource directory to an output folder.
528      *
529      * @param outputDirectory the destination folder.
530      * @throws java.io.IOException if any.
531      */
532     protected void copyResources( File outputDirectory )
533             throws IOException
534     {
535         File resourcesDirectory = new File( getBaseDir(), "resources" );
536 
537         if ( !resourcesDirectory.isDirectory() )
538         {
539             return;
540         }
541 
542         if ( !outputDirectory.exists() )
543         {
544             outputDirectory.mkdirs();
545         }
546 
547         copyDirectory( resourcesDirectory, outputDirectory );
548     }
549 
550     /**
551      * Copy content of a directory, excluding scm-specific files.
552      *
553      * @param source directory that contains the files and sub-directories to be copied.
554      * @param destination destination folder.
555      * @throws java.io.IOException if any.
556      */
557     protected void copyDirectory( File source, File destination )
558             throws IOException
559     {
560         if ( source.isDirectory() && destination.isDirectory() )
561         {
562             DirectoryScanner scanner = new DirectoryScanner();
563 
564             String[] includedResources = {"**/**"};
565 
566             scanner.setIncludes( includedResources );
567 
568             scanner.addDefaultExcludes();
569 
570             scanner.setBasedir( source );
571 
572             scanner.scan();
573 
574             List<String> includedFiles = Arrays.asList( scanner.getIncludedFiles() );
575 
576             for ( String name : includedFiles )
577             {
578                 File sourceFile = new File( source, name );
579 
580                 File destinationFile = new File( destination, name );
581 
582                 FileUtils.copyFile( sourceFile, destinationFile );
583             }
584         }
585     }
586 
587     /**
588      * @param documentModel not null
589      * @return the output name defined in the documentModel without the output extension. If the output name is not
590      * defined, return target by default.
591      * @since 1.1.1
592      * @see org.apache.maven.doxia.document.DocumentModel#getOutputName()
593      * @see #getOutputExtension()
594      */
595     protected String getOutputName( DocumentModel documentModel )
596     {
597         String outputName = documentModel.getOutputName();
598         if ( outputName == null )
599         {
600             getLogger().info( "No outputName is defined in the document descriptor. Using 'target'" );
601 
602             documentModel.setOutputName( "target" );
603         }
604 
605         outputName = outputName.trim();
606         if ( outputName.toLowerCase( Locale.ENGLISH ).endsWith( "." + getOutputExtension() ) )
607         {
608             outputName =
609                 outputName.substring( 0, outputName.toLowerCase( Locale.ENGLISH )
610                                                    .lastIndexOf( "." + getOutputExtension() ) );
611         }
612         documentModel.setOutputName( outputName );
613 
614         return documentModel.getOutputName();
615     }
616 
617     /**
618      * TODO: DOXIA-111: we need a general filter here that knows how to alter the context
619      *
620      * @param f the file to process, not null
621      * @param encoding the wanted encoding, not null
622      * @param context the current render document context not null
623      * @return a reader with
624      * @throws DocumentRendererException
625      */
626     private Reader getVelocityReader( File f, String encoding, DocumentRendererContext context )
627         throws DocumentRendererException
628     {
629         if ( getLogger().isDebugEnabled() )
630         {
631             getLogger().debug( "Velocity render for " + f.getAbsolutePath() );
632         }
633 
634         SiteResourceLoader.setResource( f.getAbsolutePath() );
635 
636         Context velocityContext = new VelocityContext();
637 
638         if ( context.getKeys() != null )
639         {
640             for ( int i = 0; i < context.getKeys().length; i++ )
641             {
642                 String key = (String) context.getKeys()[i];
643 
644                 velocityContext.put( key, context.get( key ) );
645             }
646         }
647 
648         StringWriter sw = new StringWriter();
649         try
650         {
651             velocity.getEngine().mergeTemplate( f.getAbsolutePath(), encoding, velocityContext, sw );
652         }
653         catch ( Exception e )
654         {
655             throw new DocumentRendererException( "Error whenn parsing Velocity file " + f.getAbsolutePath() + ": "
656                 + e.getMessage(), e );
657         }
658 
659         return new StringReader( sw.toString() );
660     }
661 
662     /**
663      * @param f not null
664      * @return <code>true</code> if file has a vm extension, <code>false</false> otherwise.
665      */
666     private static boolean isVelocityFile( File f )
667     {
668         return FileUtils.getExtension( f.getAbsolutePath() ).toLowerCase( Locale.ENGLISH ).endsWith( "vm" );
669     }
670 
671     private Reader validate( Reader source, String resource )
672             throws ParseException, IOException
673     {
674         getLogger().debug( "Validating: " + resource );
675 
676         try
677         {
678             String content = IOUtil.toString( new BufferedReader( source ) );
679 
680             new XmlValidator( new PlexusLoggerWrapper( getLogger() ) ).validate( content );
681 
682             return new StringReader( content );
683         }
684         finally
685         {
686             IOUtil.close( source );
687         }
688     }
689 }