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 }