View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.doxia.siterenderer;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.BufferedReader;
26  import java.io.File;
27  import java.io.FileNotFoundException;
28  import java.io.FileOutputStream;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.io.Reader;
32  import java.io.StringReader;
33  import java.io.StringWriter;
34  import java.io.UnsupportedEncodingException;
35  import java.io.Writer;
36  import java.net.URL;
37  import java.net.URLClassLoader;
38  import java.util.Arrays;
39  import java.util.Collection;
40  import java.util.Collections;
41  import java.util.Enumeration;
42  import java.util.Iterator;
43  import java.util.LinkedHashMap;
44  import java.util.LinkedList;
45  import java.util.List;
46  import java.util.Locale;
47  import java.util.Map;
48  import java.util.Properties;
49  import java.util.TimeZone;
50  import java.util.zip.ZipEntry;
51  import java.util.zip.ZipException;
52  import java.util.zip.ZipFile;
53  
54  import org.apache.commons.lang3.ArrayUtils;
55  import org.apache.commons.lang3.SystemUtils;
56  import org.apache.maven.artifact.Artifact;
57  import org.apache.maven.artifact.versioning.ArtifactVersion;
58  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
59  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
60  import org.apache.maven.artifact.versioning.Restriction;
61  import org.apache.maven.artifact.versioning.VersionRange;
62  import org.apache.maven.doxia.Doxia;
63  import org.apache.maven.doxia.parser.ParseException;
64  import org.apache.maven.doxia.parser.Parser;
65  import org.apache.maven.doxia.parser.manager.ParserNotFoundException;
66  import org.apache.maven.doxia.parser.module.ParserModule;
67  import org.apache.maven.doxia.parser.module.ParserModuleManager;
68  import org.apache.maven.doxia.site.decoration.DecorationModel;
69  import org.apache.maven.doxia.site.skin.SkinModel;
70  import org.apache.maven.doxia.site.skin.io.xpp3.SkinXpp3Reader;
71  import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
72  import org.apache.maven.doxia.util.XmlValidator;
73  import org.apache.velocity.Template;
74  import org.apache.velocity.context.Context;
75  import org.apache.velocity.exception.ParseErrorException;
76  import org.apache.velocity.exception.ResourceNotFoundException;
77  import org.apache.velocity.exception.VelocityException;
78  import org.apache.velocity.tools.Scope;
79  import org.apache.velocity.tools.ToolManager;
80  import org.apache.velocity.tools.config.ConfigurationUtils;
81  import org.apache.velocity.tools.config.EasyFactoryConfiguration;
82  import org.apache.velocity.tools.config.FactoryConfiguration;
83  import org.apache.velocity.tools.generic.AlternatorTool;
84  import org.apache.velocity.tools.generic.ClassTool;
85  import org.apache.velocity.tools.generic.ComparisonDateTool;
86  import org.apache.velocity.tools.generic.ContextTool;
87  import org.apache.velocity.tools.generic.ConversionTool;
88  import org.apache.velocity.tools.generic.DisplayTool;
89  import org.apache.velocity.tools.generic.EscapeTool;
90  import org.apache.velocity.tools.generic.FieldTool;
91  import org.apache.velocity.tools.generic.LinkTool;
92  import org.apache.velocity.tools.generic.LoopTool;
93  import org.apache.velocity.tools.generic.MathTool;
94  import org.apache.velocity.tools.generic.NumberTool;
95  import org.apache.velocity.tools.generic.RenderTool;
96  import org.apache.velocity.tools.generic.ResourceTool;
97  import org.apache.velocity.tools.generic.SortTool;
98  import org.apache.velocity.tools.generic.XmlTool;
99  import org.codehaus.plexus.PlexusContainer;
100 import org.codehaus.plexus.util.DirectoryScanner;
101 import org.codehaus.plexus.util.FileUtils;
102 import org.codehaus.plexus.util.IOUtil;
103 import org.codehaus.plexus.util.Os;
104 import org.codehaus.plexus.util.PathTool;
105 import org.codehaus.plexus.util.ReaderFactory;
106 import org.codehaus.plexus.util.StringUtils;
107 import org.codehaus.plexus.util.WriterFactory;
108 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
109 import org.codehaus.plexus.velocity.VelocityComponent;
110 import org.slf4j.Logger;
111 import org.slf4j.LoggerFactory;
112 
113 /**
114  * <p>DefaultSiteRenderer class.</p>
115  *
116  * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
117  * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
118  * @since 1.0
119  */
120 @Singleton
121 @Named
122 public class DefaultSiteRenderer implements Renderer {
123     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSiteRenderer.class);
124 
125     // ----------------------------------------------------------------------
126     // Requirements
127     // ----------------------------------------------------------------------
128 
129     @Inject
130     private VelocityComponent velocity;
131 
132     @Inject
133     private ParserModuleManager parserModuleManager;
134 
135     @Inject
136     private Doxia doxia;
137 
138     @Inject
139     private PlexusContainer plexus;
140 
141     private static final String SKIN_TEMPLATE_LOCATION = "META-INF/maven/site.vm";
142 
143     private static final String TOOLS_LOCATION = "META-INF/maven/site-tools.xml";
144 
145     private static final String DOXIA_SITE_RENDERER_VERSION = getSiteRendererVersion();
146 
147     // ----------------------------------------------------------------------
148     // Renderer implementation
149     // ----------------------------------------------------------------------
150 
151     /** {@inheritDoc} */
152     public Map<String, DocumentRenderer> locateDocumentFiles(SiteRenderingContext siteRenderingContext)
153             throws IOException, RendererException {
154         return locateDocumentFiles(siteRenderingContext, false);
155     }
156 
157     /** {@inheritDoc} */
158     public Map<String, DocumentRenderer> locateDocumentFiles(
159             SiteRenderingContext siteRenderingContext, boolean editable) throws IOException, RendererException {
160         Map<String, DocumentRenderer> files = new LinkedHashMap<String, DocumentRenderer>();
161         Map<String, String> moduleExcludes = siteRenderingContext.getModuleExcludes();
162 
163         // look in every site directory (in general src/site or target/generated-site)
164         for (File siteDirectory : siteRenderingContext.getSiteDirectories()) {
165             if (siteDirectory.exists()) {
166                 Collection<ParserModule> modules = parserModuleManager.getParserModules();
167                 // use every Doxia parser module
168                 for (ParserModule module : modules) {
169                     File moduleBasedir = new File(siteDirectory, module.getSourceDirectory());
170 
171                     String excludes = (moduleExcludes == null) ? null : moduleExcludes.get(module.getParserId());
172 
173                     addModuleFiles(
174                             siteRenderingContext.getRootDirectory(), moduleBasedir, module, excludes, files, editable);
175                 }
176             }
177         }
178 
179         return files;
180     }
181 
182     private List<String> filterExtensionIgnoreCase(List<String> fileNames, String extension) {
183         List<String> filtered = new LinkedList<String>(fileNames);
184         for (Iterator<String> it = filtered.iterator(); it.hasNext(); ) {
185             String name = it.next();
186 
187             // Take care of extension case
188             if (!endsWithIgnoreCase(name, extension)) {
189                 it.remove();
190             }
191         }
192         return filtered;
193     }
194 
195     private void addModuleFiles(
196             File rootDir,
197             File moduleBasedir,
198             ParserModule module,
199             String excludes,
200             Map<String, DocumentRenderer> files,
201             boolean editable)
202             throws IOException, RendererException {
203         if (!moduleBasedir.exists() || ArrayUtils.isEmpty(module.getExtensions())) {
204             return;
205         }
206 
207         String moduleRelativePath =
208                 PathTool.getRelativeFilePath(rootDir.getAbsolutePath(), moduleBasedir.getAbsolutePath());
209 
210         List<String> allFiles = FileUtils.getFileNames(moduleBasedir, "**/*", excludes, false);
211 
212         for (String extension : module.getExtensions()) {
213             String fullExtension = "." + extension;
214 
215             List<String> docs = filterExtensionIgnoreCase(allFiles, fullExtension);
216 
217             // *.<extension>.vm
218             List<String> velocityFiles = filterExtensionIgnoreCase(allFiles, fullExtension + ".vm");
219 
220             docs.addAll(velocityFiles);
221 
222             for (String doc : docs) {
223                 RenderingContext context = new RenderingContext(
224                         moduleBasedir, moduleRelativePath, doc, module.getParserId(), extension, editable);
225 
226                 // TODO: DOXIA-111: we need a general filter here that knows how to alter the context
227                 if (endsWithIgnoreCase(doc, ".vm")) {
228                     context.setAttribute("velocity", "true");
229                 }
230 
231                 String key = context.getOutputName();
232                 key = StringUtils.replace(key, "\\", "/");
233 
234                 if (files.containsKey(key)) {
235                     DocumentRenderer renderer = files.get(key);
236 
237                     RenderingContext originalContext = renderer.getRenderingContext();
238 
239                     File originalDoc = new File(originalContext.getBasedir(), originalContext.getInputName());
240 
241                     throw new RendererException("File '" + module.getSourceDirectory() + File.separator + doc
242                             + "' clashes with existing '" + originalDoc + "'.");
243                 }
244                 // -----------------------------------------------------------------------
245                 // Handle key without case differences
246                 // -----------------------------------------------------------------------
247                 for (Map.Entry<String, DocumentRenderer> entry : files.entrySet()) {
248                     if (entry.getKey().equalsIgnoreCase(key)) {
249                         RenderingContext originalContext = entry.getValue().getRenderingContext();
250 
251                         File originalDoc = new File(originalContext.getBasedir(), originalContext.getInputName());
252 
253                         if (Os.isFamily(Os.FAMILY_WINDOWS)) {
254                             throw new RendererException("File '" + module.getSourceDirectory() + File.separator + doc
255                                     + "' clashes with existing '" + originalDoc + "'.");
256                         }
257 
258                         if (LOGGER.isWarnEnabled()) {
259                             LOGGER.warn("File '" + module.getSourceDirectory() + File.separator + doc
260                                     + "' could clash with existing '" + originalDoc + "'.");
261                         }
262                     }
263                 }
264 
265                 files.put(key, new DoxiaDocumentRenderer(context));
266             }
267         }
268     }
269 
270     /** {@inheritDoc} */
271     public void render(
272             Collection<DocumentRenderer> documents, SiteRenderingContext siteRenderingContext, File outputDirectory)
273             throws RendererException, IOException {
274         for (DocumentRenderer docRenderer : documents) {
275             RenderingContext renderingContext = docRenderer.getRenderingContext();
276 
277             File outputFile = new File(outputDirectory, docRenderer.getOutputName());
278 
279             File inputFile = new File(renderingContext.getBasedir(), renderingContext.getInputName());
280 
281             boolean modified = !outputFile.exists()
282                     || (inputFile.lastModified() > outputFile.lastModified())
283                     || (siteRenderingContext.getDecoration().getLastModified() > outputFile.lastModified());
284 
285             if (modified || docRenderer.isOverwrite()) {
286                 if (!outputFile.getParentFile().exists()) {
287                     outputFile.getParentFile().mkdirs();
288                 }
289 
290                 if (LOGGER.isDebugEnabled()) {
291                     LOGGER.debug("Generating " + outputFile);
292                 }
293 
294                 Writer writer = null;
295                 try {
296                     if (!docRenderer.isExternalReport()) {
297                         writer = WriterFactory.newWriter(outputFile, siteRenderingContext.getOutputEncoding());
298                     }
299                     docRenderer.renderDocument(writer, this, siteRenderingContext);
300                 } finally {
301                     IOUtil.close(writer);
302                 }
303             } else {
304                 if (LOGGER.isDebugEnabled()) {
305                     LOGGER.debug(inputFile + " unchanged, not regenerating...");
306                 }
307             }
308         }
309     }
310 
311     /** {@inheritDoc} */
312     public void renderDocument(Writer writer, RenderingContext docRenderingContext, SiteRenderingContext siteContext)
313             throws RendererException, FileNotFoundException, UnsupportedEncodingException {
314         SiteRendererSink sink = new SiteRendererSink(docRenderingContext);
315 
316         File doc = new File(docRenderingContext.getBasedir(), docRenderingContext.getInputName());
317 
318         Reader reader = null;
319         try {
320             String resource = doc.getAbsolutePath();
321 
322             Parser parser = doxia.getParser(docRenderingContext.getParserId());
323             // DOXIASITETOOLS-146 don't render comments from source markup
324             parser.setEmitComments(false);
325 
326             // TODO: DOXIA-111: the filter used here must be checked generally.
327             if (docRenderingContext.getAttribute("velocity") != null) {
328                 LOGGER.debug("Processing Velocity for " + docRenderingContext.getDoxiaSourcePath());
329                 try {
330                     Context vc = createDocumentVelocityContext(docRenderingContext, siteContext);
331 
332                     StringWriter sw = new StringWriter();
333 
334                     velocity.getEngine().mergeTemplate(resource, siteContext.getInputEncoding(), vc, sw);
335 
336                     String doxiaContent = sw.toString();
337 
338                     if (siteContext.getProcessedContentOutput() != null) {
339                         // save Velocity processing result, ie the Doxia content that will be parsed after
340                         saveVelocityProcessedContent(docRenderingContext, siteContext, doxiaContent);
341                     }
342 
343                     reader = new StringReader(doxiaContent);
344                 } catch (VelocityException e) {
345                     throw new RendererException(
346                             "Error parsing " + docRenderingContext.getDoxiaSourcePath() + " as a Velocity template", e);
347                 }
348 
349                 if (parser.getType() == Parser.XML_TYPE && siteContext.isValidate()) {
350                     reader = validate(reader, resource);
351                 }
352             } else {
353                 switch (parser.getType()) {
354                     case Parser.XML_TYPE:
355                         reader = ReaderFactory.newXmlReader(doc);
356                         if (siteContext.isValidate()) {
357                             reader = validate(reader, resource);
358                         }
359                         break;
360 
361                     case Parser.TXT_TYPE:
362                     case Parser.UNKNOWN_TYPE:
363                     default:
364                         reader = ReaderFactory.newReader(doc, siteContext.getInputEncoding());
365                 }
366             }
367 
368             doxia.parse(reader, docRenderingContext.getParserId(), sink, docRenderingContext.getInputName());
369         } catch (ParserNotFoundException e) {
370             throw new RendererException("Error getting a parser for '" + doc + "'", e);
371         } catch (ParseException e) {
372             StringBuilder errorMsgBuilder = new StringBuilder();
373             errorMsgBuilder.append("Error parsing '").append(doc).append("'");
374             if (e.getLineNumber() > 0) {
375                 errorMsgBuilder.append(", line ").append(e.getLineNumber());
376             }
377             throw new RendererException(errorMsgBuilder.toString(), e);
378         } catch (IOException e) {
379             throw new RendererException("Error while processing '" + doc + "'", e);
380         } finally {
381             sink.flush();
382 
383             sink.close();
384 
385             IOUtil.close(reader);
386         }
387 
388         mergeDocumentIntoSite(writer, (DocumentContent) sink, siteContext);
389     }
390 
391     private void saveVelocityProcessedContent(
392             RenderingContext docRenderingContext, SiteRenderingContext siteContext, String doxiaContent)
393             throws IOException {
394         if (!siteContext.getProcessedContentOutput().exists()) {
395             siteContext.getProcessedContentOutput().mkdirs();
396         }
397 
398         String input = docRenderingContext.getInputName();
399         File outputFile = new File(siteContext.getProcessedContentOutput(), input.substring(0, input.length() - 3));
400 
401         File outputParent = outputFile.getParentFile();
402         if (!outputParent.exists()) {
403             outputParent.mkdirs();
404         }
405 
406         FileUtils.fileWrite(outputFile, siteContext.getInputEncoding(), doxiaContent);
407     }
408 
409     /**
410      * Creates a Velocity Context with all generic tools configured wit the site rendering context.
411      *
412      * @param siteRenderingContext the site rendering context
413      * @return a Velocity tools managed context
414      */
415     protected Context createToolManagedVelocityContext(SiteRenderingContext siteRenderingContext) {
416         Locale locale = siteRenderingContext.getLocale();
417         String dateFormat =
418                 siteRenderingContext.getDecoration().getPublishDate().getFormat();
419         String timeZoneId =
420                 siteRenderingContext.getDecoration().getPublishDate().getTimezone();
421         TimeZone timeZone =
422                 "system".equalsIgnoreCase(timeZoneId) ? TimeZone.getDefault() : TimeZone.getTimeZone(timeZoneId);
423 
424         EasyFactoryConfiguration config = new EasyFactoryConfiguration(false);
425         config.property("safeMode", Boolean.FALSE);
426         config.toolbox(Scope.REQUEST)
427                 .tool(ContextTool.class)
428                 .tool(LinkTool.class)
429                 .tool(LoopTool.class)
430                 .tool(RenderTool.class);
431         config.toolbox(Scope.APPLICATION)
432                 .property("locale", locale)
433                 .tool(AlternatorTool.class)
434                 .tool(ClassTool.class)
435                 .tool(ComparisonDateTool.class)
436                 .property("format", dateFormat)
437                 .property("timezone", timeZone)
438                 .tool(ConversionTool.class)
439                 .property("dateFormat", dateFormat)
440                 .tool(DisplayTool.class)
441                 .tool(EscapeTool.class)
442                 .tool(FieldTool.class)
443                 .tool(MathTool.class)
444                 .tool(NumberTool.class)
445                 .tool(ResourceTool.class)
446                 .property("bundles", new String[] {"site-renderer"})
447                 .tool(SortTool.class)
448                 .tool(XmlTool.class);
449 
450         FactoryConfiguration customConfig = ConfigurationUtils.findInClasspath(TOOLS_LOCATION);
451 
452         if (customConfig != null) {
453             config.addConfiguration(customConfig);
454         }
455 
456         ToolManager manager = new ToolManager(false, false);
457         manager.configure(config);
458 
459         return manager.createContext();
460     }
461 
462     /**
463      * Create a Velocity Context for a Doxia document, containing every information about rendered document.
464      *
465      * @param renderingContext the document's RenderingContext
466      * @param siteRenderingContext the site rendering context
467      * @return a Velocity tools managed context
468      */
469     protected Context createDocumentVelocityContext(
470             RenderingContext renderingContext, SiteRenderingContext siteRenderingContext) {
471         Context context = createToolManagedVelocityContext(siteRenderingContext);
472         // ----------------------------------------------------------------------
473         // Data objects
474         // ----------------------------------------------------------------------
475 
476         context.put("relativePath", renderingContext.getRelativePath());
477 
478         String currentFileName = renderingContext.getOutputName().replace('\\', '/');
479         context.put("currentFileName", currentFileName);
480 
481         context.put("alignedFileName", PathTool.calculateLink(currentFileName, renderingContext.getRelativePath()));
482 
483         context.put("decoration", siteRenderingContext.getDecoration());
484 
485         context.put("locale", siteRenderingContext.getLocale());
486         context.put("supportedLocales", Collections.unmodifiableList(siteRenderingContext.getSiteLocales()));
487 
488         context.put("publishDate", siteRenderingContext.getPublishDate());
489 
490         if (DOXIA_SITE_RENDERER_VERSION != null) {
491             context.put("doxiaSiteRendererVersion", DOXIA_SITE_RENDERER_VERSION);
492         }
493 
494         // Add user properties
495         Map<String, ?> templateProperties = siteRenderingContext.getTemplateProperties();
496 
497         if (templateProperties != null) {
498             for (Map.Entry<String, ?> entry : templateProperties.entrySet()) {
499                 context.put(entry.getKey(), entry.getValue());
500             }
501         }
502 
503         // ----------------------------------------------------------------------
504         // Tools
505         // ----------------------------------------------------------------------
506 
507         context.put("PathTool", new PathTool());
508 
509         context.put("StringUtils", new StringUtils());
510 
511         context.put("plexus", plexus);
512         return context;
513     }
514 
515     /**
516      * Create a Velocity Context for the site template decorating the document. In addition to all the informations
517      * from the document, this context contains data gathered in {@link SiteRendererSink} during document rendering.
518      *
519      * @param content the document content to be merged into the template
520      * @param siteRenderingContext the site rendering context
521      * @return a Velocity tools managed context
522      */
523     protected Context createSiteTemplateVelocityContext(
524             DocumentContent content, SiteRenderingContext siteRenderingContext) {
525         // first get the context from document
526         Context context = createDocumentVelocityContext(content.getRenderingContext(), siteRenderingContext);
527 
528         // then add data objects from rendered document
529 
530         // Add infos from document
531         context.put("authors", content.getAuthors());
532 
533         context.put("shortTitle", content.getTitle());
534 
535         // DOXIASITETOOLS-70: Prepend the project name to the title, if any
536         StringBuilder title = new StringBuilder();
537         if (siteRenderingContext.getDecoration() != null
538                 && StringUtils.isNotEmpty(siteRenderingContext.getDecoration().getName())) {
539             title.append(siteRenderingContext.getDecoration().getName());
540         } else if (StringUtils.isNotEmpty(siteRenderingContext.getDefaultTitle())) {
541             title.append(siteRenderingContext.getDefaultTitle());
542         }
543 
544         if (title.length() > 0 && StringUtils.isNotEmpty(content.getTitle())) {
545             title.append(" &#x2013; "); // Symbol Name: En Dash, Html Entity: &ndash;
546         }
547         if (StringUtils.isNotEmpty(content.getTitle())) {
548             title.append(content.getTitle());
549         }
550 
551         context.put("title", title.length() > 0 ? title.toString() : null);
552 
553         context.put("headContent", content.getHead());
554 
555         context.put("bodyContent", content.getBody());
556 
557         // document date (got from Doxia Sink date() API)
558         context.put("documentDate", content.getDate());
559 
560         // document rendering context, to get eventual inputName
561         context.put("docRenderingContext", content.getRenderingContext());
562 
563         return context;
564     }
565 
566     /** {@inheritDoc} */
567     public void generateDocument(Writer writer, SiteRendererSink sink, SiteRenderingContext siteRenderingContext)
568             throws RendererException {
569         mergeDocumentIntoSite(writer, sink, siteRenderingContext);
570     }
571 
572     /** {@inheritDoc} */
573     public void mergeDocumentIntoSite(Writer writer, DocumentContent content, SiteRenderingContext siteRenderingContext)
574             throws RendererException {
575         String templateName = siteRenderingContext.getTemplateName();
576 
577         LOGGER.debug("Processing Velocity for template " + templateName + " on "
578                 + content.getRenderingContext().getInputName());
579 
580         Context context = createSiteTemplateVelocityContext(content, siteRenderingContext);
581 
582         ClassLoader old = null;
583 
584         if (siteRenderingContext.getTemplateClassLoader() != null) {
585             // -------------------------------------------------------------------------
586             // If no template classloader was set we'll just use the context classloader
587             // -------------------------------------------------------------------------
588 
589             old = Thread.currentThread().getContextClassLoader();
590 
591             Thread.currentThread().setContextClassLoader(siteRenderingContext.getTemplateClassLoader());
592         }
593 
594         try {
595             Template template;
596             Artifact skin = siteRenderingContext.getSkin();
597 
598             try {
599                 SkinModel skinModel = siteRenderingContext.getSkinModel();
600                 String encoding = (skinModel == null) ? null : skinModel.getEncoding();
601 
602                 template = (encoding == null)
603                         ? velocity.getEngine().getTemplate(templateName)
604                         : velocity.getEngine().getTemplate(templateName, encoding);
605             } catch (ParseErrorException pee) {
606                 throw new RendererException(
607                         "Velocity parsing error while reading the site decoration template " + "from " + skin.getId()
608                                 + " skin",
609                         pee);
610             } catch (ResourceNotFoundException rnfe) {
611                 throw new RendererException(
612                         "Could not find the site decoration template " + "from " + skin.getId() + " skin", rnfe);
613             }
614 
615             try {
616                 StringWriter sw = new StringWriter();
617                 template.merge(context, sw);
618                 writer.write(sw.toString().replaceAll("\r?\n", SystemUtils.LINE_SEPARATOR));
619             } catch (VelocityException ve) {
620                 throw new RendererException("Velocity error while merging site decoration template.", ve);
621             } catch (IOException ioe) {
622                 throw new RendererException("IO exception while merging site decoration template.", ioe);
623             }
624         } finally {
625             IOUtil.close(writer);
626 
627             if (old != null) {
628                 Thread.currentThread().setContextClassLoader(old);
629             }
630         }
631     }
632 
633     private SiteRenderingContext createSiteRenderingContext(
634             Map<String, ?> attributes, DecorationModel decoration, String defaultTitle, Locale locale) {
635         SiteRenderingContext context = new SiteRenderingContext();
636 
637         context.setTemplateProperties(attributes);
638         context.setLocale(locale);
639         context.setDecoration(decoration);
640         context.setDefaultTitle(defaultTitle);
641 
642         return context;
643     }
644 
645     /** {@inheritDoc} */
646     public SiteRenderingContext createContextForSkin(
647             Artifact skin, Map<String, ?> attributes, DecorationModel decoration, String defaultTitle, Locale locale)
648             throws IOException, RendererException {
649         SiteRenderingContext context = createSiteRenderingContext(attributes, decoration, defaultTitle, locale);
650 
651         context.setSkin(skin);
652 
653         ZipFile zipFile = getZipFile(skin.getFile());
654         InputStream in = null;
655 
656         try {
657             if (zipFile.getEntry(SKIN_TEMPLATE_LOCATION) == null) {
658                 throw new RendererException("Skin does not contain template at " + SKIN_TEMPLATE_LOCATION);
659             }
660             context.setTemplateName(SKIN_TEMPLATE_LOCATION);
661             context.setTemplateClassLoader(
662                     new URLClassLoader(new URL[] {skin.getFile().toURI().toURL()}));
663 
664             ZipEntry skinDescriptorEntry = zipFile.getEntry(SkinModel.SKIN_DESCRIPTOR_LOCATION);
665             if (skinDescriptorEntry != null) {
666                 in = zipFile.getInputStream(skinDescriptorEntry);
667 
668                 SkinModel skinModel = new SkinXpp3Reader().read(in);
669                 context.setSkinModel(skinModel);
670 
671                 String toolsPrerequisite = skinModel.getPrerequisites() == null
672                         ? null
673                         : skinModel.getPrerequisites().getDoxiaSitetools();
674 
675                 Package p = DefaultSiteRenderer.class.getPackage();
676                 String current = (p == null) ? null : p.getImplementationVersion();
677 
678                 if (StringUtils.isNotBlank(toolsPrerequisite)
679                         && (current != null)
680                         && !matchVersion(current, toolsPrerequisite)) {
681                     throw new RendererException("Cannot use skin: has " + toolsPrerequisite
682                             + " Doxia Sitetools prerequisite, but current is " + current);
683                 }
684             }
685         } catch (XmlPullParserException e) {
686             throw new RendererException(
687                     "Failed to parse " + SkinModel.SKIN_DESCRIPTOR_LOCATION + " skin descriptor from " + skin.getId()
688                             + " skin",
689                     e);
690         } finally {
691             IOUtil.close(in);
692             closeZipFile(zipFile);
693         }
694 
695         return context;
696     }
697 
698     boolean matchVersion(String current, String prerequisite) throws RendererException {
699         try {
700             ArtifactVersion v = new DefaultArtifactVersion(current);
701             VersionRange vr = VersionRange.createFromVersionSpec(prerequisite);
702 
703             boolean matched = false;
704             ArtifactVersion recommendedVersion = vr.getRecommendedVersion();
705             if (recommendedVersion == null) {
706                 List<Restriction> restrictions = vr.getRestrictions();
707                 for (Restriction restriction : restrictions) {
708                     if (restriction.containsVersion(v)) {
709                         matched = true;
710                         break;
711                     }
712                 }
713             } else {
714                 // only singular versions ever have a recommendedVersion
715                 @SuppressWarnings("unchecked")
716                 int compareTo = recommendedVersion.compareTo(v);
717                 matched = (compareTo <= 0);
718             }
719 
720             if (LOGGER.isDebugEnabled()) {
721                 LOGGER.debug("Skin doxia-sitetools prerequisite: " + prerequisite + ", current: " + current
722                         + ", matched = " + matched);
723             }
724 
725             return matched;
726         } catch (InvalidVersionSpecificationException e) {
727             throw new RendererException("Invalid skin doxia-sitetools prerequisite: " + prerequisite, e);
728         }
729     }
730 
731     /** {@inheritDoc} */
732     public void copyResources(SiteRenderingContext siteRenderingContext, File resourcesDirectory, File outputDirectory)
733             throws IOException {
734         throw new AssertionError("copyResources( SiteRenderingContext, File, File ) is deprecated.");
735     }
736 
737     /** {@inheritDoc} */
738     public void copyResources(SiteRenderingContext siteRenderingContext, File outputDirectory) throws IOException {
739         ZipFile file = getZipFile(siteRenderingContext.getSkin().getFile());
740 
741         try {
742             for (Enumeration<? extends ZipEntry> e = file.entries(); e.hasMoreElements(); ) {
743                 ZipEntry entry = e.nextElement();
744 
745                 if (!entry.getName().startsWith("META-INF/")) {
746                     File destFile = new File(outputDirectory, entry.getName());
747                     if (!entry.isDirectory()) {
748                         if (destFile.exists()) {
749                             // don't override existing content: avoids extra rewrite with same content or extra site
750                             // resource
751                             continue;
752                         }
753 
754                         destFile.getParentFile().mkdirs();
755 
756                         copyFileFromZip(file, entry, destFile);
757                     } else {
758                         destFile.mkdirs();
759                     }
760                 }
761             }
762         } finally {
763             closeZipFile(file);
764         }
765 
766         // Copy extra site resources
767         for (File siteDirectory : siteRenderingContext.getSiteDirectories()) {
768             File resourcesDirectory = new File(siteDirectory, "resources");
769 
770             if (resourcesDirectory != null && resourcesDirectory.exists()) {
771                 copyDirectory(resourcesDirectory, outputDirectory);
772             }
773         }
774 
775         // Check for the existence of /css/site.css
776         File siteCssFile = new File(outputDirectory, "/css/site.css");
777         if (!siteCssFile.exists()) {
778             // Create the subdirectory css if it doesn't exist, DOXIA-151
779             File cssDirectory = new File(outputDirectory, "/css/");
780             boolean created = cssDirectory.mkdirs();
781             if (created && LOGGER.isDebugEnabled()) {
782                 LOGGER.debug("The directory '" + cssDirectory.getAbsolutePath() + "' did not exist. It was created.");
783             }
784 
785             // If the file is not there - create an empty file, DOXIA-86
786             if (LOGGER.isDebugEnabled()) {
787                 LOGGER.debug(
788                         "The file '" + siteCssFile.getAbsolutePath() + "' does not exist. Creating an empty file.");
789             }
790             Writer writer = null;
791             try {
792                 writer = WriterFactory.newWriter(siteCssFile, siteRenderingContext.getOutputEncoding());
793                 // DOXIA-290...the file should not be 0 bytes.
794                 writer.write("/* You can override this file with your own styles */");
795             } finally {
796                 IOUtil.close(writer);
797             }
798         }
799     }
800 
801     private static void copyFileFromZip(ZipFile file, ZipEntry entry, File destFile) throws IOException {
802         FileOutputStream fos = new FileOutputStream(destFile);
803 
804         try {
805             IOUtil.copy(file.getInputStream(entry), fos);
806         } finally {
807             IOUtil.close(fos);
808         }
809     }
810 
811     /**
812      * Copy the directory
813      *
814      * @param source      source file to be copied
815      * @param destination destination file
816      * @throws java.io.IOException if any
817      */
818     protected void copyDirectory(File source, File destination) throws IOException {
819         if (source.exists()) {
820             DirectoryScanner scanner = new DirectoryScanner();
821 
822             String[] includedResources = {"**/*"};
823 
824             scanner.setIncludes(includedResources);
825 
826             scanner.addDefaultExcludes();
827 
828             scanner.setBasedir(source);
829 
830             scanner.scan();
831 
832             List<String> includedFiles = Arrays.asList(scanner.getIncludedFiles());
833 
834             for (String name : includedFiles) {
835                 File sourceFile = new File(source, name);
836 
837                 File destinationFile = new File(destination, name);
838 
839                 FileUtils.copyFile(sourceFile, destinationFile);
840             }
841         }
842     }
843 
844     private Reader validate(Reader source, String resource) throws ParseException, IOException {
845         LOGGER.debug("Validating: " + resource);
846 
847         try {
848             String content = IOUtil.toString(new BufferedReader(source));
849 
850             new XmlValidator().validate(content);
851 
852             return new StringReader(content);
853         } finally {
854             IOUtil.close(source);
855         }
856     }
857 
858     // TODO replace with StringUtils.endsWithIgnoreCase() from maven-shared-utils 0.7
859     static boolean endsWithIgnoreCase(String str, String searchStr) {
860         if (str.length() < searchStr.length()) {
861             return false;
862         }
863 
864         return str.regionMatches(true, str.length() - searchStr.length(), searchStr, 0, searchStr.length());
865     }
866 
867     private static ZipFile getZipFile(File file) throws IOException {
868         if (file == null) {
869             throw new IOException("Error opening ZipFile: null");
870         }
871 
872         try {
873             // TODO: plexus-archiver, if it could do the excludes
874             return new ZipFile(file);
875         } catch (ZipException ex) {
876             IOException ioe = new IOException("Error opening ZipFile: " + file.getAbsolutePath());
877             ioe.initCause(ex);
878             throw ioe;
879         }
880     }
881 
882     private static void closeZipFile(ZipFile zipFile) {
883         // TODO: move to plexus utils
884         try {
885             zipFile.close();
886         } catch (IOException e) {
887             // ignore
888         }
889     }
890 
891     private static String getSiteRendererVersion() {
892         InputStream inputStream = DefaultSiteRenderer.class.getResourceAsStream(
893                 "/META-INF/" + "maven/org.apache.maven.doxia/doxia-site-renderer/pom.properties");
894         if (inputStream == null) {
895             LOGGER.debug("pom.properties for doxia-site-renderer not found");
896         } else {
897             Properties properties = new Properties();
898             try (InputStream in = inputStream) {
899                 properties.load(in);
900                 return properties.getProperty("version");
901             } catch (IOException e) {
902                 LOGGER.debug("Failed to load pom.properties, so Doxia SiteRenderer version will not be available", e);
903             }
904         }
905 
906         return null;
907     }
908 }