1 package org.apache.maven.doxia.siterenderer;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import java.io.BufferedReader;
23 import java.io.File;
24 import java.io.FileNotFoundException;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.LineNumberReader;
29 import java.io.OutputStream;
30 import java.io.Reader;
31 import java.io.StringReader;
32 import java.io.StringWriter;
33 import java.io.UnsupportedEncodingException;
34 import java.io.Writer;
35
36 import java.net.MalformedURLException;
37 import java.net.URL;
38 import java.net.URLClassLoader;
39
40 import java.text.DateFormat;
41 import java.text.SimpleDateFormat;
42
43 import java.util.Arrays;
44 import java.util.Collection;
45 import java.util.Collections;
46 import java.util.Date;
47 import java.util.Enumeration;
48 import java.util.Iterator;
49 import java.util.LinkedHashMap;
50 import java.util.LinkedList;
51 import java.util.List;
52 import java.util.Locale;
53 import java.util.Map;
54 import java.util.zip.ZipEntry;
55 import java.util.zip.ZipFile;
56
57 import org.apache.maven.doxia.Doxia;
58 import org.apache.maven.doxia.logging.PlexusLoggerWrapper;
59 import org.apache.maven.doxia.sink.render.RenderingContext;
60 import org.apache.maven.doxia.parser.ParseException;
61 import org.apache.maven.doxia.parser.Parser;
62 import org.apache.maven.doxia.parser.manager.ParserNotFoundException;
63 import org.apache.maven.doxia.site.decoration.DecorationModel;
64 import org.apache.maven.doxia.module.site.SiteModule;
65 import org.apache.maven.doxia.module.site.manager.SiteModuleManager;
66 import org.apache.maven.doxia.module.site.manager.SiteModuleNotFoundException;
67 import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
68 import org.apache.maven.doxia.util.XmlValidator;
69
70 import org.apache.velocity.Template;
71 import org.apache.velocity.VelocityContext;
72 import org.apache.velocity.context.Context;
73
74 import org.codehaus.plexus.i18n.I18N;
75 import org.codehaus.plexus.logging.AbstractLogEnabled;
76 import org.codehaus.plexus.util.DirectoryScanner;
77 import org.codehaus.plexus.util.FileUtils;
78 import org.codehaus.plexus.util.IOUtil;
79 import org.codehaus.plexus.util.Os;
80 import org.codehaus.plexus.util.PathTool;
81 import org.codehaus.plexus.util.ReaderFactory;
82 import org.codehaus.plexus.util.StringUtils;
83 import org.codehaus.plexus.util.WriterFactory;
84 import org.codehaus.plexus.velocity.SiteResourceLoader;
85 import org.codehaus.plexus.velocity.VelocityComponent;
86
87
88
89
90
91
92
93
94
95
96
97 public class DefaultSiteRenderer
98 extends AbstractLogEnabled
99 implements Renderer
100 {
101
102
103
104
105
106 private VelocityComponent velocity;
107
108
109
110
111 private SiteModuleManager siteModuleManager;
112
113
114 private Doxia doxia;
115
116
117 private I18N i18n;
118
119 private static final String RESOURCE_DIR = "org/apache/maven/doxia/siterenderer/resources";
120
121 private static final String DEFAULT_TEMPLATE = RESOURCE_DIR + "/default-site.vm";
122
123 private static final String SKIN_TEMPLATE_LOCATION = "META-INF/maven/site.vm";
124
125
126
127
128
129
130 public void render( Collection<DocumentRenderer> documents, SiteRenderingContext siteRenderingContext,
131 File outputDirectory )
132 throws RendererException, IOException
133 {
134 renderModule( documents, siteRenderingContext, outputDirectory );
135
136 for ( File siteDirectory : siteRenderingContext.getSiteDirectories() )
137 {
138 copyResources( siteRenderingContext, new File( siteDirectory, "resources" ), outputDirectory );
139 }
140 }
141
142
143 public Map<String, DocumentRenderer> locateDocumentFiles( SiteRenderingContext siteRenderingContext )
144 throws IOException, RendererException
145 {
146 Map<String, DocumentRenderer> files = new LinkedHashMap<String, DocumentRenderer>();
147 Map<String, String> moduleExcludes = siteRenderingContext.getModuleExcludes();
148
149 for ( File siteDirectory : siteRenderingContext.getSiteDirectories() )
150 {
151 if ( siteDirectory.exists() )
152 {
153 Collection<SiteModule> modules = siteModuleManager.getSiteModules();
154 for ( SiteModule module : modules )
155 {
156 File moduleBasedir = new File( siteDirectory, module.getSourceDirectory() );
157
158 if ( moduleExcludes != null && moduleExcludes.containsKey( module.getParserId() ) )
159 {
160 addModuleFiles( moduleBasedir, module, moduleExcludes.get( module.getParserId() ),
161 files );
162 }
163 else
164 {
165 addModuleFiles( moduleBasedir, module, null, files );
166 }
167 }
168 }
169 }
170
171 for ( ModuleReference module : siteRenderingContext.getModules() )
172 {
173 try
174 {
175 if ( moduleExcludes != null && moduleExcludes.containsKey( module.getParserId() ) )
176 {
177 addModuleFiles( module.getBasedir(), siteModuleManager.getSiteModule( module.getParserId() ),
178 moduleExcludes.get( module.getParserId() ), files );
179 }
180 else
181 {
182 addModuleFiles( module.getBasedir(), siteModuleManager.getSiteModule( module.getParserId() ), null,
183 files );
184 }
185 }
186 catch ( SiteModuleNotFoundException e )
187 {
188 throw new RendererException( "Unable to find module: " + e.getMessage(), e );
189 }
190 }
191 return files;
192 }
193
194 private void addModuleFiles( File moduleBasedir, SiteModule module, String excludes,
195 Map<String, DocumentRenderer> files )
196 throws IOException, RendererException
197 {
198 if ( moduleBasedir.exists() )
199 {
200 List<String> allFiles = FileUtils.getFileNames( moduleBasedir, "**/*.*", excludes, false );
201
202 String lowerCaseExtension = module.getExtension().toLowerCase( Locale.ENGLISH );
203 List<String> docs = new LinkedList<String>( allFiles );
204
205 for ( Iterator<String> it = docs.iterator(); it.hasNext(); )
206 {
207 String name = it.next().trim();
208
209 if ( !name.toLowerCase( Locale.ENGLISH ).endsWith( "." + lowerCaseExtension ) )
210 {
211 it.remove();
212 }
213 }
214
215 List<String> velocityFiles = new LinkedList<String>( allFiles );
216
217 for ( Iterator<String> it = velocityFiles.iterator(); it.hasNext(); )
218 {
219 String name = it.next().trim();
220
221 if ( !name.toLowerCase( Locale.ENGLISH ).endsWith( lowerCaseExtension + ".vm" ) )
222 {
223 it.remove();
224 }
225 }
226 docs.addAll( velocityFiles );
227
228 for ( String doc : docs )
229 {
230 String docc = doc.trim();
231
232 RenderingContext context =
233 new RenderingContext( moduleBasedir, docc, module.getParserId(), module.getExtension() );
234
235
236 if ( docc.toLowerCase( Locale.ENGLISH ).endsWith( ".vm" ) )
237 {
238 context.setAttribute( "velocity", "true" );
239 }
240
241 String key = context.getOutputName();
242 key = StringUtils.replace( key, "\\", "/" );
243
244 if ( files.containsKey( key ) )
245 {
246 DocumentRenderer renderer = files.get( key );
247
248 RenderingContext originalContext = renderer.getRenderingContext();
249
250 File originalDoc = new File( originalContext.getBasedir(), originalContext.getInputName() );
251
252 throw new RendererException( "Files '" + module.getSourceDirectory() + File.separator + docc
253 + "' clashes with existing '" + originalDoc + "'." );
254 }
255
256
257
258 for ( Map.Entry<String, DocumentRenderer> entry : files.entrySet() )
259 {
260 if ( entry.getKey().equalsIgnoreCase( key ) )
261 {
262 RenderingContext originalContext = entry.getValue().getRenderingContext();
263
264 File originalDoc = new File( originalContext.getBasedir(), originalContext.getInputName() );
265
266 if ( Os.isFamily( Os.FAMILY_WINDOWS ) )
267 {
268 throw new RendererException( "Files '" + module.getSourceDirectory() + File.separator
269 + docc + "' clashes with existing '" + originalDoc + "'." );
270 }
271
272 if ( getLogger().isWarnEnabled() )
273 {
274 getLogger().warn(
275 "Files '" + module.getSourceDirectory() + File.separator + docc
276 + "' could clashes with existing '" + originalDoc + "'." );
277 }
278 }
279 }
280
281 files.put( key, new DoxiaDocumentRenderer( context ) );
282 }
283 }
284 }
285
286 private void renderModule( Collection<DocumentRenderer> docs, SiteRenderingContext siteRenderingContext,
287 File outputDirectory )
288 throws IOException, RendererException
289 {
290 for ( DocumentRenderer docRenderer : docs )
291 {
292 RenderingContext renderingContext = docRenderer.getRenderingContext();
293
294 File outputFile = new File( outputDirectory, docRenderer.getOutputName() );
295
296 File inputFile = new File( renderingContext.getBasedir(), renderingContext.getInputName() );
297
298 boolean modified = !outputFile.exists() || ( inputFile.lastModified() > outputFile.lastModified() )
299 || ( siteRenderingContext.getDecoration().getLastModified() > outputFile.lastModified() );
300
301 if ( modified || docRenderer.isOverwrite() )
302 {
303 if ( !outputFile.getParentFile().exists() )
304 {
305 outputFile.getParentFile().mkdirs();
306 }
307
308 if ( getLogger().isDebugEnabled() )
309 {
310 getLogger().debug( "Generating " + outputFile );
311 }
312
313 Writer writer = null;
314 try
315 {
316 writer = WriterFactory.newWriter( outputFile, siteRenderingContext.getOutputEncoding() );
317 docRenderer.renderDocument( writer, this, siteRenderingContext );
318 }
319 finally
320 {
321 IOUtil.close( writer );
322 }
323 }
324 else
325 {
326 if ( getLogger().isDebugEnabled() )
327 {
328 getLogger().debug( inputFile + " unchanged, not regenerating..." );
329 }
330 }
331 }
332 }
333
334
335 public void renderDocument( Writer writer, RenderingContext renderingContext, SiteRenderingContext context )
336 throws RendererException, FileNotFoundException, UnsupportedEncodingException
337 {
338 SiteRendererSink sink = new SiteRendererSink( renderingContext );
339
340 File doc = new File( renderingContext.getBasedir(), renderingContext.getInputName() );
341
342 Reader reader = null;
343 try
344 {
345 String resource = doc.getAbsolutePath();
346
347 Parser parser = doxia.getParser( renderingContext.getParserId() );
348
349
350 if ( renderingContext.getAttribute( "velocity" ) != null )
351 {
352 try
353 {
354 SiteResourceLoader.setResource( resource );
355
356 Context vc = createContext( sink, context );
357
358 StringWriter sw = new StringWriter();
359
360 velocity.getEngine().mergeTemplate( resource, context.getInputEncoding(), vc, sw );
361
362 reader = new StringReader( sw.toString() );
363 if ( parser.getType() == Parser.XML_TYPE && context.isValidate() )
364 {
365 reader = validate( reader, resource );
366 }
367 }
368 catch ( Exception e )
369 {
370 if ( getLogger().isDebugEnabled() )
371 {
372 getLogger().error( "Error parsing " + resource + " as a velocity template, using as text.", e );
373 }
374 else
375 {
376 getLogger().error( "Error parsing " + resource + " as a velocity template, using as text." );
377 }
378 }
379 }
380 else
381 {
382 switch ( parser.getType() )
383 {
384 case Parser.XML_TYPE:
385 reader = ReaderFactory.newXmlReader( doc );
386 if ( context.isValidate() )
387 {
388 reader = validate( reader, resource );
389 }
390 break;
391
392 case Parser.TXT_TYPE:
393 case Parser.UNKNOWN_TYPE:
394 default:
395 reader = ReaderFactory.newReader( doc, context.getInputEncoding() );
396 }
397 }
398 sink.enableLogging( new PlexusLoggerWrapper( getLogger() ) );
399 doxia.parse( reader, renderingContext.getParserId(), sink );
400 }
401 catch ( ParserNotFoundException e )
402 {
403 throw new RendererException( "Error getting a parser for '" + doc + "': " + e.getMessage(), e );
404 }
405 catch ( ParseException e )
406 {
407 throw new RendererException( "Error parsing '"
408 + doc + "': line [" + e.getLineNumber() + "] " + e.getMessage(), e );
409 }
410 catch ( IOException e )
411 {
412 throw new RendererException( "IOException when processing '" + doc + "'", e );
413 }
414 finally
415 {
416 sink.flush();
417
418 sink.close();
419
420 IOUtil.close( reader );
421 }
422
423 generateDocument( writer, sink, context );
424 }
425
426 private Context createContext( SiteRendererSink sink, SiteRenderingContext siteRenderingContext )
427 {
428 VelocityContext context = new VelocityContext();
429
430
431
432
433
434 RenderingContext renderingContext = sink.getRenderingContext();
435 context.put( "relativePath", renderingContext.getRelativePath() );
436
437
438 context.put( "authors", sink.getAuthors() );
439
440 context.put( "title", sink.getTitle() );
441
442 context.put( "headContent", sink.getHead() );
443
444 context.put( "bodyContent", sink.getBody() );
445
446 context.put( "decoration", siteRenderingContext.getDecoration() );
447
448 SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMdd" );
449 if ( StringUtils.isNotEmpty( sink.getDate() ) )
450 {
451 try
452 {
453
454 context.put( "dateCreation",
455 sdf.format( new SimpleDateFormat( "yyyy-MM-dd" ).parse( sink.getDate() ) ) );
456 }
457 catch ( java.text.ParseException e )
458 {
459 getLogger().debug( "Could not parse date: " + sink.getDate() + ", ignoring!", e );
460 }
461 }
462 context.put( "dateRevision", sdf.format( new Date() ) );
463
464 context.put( "currentDate", new Date() );
465
466 Locale locale = siteRenderingContext.getLocale();
467 context.put( "dateFormat", DateFormat.getDateInstance( DateFormat.DEFAULT, locale ) );
468
469 String currentFileName = renderingContext.getOutputName().replace( '\\', '/' );
470 context.put( "currentFileName", currentFileName );
471
472 context.put( "alignedFileName", PathTool.calculateLink( currentFileName, renderingContext.getRelativePath() ) );
473
474 context.put( "locale", locale );
475 context.put( "supportedLocales", Collections.unmodifiableList( siteRenderingContext.getSiteLocales() ) );
476
477
478 Map<String, ?> templateProperties = siteRenderingContext.getTemplateProperties();
479
480 if ( templateProperties != null )
481 {
482 for ( Map.Entry<String, ?> entry : templateProperties.entrySet() )
483 {
484 context.put( entry.getKey(), entry.getValue() );
485 }
486 }
487
488
489
490
491
492 context.put( "PathTool", new PathTool() );
493
494 context.put( "FileUtils", new FileUtils() );
495
496 context.put( "StringUtils", new StringUtils() );
497
498 context.put( "i18n", i18n );
499
500 return context;
501 }
502
503
504 public void generateDocument( Writer writer, SiteRendererSink sink, SiteRenderingContext siteRenderingContext )
505 throws RendererException
506 {
507 Context context = createContext( sink, siteRenderingContext );
508
509 writeTemplate( writer, context, siteRenderingContext );
510 }
511
512 private void writeTemplate( Writer writer, Context context, SiteRenderingContext siteContext )
513 throws RendererException
514 {
515 ClassLoader old = null;
516
517 if ( siteContext.getTemplateClassLoader() != null )
518 {
519
520
521
522
523 old = Thread.currentThread().getContextClassLoader();
524
525 Thread.currentThread().setContextClassLoader( siteContext.getTemplateClassLoader() );
526 }
527
528 try
529 {
530 processTemplate( siteContext.getTemplateName(), context, writer );
531 }
532 finally
533 {
534 IOUtil.close( writer );
535
536 if ( old != null )
537 {
538 Thread.currentThread().setContextClassLoader( old );
539 }
540 }
541 }
542
543
544
545
546 private void processTemplate( String templateName, Context context, Writer writer )
547 throws RendererException
548 {
549 Template template;
550
551 try
552 {
553 template = velocity.getEngine().getTemplate( templateName );
554 }
555 catch ( Exception e )
556 {
557 throw new RendererException( "Could not find the template '" + templateName, e );
558 }
559
560 try
561 {
562 template.merge( context, writer );
563 }
564 catch ( Exception e )
565 {
566 throw new RendererException( "Error while generating code.", e );
567 }
568 }
569
570
571 public SiteRenderingContext createContextForSkin( File skinFile, Map<String, ?> attributes, DecorationModel decoration,
572 String defaultWindowTitle, Locale locale )
573 throws IOException
574 {
575 SiteRenderingContext context = new SiteRenderingContext();
576
577
578 ZipFile zipFile = new ZipFile( skinFile );
579 try
580 {
581 if ( zipFile.getEntry( SKIN_TEMPLATE_LOCATION ) != null )
582 {
583 context.setTemplateName( SKIN_TEMPLATE_LOCATION );
584 context.setTemplateClassLoader( new URLClassLoader( new URL[]{skinFile.toURI().toURL()} ) );
585 }
586 else
587 {
588 context.setTemplateName( DEFAULT_TEMPLATE );
589 context.setTemplateClassLoader( getClass().getClassLoader() );
590 context.setUsingDefaultTemplate( true );
591 }
592 }
593 finally
594 {
595 closeZipFile( zipFile );
596 }
597
598 context.setTemplateProperties( attributes );
599 context.setLocale( locale );
600 context.setDecoration( decoration );
601 context.setDefaultWindowTitle( defaultWindowTitle );
602 context.setSkinJarFile( skinFile );
603
604 return context;
605 }
606
607
608 public SiteRenderingContext createContextForTemplate( File templateFile, File skinFile, Map<String, ?> attributes,
609 DecorationModel decoration, String defaultWindowTitle,
610 Locale locale )
611 throws MalformedURLException
612 {
613 SiteRenderingContext context = new SiteRenderingContext();
614
615 context.setTemplateName( templateFile.getName() );
616 context.setTemplateClassLoader( new URLClassLoader( new URL[]{templateFile.getParentFile().toURI().toURL()} ) );
617
618 context.setTemplateProperties( attributes );
619 context.setLocale( locale );
620 context.setDecoration( decoration );
621 context.setDefaultWindowTitle( defaultWindowTitle );
622 context.setSkinJarFile( skinFile );
623
624 return context;
625 }
626
627 private void closeZipFile( ZipFile zipFile )
628 {
629
630 try
631 {
632 zipFile.close();
633 }
634 catch ( IOException e )
635 {
636
637 }
638 }
639
640
641 public void copyResources( SiteRenderingContext siteRenderingContext, File resourcesDirectory, File outputDirectory )
642 throws IOException
643 {
644 if ( siteRenderingContext.getSkinJarFile() != null )
645 {
646
647 ZipFile file = new ZipFile( siteRenderingContext.getSkinJarFile() );
648 try
649 {
650 for ( Enumeration<? extends ZipEntry> e = file.entries(); e.hasMoreElements(); )
651 {
652 ZipEntry entry = e.nextElement();
653
654 if ( !entry.getName().startsWith( "META-INF/" ) )
655 {
656 File destFile = new File( outputDirectory, entry.getName() );
657 if ( !entry.isDirectory() )
658 {
659 destFile.getParentFile().mkdirs();
660
661 copyFileFromZip( file, entry, destFile );
662 }
663 else
664 {
665 destFile.mkdirs();
666 }
667 }
668 }
669 }
670 finally
671 {
672 file.close();
673 }
674 }
675
676 if ( siteRenderingContext.isUsingDefaultTemplate() )
677 {
678 InputStream resourceList = getClass().getClassLoader()
679 .getResourceAsStream( RESOURCE_DIR + "/resources.txt" );
680
681 if ( resourceList != null )
682 {
683 Reader r = null;
684 try
685 {
686 r = ReaderFactory.newReader( resourceList, ReaderFactory.UTF_8 );
687 LineNumberReader reader = new LineNumberReader( r );
688
689 String line = reader.readLine();
690
691 while ( line != null )
692 {
693 InputStream is = getClass().getClassLoader().getResourceAsStream( RESOURCE_DIR + "/" + line );
694
695 if ( is == null )
696 {
697 throw new IOException( "The resource " + line + " doesn't exist." );
698 }
699
700 File outputFile = new File( outputDirectory, line );
701
702 if ( !outputFile.getParentFile().exists() )
703 {
704 outputFile.getParentFile().mkdirs();
705 }
706
707 OutputStream os = null;
708 try
709 {
710
711 os = new FileOutputStream( outputFile );
712 IOUtil.copy( is, os );
713 }
714 finally
715 {
716 IOUtil.close( os );
717 }
718
719 IOUtil.close( is );
720
721 line = reader.readLine();
722 }
723 }
724 finally
725 {
726 IOUtil.close( r );
727 }
728 }
729 }
730
731
732 if ( resourcesDirectory != null && resourcesDirectory.exists() )
733 {
734 copyDirectory( resourcesDirectory, outputDirectory );
735 }
736
737
738 File siteCssFile = new File( outputDirectory, "/css/site.css" );
739 if ( !siteCssFile.exists() )
740 {
741
742 File cssDirectory = new File( outputDirectory, "/css/" );
743 boolean created = cssDirectory.mkdirs();
744 if ( created && getLogger().isDebugEnabled() )
745 {
746 getLogger().debug(
747 "The directory '" + cssDirectory.getAbsolutePath() + "' did not exist. It was created." );
748 }
749
750
751 if ( getLogger().isDebugEnabled() )
752 {
753 getLogger().debug(
754 "The file '" + siteCssFile.getAbsolutePath() + "' does not exists. Creating an empty file." );
755 }
756 Writer writer = null;
757 try
758 {
759 writer = WriterFactory.newWriter( siteCssFile, siteRenderingContext.getOutputEncoding() );
760
761 writer.write( "/* You can override this file with your own styles */" );
762 }
763 finally
764 {
765 IOUtil.close( writer );
766 }
767 }
768 }
769
770 private void copyFileFromZip( ZipFile file, ZipEntry entry, File destFile )
771 throws IOException
772 {
773 FileOutputStream fos = new FileOutputStream( destFile );
774
775 try
776 {
777 IOUtil.copy( file.getInputStream( entry ), fos );
778 }
779 finally
780 {
781 IOUtil.close( fos );
782 }
783 }
784
785
786
787
788
789
790
791
792 protected void copyDirectory( File source, File destination )
793 throws IOException
794 {
795 if ( source.exists() )
796 {
797 DirectoryScanner scanner = new DirectoryScanner();
798
799 String[] includedResources = {"**/**"};
800
801 scanner.setIncludes( includedResources );
802
803 scanner.addDefaultExcludes();
804
805 scanner.setBasedir( source );
806
807 scanner.scan();
808
809 List<String> includedFiles = Arrays.asList( scanner.getIncludedFiles() );
810
811 for ( String name : includedFiles )
812 {
813 File sourceFile = new File( source, name );
814
815 File destinationFile = new File( destination, name );
816
817 FileUtils.copyFile( sourceFile, destinationFile );
818 }
819 }
820 }
821
822 private Reader validate( Reader source, String resource )
823 throws ParseException, IOException
824 {
825 getLogger().debug( "Validating: " + resource );
826
827 try
828 {
829 String content = IOUtil.toString( new BufferedReader( source ) );
830
831 new XmlValidator( new PlexusLoggerWrapper( getLogger() ) ).validate( content );
832
833 return new StringReader( content );
834 }
835 finally
836 {
837 IOUtil.close( source );
838 }
839 }
840
841 }