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