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.parser.module.ParserModule;
47 import org.apache.maven.doxia.parser.module.ParserModuleManager;
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 1726406 2016-01-23 15:06:45Z hboutemy $
71 * @since 1.1
72 */
73 public abstract class AbstractDocumentRenderer
74 extends AbstractLogEnabled
75 implements DocumentRenderer
76 {
77 @Requirement
78 protected ParserModuleManager parserModuleManager;
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 ParserModule 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, ParserModule> 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 ParserModule 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, ParserModule> 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 ),
210 null );
211 }
212 }
213
214 /**
215 * Render documents separately for each file found in a Map.
216 *
217 * @param filesToProcess the Map of Files to process. The Map should contain as keys the paths of the
218 * source files (relative to {@link #getBaseDir() baseDir}), and the corresponding ParserModule as values.
219 * @param outputDirectory the output directory where the documents should be generated.
220 * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException if any
221 * @throws java.io.IOException if any
222 * @since 1.1.1
223 * @deprecated since 1.1.2, use {@link #renderIndividual(Map, File, DocumentRendererContext)}
224 */
225 public void renderIndividual( Map<String, ParserModule> filesToProcess, File outputDirectory )
226 throws DocumentRendererException, IOException
227 {
228 // nop
229 }
230
231 /**
232 * Render documents separately for each file found in a Map.
233 *
234 * @param filesToProcess the Map of Files to process. The Map should contain as keys the paths of the
235 * source files (relative to {@link #getBaseDir() baseDir}), and the corresponding ParserModule as values.
236 * @param outputDirectory the output directory where the documents should be generated.
237 * @param context the rendering context.
238 * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException if any
239 * @throws java.io.IOException if any
240 * @since 1.1.2
241 */
242 public void renderIndividual( Map<String, ParserModule> filesToProcess, File outputDirectory,
243 DocumentRendererContext context )
244 throws DocumentRendererException, IOException
245 {
246 // nop
247 }
248
249 /**
250 * Returns a Map of files to process. The Map contains as keys the paths of the source files
251 * (relative to {@link #getBaseDir() baseDir}), and the corresponding ParserModule as values.
252 *
253 * @param baseDirectory the directory containing the source files.
254 * This should follow the standard Maven convention, ie containing all the site modules.
255 * @return a Map of files to process.
256 * @throws java.io.IOException in case of a problem reading the files under baseDirectory.
257 * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException if any
258 */
259 public Map<String, ParserModule> getFilesToProcess( File baseDirectory )
260 throws IOException, DocumentRendererException
261 {
262 if ( !baseDirectory.isDirectory() )
263 {
264 getLogger().warn( "No files found to process!" );
265
266 return new HashMap<String, ParserModule>();
267 }
268
269 setBaseDir( baseDirectory.getAbsolutePath() );
270
271 Map<String, ParserModule> filesToProcess = new LinkedHashMap<String, ParserModule>();
272 Map<String, String> duplicatesFiles = new LinkedHashMap<String, String>();
273
274 Collection<ParserModule> modules = parserModuleManager.getParserModules();
275 for ( ParserModule module : modules )
276 {
277 File moduleBasedir = new File( baseDirectory, module.getSourceDirectory() );
278
279 if ( moduleBasedir.exists() )
280 {
281 // TODO: handle in/excludes
282 List<String> allFiles = FileUtils.getFileNames( moduleBasedir, "**/*.*", null, false );
283
284 String[] extensions = getExtensions( module );
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 ( !endsWithIgnoreCase( name, extensions ) )
292 {
293 it.remove();
294 }
295 }
296
297 String[] vmExtensions = new String[extensions.length];
298 for ( int i = 0; i < extensions.length; i++ )
299 {
300 vmExtensions[i] = extensions[i] + ".vm";
301 }
302 List<String> velocityFiles = new LinkedList<String>( allFiles );
303 // *.xml.vm
304 for ( Iterator<String> it = velocityFiles.iterator(); it.hasNext(); )
305 {
306 String name = it.next().trim();
307
308 if ( !endsWithIgnoreCase( name, vmExtensions ) )
309 {
310 it.remove();
311 }
312 }
313 docs.addAll( velocityFiles );
314
315 for ( String filePath : docs )
316 {
317 filePath = filePath.trim();
318
319 if ( filePath.lastIndexOf( '.' ) > 0 )
320 {
321 String key = filePath.substring( 0, filePath.lastIndexOf( '.' ) );
322
323 if ( duplicatesFiles.containsKey( key ) )
324 {
325 throw new DocumentRendererException( "Files '" + module.getSourceDirectory()
326 + File.separator + filePath + "' clashes with existing '"
327 + duplicatesFiles.get( key ) + "'." );
328 }
329
330 duplicatesFiles.put( key, module.getSourceDirectory() + File.separator + filePath );
331 }
332
333 filesToProcess.put( filePath, module );
334 }
335 }
336 }
337
338 return filesToProcess;
339 }
340
341 protected static String[] getExtensions( ParserModule module )
342 {
343 String[] extensions = new String[module.getExtensions().length];
344 for ( int i = module.getExtensions().length - 1; i >= 0; i-- )
345 {
346 extensions[i] = '.' + module.getExtensions()[i];
347 }
348 return extensions;
349 }
350
351 // TODO replace with StringUtils.endsWithIgnoreCase() from maven-shared-utils 0.7
352 protected static boolean endsWithIgnoreCase( String str, String searchStr )
353 {
354 if ( str.length() < searchStr.length() )
355 {
356 return false;
357 }
358
359 return str.regionMatches( true, str.length() - searchStr.length(), searchStr, 0, searchStr.length() );
360 }
361
362 protected static boolean endsWithIgnoreCase( String str, String[] searchStrs )
363 {
364 for ( String searchStr : searchStrs )
365 {
366 if ( endsWithIgnoreCase( str, searchStr ) )
367 {
368 return true;
369 }
370 }
371 return false;
372 }
373
374 /**
375 * Returns a Map of files to process. The Map contains as keys the paths of the source files
376 * (relative to {@link #getBaseDir() baseDir}), and the corresponding ParserModule as values.
377 *
378 * @param files The Collection of source files.
379 * @return a Map of files to process.
380 */
381 public Map<String, ParserModule> getFilesToProcess( Collection<String> files )
382 {
383 // ----------------------------------------------------------------------
384 // Map all the file names to parser ids
385 // ----------------------------------------------------------------------
386
387 Map<String, ParserModule> filesToProcess = new HashMap<String, ParserModule>();
388
389 Collection<ParserModule> modules = parserModuleManager.getParserModules();
390 for ( ParserModule module : modules )
391 {
392 String[] extensions = getExtensions( module );
393
394 String sourceDirectory = File.separator + module.getSourceDirectory() + File.separator;
395
396 for ( String file : files )
397 {
398 // first check if the file path contains one of the recognized source dir identifiers
399 // (there's trouble if a pathname contains 2 identifiers), then match file extensions (not unique).
400
401 if ( file.indexOf( sourceDirectory ) != -1 )
402 {
403 filesToProcess.put( file, module );
404 }
405 else
406 {
407 // don't overwrite if it's there already
408 if ( endsWithIgnoreCase( file, extensions ) && !filesToProcess.containsKey( file ) )
409 {
410 filesToProcess.put( file, module );
411 }
412 }
413 }
414 }
415
416 return filesToProcess;
417 }
418
419 /** {@inheritDoc} */
420 public DocumentModel readDocumentModel( File documentDescriptor )
421 throws DocumentRendererException, IOException
422 {
423 DocumentModel documentModel;
424
425 Reader reader = null;
426 try
427 {
428 reader = ReaderFactory.newXmlReader( documentDescriptor );
429 documentModel = new DocumentXpp3Reader().read( reader );
430 }
431 catch ( XmlPullParserException e )
432 {
433 throw new DocumentRendererException( "Error parsing document descriptor", e );
434 }
435 finally
436 {
437 IOUtil.close( reader );
438 }
439
440 return documentModel;
441 }
442
443 /**
444 * Sets the current base directory.
445 *
446 * @param newDir the absolute path to the base directory to set.
447 */
448 public void setBaseDir( String newDir )
449 {
450 this.baseDir = newDir;
451 }
452
453 /**
454 * Return the current base directory.
455 *
456 * @return the current base directory.
457 */
458 public String getBaseDir()
459 {
460 return this.baseDir;
461 }
462
463 //--------------------------------------------
464 //
465 //--------------------------------------------
466
467 /**
468 * Parse a source document into a sink.
469 *
470 * @param fullDocPath absolute path to the source document.
471 * @param parserId determines the parser to use.
472 * @param sink the sink to receive the events.
473 * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException in case of a parsing error.
474 * @throws java.io.IOException if the source document cannot be opened.
475 * @deprecated since 1.1.2, use {@link #parse(String, String, Sink, DocumentRendererContext)}
476 */
477 protected void parse( String fullDocPath, String parserId, Sink sink )
478 throws DocumentRendererException, IOException
479 {
480 parse( fullDocPath, parserId, sink, null );
481 }
482
483 /**
484 * Parse a source document into a sink.
485 *
486 * @param fullDocPath absolute path to the source document.
487 * @param parserId determines the parser to use.
488 * @param sink the sink to receive the events.
489 * @param context the rendering context.
490 * @throws org.apache.maven.doxia.docrenderer.DocumentRendererException in case of a parsing error.
491 * @throws java.io.IOException if the source document cannot be opened.
492 */
493 protected void parse( String fullDocPath, String parserId, Sink sink, DocumentRendererContext context )
494 throws DocumentRendererException, IOException
495 {
496 if ( getLogger().isDebugEnabled() )
497 {
498 getLogger().debug( "Parsing file " + fullDocPath );
499 }
500
501 Reader reader = null;
502 try
503 {
504 File f = new File( fullDocPath );
505
506 Parser parser = doxia.getParser( parserId );
507 switch ( parser.getType() )
508 {
509 case Parser.XML_TYPE:
510 reader = ReaderFactory.newXmlReader( f );
511
512 if ( isVelocityFile( f ) )
513 {
514 reader = getVelocityReader( f, ( (XmlStreamReader) reader ).getEncoding(), context );
515 }
516 if ( context != null && Boolean.TRUE.equals( (Boolean) context.get( "validate" ) ) )
517 {
518 reader = validate( reader, fullDocPath );
519 }
520 break;
521
522 case Parser.TXT_TYPE:
523 case Parser.UNKNOWN_TYPE:
524 default:
525 if ( isVelocityFile( f ) )
526 {
527 reader =
528 getVelocityReader( f, ( context == null ? ReaderFactory.FILE_ENCODING
529 : context.getInputEncoding() ), context );
530 }
531 else
532 {
533 if ( context == null )
534 {
535 reader = ReaderFactory.newPlatformReader( f );
536 }
537 else
538 {
539 reader = ReaderFactory.newReader( f, context.getInputEncoding() );
540 }
541 }
542 }
543
544 sink.enableLogging( new PlexusLoggerWrapper( getLogger() ) );
545
546 doxia.parse( reader, parserId, sink );
547 }
548 catch ( ParserNotFoundException e )
549 {
550 throw new DocumentRendererException( "No parser '" + parserId
551 + "' found for " + fullDocPath + ": " + e.getMessage(), e );
552 }
553 catch ( ParseException e )
554 {
555 throw new DocumentRendererException( "Error parsing " + fullDocPath + ": " + e.getMessage(), e );
556 }
557 finally
558 {
559 IOUtil.close( reader );
560
561 sink.flush();
562 }
563 }
564
565 /**
566 * Copies the contents of the resource directory to an output folder.
567 *
568 * @param outputDirectory the destination folder.
569 * @throws java.io.IOException if any.
570 */
571 protected void copyResources( File outputDirectory )
572 throws IOException
573 {
574 File resourcesDirectory = new File( getBaseDir(), "resources" );
575
576 if ( !resourcesDirectory.isDirectory() )
577 {
578 return;
579 }
580
581 if ( !outputDirectory.exists() )
582 {
583 outputDirectory.mkdirs();
584 }
585
586 copyDirectory( resourcesDirectory, outputDirectory );
587 }
588
589 /**
590 * Copy content of a directory, excluding scm-specific files.
591 *
592 * @param source directory that contains the files and sub-directories to be copied.
593 * @param destination destination folder.
594 * @throws java.io.IOException if any.
595 */
596 protected void copyDirectory( File source, File destination )
597 throws IOException
598 {
599 if ( source.isDirectory() && destination.isDirectory() )
600 {
601 DirectoryScanner scanner = new DirectoryScanner();
602
603 String[] includedResources = {"**/**"};
604
605 scanner.setIncludes( includedResources );
606
607 scanner.addDefaultExcludes();
608
609 scanner.setBasedir( source );
610
611 scanner.scan();
612
613 List<String> includedFiles = Arrays.asList( scanner.getIncludedFiles() );
614
615 for ( String name : includedFiles )
616 {
617 File sourceFile = new File( source, name );
618
619 File destinationFile = new File( destination, name );
620
621 FileUtils.copyFile( sourceFile, destinationFile );
622 }
623 }
624 }
625
626 /**
627 * @param documentModel not null
628 * @return the output name defined in the documentModel without the output extension. If the output name is not
629 * defined, return target by default.
630 * @since 1.1.1
631 * @see org.apache.maven.doxia.document.DocumentModel#getOutputName()
632 * @see #getOutputExtension()
633 */
634 protected String getOutputName( DocumentModel documentModel )
635 {
636 String outputName = documentModel.getOutputName();
637 if ( outputName == null )
638 {
639 getLogger().info( "No outputName is defined in the document descriptor. Using 'target'" );
640
641 documentModel.setOutputName( "target" );
642 }
643
644 outputName = outputName.trim();
645 if ( outputName.toLowerCase( Locale.ENGLISH ).endsWith( "." + getOutputExtension() ) )
646 {
647 outputName =
648 outputName.substring( 0, outputName.toLowerCase( Locale.ENGLISH )
649 .lastIndexOf( "." + getOutputExtension() ) );
650 }
651 documentModel.setOutputName( outputName );
652
653 return documentModel.getOutputName();
654 }
655
656 /**
657 * TODO: DOXIA-111: we need a general filter here that knows how to alter the context
658 *
659 * @param f the file to process, not null
660 * @param encoding the wanted encoding, not null
661 * @param context the current render document context not null
662 * @return a reader with
663 * @throws DocumentRendererException
664 */
665 private Reader getVelocityReader( File f, String encoding, DocumentRendererContext context )
666 throws DocumentRendererException
667 {
668 if ( getLogger().isDebugEnabled() )
669 {
670 getLogger().debug( "Velocity render for " + f.getAbsolutePath() );
671 }
672
673 SiteResourceLoader.setResource( f.getAbsolutePath() );
674
675 Context velocityContext = new VelocityContext();
676
677 if ( context.getKeys() != null )
678 {
679 for ( int i = 0; i < context.getKeys().length; i++ )
680 {
681 String key = (String) context.getKeys()[i];
682
683 velocityContext.put( key, context.get( key ) );
684 }
685 }
686
687 StringWriter sw = new StringWriter();
688 try
689 {
690 velocity.getEngine().mergeTemplate( f.getAbsolutePath(), encoding, velocityContext, sw );
691 }
692 catch ( Exception e )
693 {
694 throw new DocumentRendererException( "Error whenn parsing Velocity file " + f.getAbsolutePath() + ": "
695 + e.getMessage(), e );
696 }
697
698 return new StringReader( sw.toString() );
699 }
700
701 /**
702 * @param f not null
703 * @return <code>true</code> if file has a vm extension, <code>false</false> otherwise.
704 */
705 private static boolean isVelocityFile( File f )
706 {
707 return FileUtils.getExtension( f.getAbsolutePath() ).toLowerCase( Locale.ENGLISH ).endsWith( "vm" );
708 }
709
710 private Reader validate( Reader source, String resource )
711 throws ParseException, IOException
712 {
713 getLogger().debug( "Validating: " + resource );
714
715 try
716 {
717 String content = IOUtil.toString( new BufferedReader( source ) );
718
719 new XmlValidator( new PlexusLoggerWrapper( getLogger() ) ).validate( content );
720
721 return new StringReader( content );
722 }
723 finally
724 {
725 IOUtil.close( source );
726 }
727 }
728 }