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 1087124 2011-03-30 22:45:41Z hboutemy $
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                 List<String> allFiles = FileUtils.getFileNames( moduleBasedir, "**/*.*", null, false );
281 
282                 String lowerCaseExtension = module.getExtension().toLowerCase( Locale.ENGLISH );
283                 List<String> docs = new LinkedList<String>( allFiles );
284                 // Take care of extension case
285                 for ( Iterator<String> it = docs.iterator(); it.hasNext(); )
286                 {
287                     String name = it.next().trim();
288 
289                     if ( !name.toLowerCase( Locale.ENGLISH ).endsWith( "." + lowerCaseExtension ) )
290                     {
291                         it.remove();
292                     }
293                 }
294 
295                 List<String> velocityFiles = new LinkedList<String>( allFiles );
296                 // *.xml.vm
297                 for ( Iterator<String> it = velocityFiles.iterator(); it.hasNext(); )
298                 {
299                     String name = it.next().trim();
300 
301                     if ( !name.toLowerCase( Locale.ENGLISH ).endsWith( lowerCaseExtension + ".vm" ) )
302                     {
303                         it.remove();
304                     }
305                 }
306                 docs.addAll( velocityFiles );
307 
308                 for ( String filePath : docs )
309                 {
310                     filePath = filePath.trim();
311 
312                     if ( filePath.lastIndexOf( "." ) > 0 )
313                     {
314                         String key = filePath.substring( 0, filePath.lastIndexOf( "." ) );
315 
316                         if ( duplicatesFiles.containsKey( key ) )
317                         {
318                             throw new DocumentRendererException( "Files '" + module.getSourceDirectory()
319                                 + File.separator + filePath + "' clashes with existing '"
320                                 + duplicatesFiles.get( key ) + "'." );
321                         }
322 
323                         duplicatesFiles.put( key, module.getSourceDirectory() + File.separator + filePath );
324                     }
325 
326                     filesToProcess.put( filePath, module );
327                 }
328             }
329         }
330 
331         return filesToProcess;
332     }
333 
334     /**
335      * Returns a Map of files to process. The Map contains as keys the paths of the source files
336      *      (relative to {@link #getBaseDir() baseDir}), and the corresponding SiteModule as values.
337      *
338      * @param files The Collection of source files.
339      * @return a Map of files to process.
340      */
341     public Map<String, SiteModule> getFilesToProcess( Collection<String> files )
342     {
343         // ----------------------------------------------------------------------
344         // Map all the file names to parser ids
345         // ----------------------------------------------------------------------
346 
347         Map<String, SiteModule> filesToProcess = new HashMap<String, SiteModule>();
348 
349         Collection<SiteModule> modules = siteModuleManager.getSiteModules();
350         for ( SiteModule siteModule : modules )
351         {
352             String extension = "." + siteModule.getExtension();
353 
354             String sourceDirectory = File.separator + siteModule.getSourceDirectory() + File.separator;
355 
356             for ( String file : files )
357             {
358                 // first check if the file path contains one of the recognized source dir identifiers
359                 // (there's trouble if a pathname contains 2 identifiers), then match file extensions (not unique).
360 
361                 if ( file.indexOf( sourceDirectory ) != -1 )
362                 {
363                     filesToProcess.put( file, siteModule );
364                 }
365                 else if ( file.toLowerCase( Locale.ENGLISH ).endsWith( extension ) )
366                 {
367                     // don't overwrite if it's there already
368                     if ( !filesToProcess.containsKey( file ) )
369                     {
370                         filesToProcess.put( file, siteModule );
371                     }
372                 }
373             }
374         }
375 
376         return filesToProcess;
377     }
378 
379     /** {@inheritDoc} */
380     public DocumentModel readDocumentModel( File documentDescriptor )
381         throws DocumentRendererException, IOException
382     {
383         DocumentModel documentModel;
384 
385         Reader reader = null;
386         try
387         {
388             reader = ReaderFactory.newXmlReader( documentDescriptor );
389             documentModel = new DocumentXpp3Reader().read( reader );
390         }
391         catch ( XmlPullParserException e )
392         {
393             throw new DocumentRendererException( "Error parsing document descriptor", e );
394         }
395         finally
396         {
397             IOUtil.close( reader );
398         }
399 
400         return documentModel;
401     }
402 
403     /**
404      * Sets the current base directory.
405      *
406      * @param newDir the absolute path to the base directory to set.
407      */
408     public void setBaseDir( String newDir )
409     {
410         this.baseDir = newDir;
411     }
412 
413     /**
414      * Return the current base directory.
415      *
416      * @return the current base directory.
417      */
418     public String getBaseDir()
419     {
420         return this.baseDir;
421     }
422 
423       //--------------------------------------------
424      //
425     //--------------------------------------------
426 
427     /**
428      * Parse a source document into a sink.
429      *
430      * @param fullDocPath absolute path to the source document.
431      * @param parserId determines the parser to use.
432      * @param sink the sink to receive the events.
433      * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException in case of a parsing error.
434      * @throws java.io.IOException if the source document cannot be opened.
435      * @deprecated since 1.1.2, use {@link #parse(String, String, Sink, DocumentRendererContext)}
436      */
437     protected void parse( String fullDocPath, String parserId, Sink sink )
438         throws DocumentRendererException, IOException
439     {
440         parse( fullDocPath, parserId, sink, null );
441     }
442 
443     /**
444      * Parse a source document into a sink.
445      *
446      * @param fullDocPath absolute path to the source document.
447      * @param parserId determines the parser to use.
448      * @param sink the sink to receive the events.
449      * @param context the rendering context.
450      * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException in case of a parsing error.
451      * @throws java.io.IOException if the source document cannot be opened.
452      */
453     protected void parse( String fullDocPath, String parserId, Sink sink, DocumentRendererContext context )
454         throws DocumentRendererException, IOException
455     {
456         if ( getLogger().isDebugEnabled() )
457         {
458             getLogger().debug( "Parsing file " + fullDocPath );
459         }
460 
461         Reader reader = null;
462         try
463         {
464             File f = new File( fullDocPath );
465 
466             Parser parser = doxia.getParser( parserId );
467             switch ( parser.getType() )
468             {
469                 case Parser.XML_TYPE:
470                     reader = ReaderFactory.newXmlReader( f );
471 
472                     if ( isVelocityFile( f ) )
473                     {
474                         reader = getVelocityReader( f, ( (XmlStreamReader) reader ).getEncoding(), context );
475                     }
476                     if ( context != null && Boolean.TRUE.equals( (Boolean) context.get( "validate" ) ) )
477                     {
478                         reader = validate( reader, fullDocPath );
479                     }
480                     break;
481 
482                 case Parser.TXT_TYPE:
483                 case Parser.UNKNOWN_TYPE:
484                 default:
485                     if ( isVelocityFile( f ) )
486                     {
487                         reader =
488                             getVelocityReader( f, ( context == null ? ReaderFactory.FILE_ENCODING
489                                             : context.getInputEncoding() ), context );
490                     }
491                     else
492                     {
493                         if ( context == null )
494                         {
495                             reader = ReaderFactory.newPlatformReader( f );
496                         }
497                         else
498                         {
499                             reader = ReaderFactory.newReader( f, context.getInputEncoding() );
500                         }
501                     }
502             }
503 
504             sink.enableLogging( new PlexusLoggerWrapper( getLogger() ) );
505 
506             doxia.parse( reader, parserId, sink );
507         }
508         catch ( ParserNotFoundException e )
509         {
510             throw new DocumentRendererException( "No parser '" + parserId
511                         + "' found for " + fullDocPath + ": " + e.getMessage(), e );
512         }
513         catch ( ParseException e )
514         {
515             throw new DocumentRendererException( "Error parsing " + fullDocPath + ": " + e.getMessage(), e );
516         }
517         finally
518         {
519             IOUtil.close( reader );
520 
521             sink.flush();
522         }
523     }
524 
525     /**
526      * Copies the contents of the resource directory to an output folder.
527      *
528      * @param outputDirectory the destination folder.
529      * @throws java.io.IOException if any.
530      */
531     protected void copyResources( File outputDirectory )
532             throws IOException
533     {
534         File resourcesDirectory = new File( getBaseDir(), "resources" );
535 
536         if ( !resourcesDirectory.isDirectory() )
537         {
538             return;
539         }
540 
541         if ( !outputDirectory.exists() )
542         {
543             outputDirectory.mkdirs();
544         }
545 
546         copyDirectory( resourcesDirectory, outputDirectory );
547     }
548 
549     /**
550      * Copy content of a directory, excluding scm-specific files.
551      *
552      * @param source directory that contains the files and sub-directories to be copied.
553      * @param destination destination folder.
554      * @throws java.io.IOException if any.
555      */
556     protected void copyDirectory( File source, File destination )
557             throws IOException
558     {
559         if ( source.isDirectory() && destination.isDirectory() )
560         {
561             DirectoryScanner scanner = new DirectoryScanner();
562 
563             String[] includedResources = {"**/**"};
564 
565             scanner.setIncludes( includedResources );
566 
567             scanner.addDefaultExcludes();
568 
569             scanner.setBasedir( source );
570 
571             scanner.scan();
572 
573             List<String> includedFiles = Arrays.asList( scanner.getIncludedFiles() );
574 
575             for ( String name : includedFiles )
576             {
577                 File sourceFile = new File( source, name );
578 
579                 File destinationFile = new File( destination, name );
580 
581                 FileUtils.copyFile( sourceFile, destinationFile );
582             }
583         }
584     }
585 
586     /**
587      * @param documentModel not null
588      * @return the output name defined in the documentModel without the output extension. If the output name is not
589      * defined, return target by default.
590      * @since 1.1.1
591      * @see org.apache.maven.doxia.document.DocumentModel#getOutputName()
592      * @see #getOutputExtension()
593      */
594     protected String getOutputName( DocumentModel documentModel )
595     {
596         String outputName = documentModel.getOutputName();
597         if ( outputName == null )
598         {
599             getLogger().info( "No outputName is defined in the document descriptor. Using 'target'" );
600 
601             documentModel.setOutputName( "target" );
602         }
603 
604         outputName = outputName.trim();
605         if ( outputName.toLowerCase( Locale.ENGLISH ).endsWith( "." + getOutputExtension() ) )
606         {
607             outputName =
608                 outputName.substring( 0, outputName.toLowerCase( Locale.ENGLISH )
609                                                    .lastIndexOf( "." + getOutputExtension() ) );
610         }
611         documentModel.setOutputName( outputName );
612 
613         return documentModel.getOutputName();
614     }
615 
616     /**
617      * TODO: DOXIA-111: we need a general filter here that knows how to alter the context
618      *
619      * @param f the file to process, not null
620      * @param encoding the wanted encoding, not null
621      * @param context the current render document context not null
622      * @return a reader with
623      * @throws DocumentRendererException
624      */
625     private Reader getVelocityReader( File f, String encoding, DocumentRendererContext context )
626         throws DocumentRendererException
627     {
628         if ( getLogger().isDebugEnabled() )
629         {
630             getLogger().debug( "Velocity render for " + f.getAbsolutePath() );
631         }
632 
633         SiteResourceLoader.setResource( f.getAbsolutePath() );
634 
635         Context velocityContext = new VelocityContext();
636 
637         if ( context.getKeys() != null )
638         {
639             for ( int i = 0; i < context.getKeys().length; i++ )
640             {
641                 String key = (String) context.getKeys()[i];
642 
643                 velocityContext.put( key, context.get( key ) );
644             }
645         }
646 
647         StringWriter sw = new StringWriter();
648         try
649         {
650             velocity.getEngine().mergeTemplate( f.getAbsolutePath(), encoding, velocityContext, sw );
651         }
652         catch ( Exception e )
653         {
654             throw new DocumentRendererException( "Error whenn parsing Velocity file " + f.getAbsolutePath() + ": "
655                 + e.getMessage(), e );
656         }
657 
658         return new StringReader( sw.toString() );
659     }
660 
661     /**
662      * @param f not null
663      * @return <code>true</code> if file has a vm extension, <code>false</false> otherwise.
664      */
665     private static boolean isVelocityFile( File f )
666     {
667         return FileUtils.getExtension( f.getAbsolutePath() ).toLowerCase( Locale.ENGLISH ).endsWith( "vm" );
668     }
669 
670     private Reader validate( Reader source, String resource )
671             throws ParseException, IOException
672     {
673         getLogger().debug( "Validating: " + resource );
674 
675         try
676         {
677             String content = IOUtil.toString( new BufferedReader( source ) );
678 
679             new XmlValidator( new PlexusLoggerWrapper( getLogger() ) ).validate( content );
680 
681             return new StringReader( content );
682         }
683         finally
684         {
685             IOUtil.close( source );
686         }
687     }
688 }