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