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 }