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.FileOutputStream;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.io.Reader;
31  import java.io.StringReader;
32  import java.io.StringWriter;
33  import java.io.Writer;
34  import java.net.URL;
35  import java.net.URLClassLoader;
36  import java.util.Arrays;
37  import java.util.Collection;
38  import java.util.Collections;
39  import java.util.Enumeration;
40  import java.util.Iterator;
41  import java.util.LinkedHashMap;
42  import java.util.LinkedList;
43  import java.util.List;
44  import java.util.Locale;
45  import java.util.Map;
46  import java.util.Properties;
47  import java.util.TimeZone;
48  import java.util.zip.ZipEntry;
49  import java.util.zip.ZipException;
50  import java.util.zip.ZipFile;
51  
52  import org.apache.commons.lang3.ArrayUtils;
53  import org.apache.commons.lang3.SystemUtils;
54  import org.apache.maven.artifact.Artifact;
55  import org.apache.maven.artifact.versioning.ArtifactVersion;
56  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
57  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
58  import org.apache.maven.artifact.versioning.Restriction;
59  import org.apache.maven.artifact.versioning.VersionRange;
60  import org.apache.maven.doxia.Doxia;
61  import org.apache.maven.doxia.parser.ParseException;
62  import org.apache.maven.doxia.parser.Parser;
63  import org.apache.maven.doxia.parser.manager.ParserNotFoundException;
64  import org.apache.maven.doxia.parser.module.ParserModule;
65  import org.apache.maven.doxia.parser.module.ParserModuleManager;
66  import org.apache.maven.doxia.site.SiteModel;
67  import org.apache.maven.doxia.site.skin.SkinModel;
68  import org.apache.maven.doxia.site.skin.io.xpp3.SkinXpp3Reader;
69  import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
70  import org.apache.maven.doxia.util.XmlValidator;
71  import org.apache.velocity.Template;
72  import org.apache.velocity.context.Context;
73  import org.apache.velocity.exception.ParseErrorException;
74  import org.apache.velocity.exception.ResourceNotFoundException;
75  import org.apache.velocity.exception.VelocityException;
76  import org.apache.velocity.tools.Scope;
77  import org.apache.velocity.tools.ToolManager;
78  import org.apache.velocity.tools.config.ConfigurationUtils;
79  import org.apache.velocity.tools.config.EasyFactoryConfiguration;
80  import org.apache.velocity.tools.config.FactoryConfiguration;
81  import org.apache.velocity.tools.generic.AlternatorTool;
82  import org.apache.velocity.tools.generic.ClassTool;
83  import org.apache.velocity.tools.generic.ComparisonDateTool;
84  import org.apache.velocity.tools.generic.ContextTool;
85  import org.apache.velocity.tools.generic.ConversionTool;
86  import org.apache.velocity.tools.generic.DisplayTool;
87  import org.apache.velocity.tools.generic.EscapeTool;
88  import org.apache.velocity.tools.generic.FieldTool;
89  import org.apache.velocity.tools.generic.LinkTool;
90  import org.apache.velocity.tools.generic.LoopTool;
91  import org.apache.velocity.tools.generic.MathTool;
92  import org.apache.velocity.tools.generic.NumberTool;
93  import org.apache.velocity.tools.generic.RenderTool;
94  import org.apache.velocity.tools.generic.ResourceTool;
95  import org.apache.velocity.tools.generic.SortTool;
96  import org.apache.velocity.tools.generic.XmlTool;
97  import org.codehaus.plexus.PlexusContainer;
98  import org.codehaus.plexus.util.DirectoryScanner;
99  import org.codehaus.plexus.util.FileUtils;
100 import org.codehaus.plexus.util.IOUtil;
101 import org.codehaus.plexus.util.Os;
102 import org.codehaus.plexus.util.PathTool;
103 import org.codehaus.plexus.util.ReaderFactory;
104 import org.codehaus.plexus.util.StringUtils;
105 import org.codehaus.plexus.util.WriterFactory;
106 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
107 import org.codehaus.plexus.velocity.VelocityComponent;
108 import org.slf4j.Logger;
109 import org.slf4j.LoggerFactory;
110 
111 /**
112  * <p>DefaultSiteRenderer class.</p>
113  *
114  * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
115  * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
116  * @since 1.0
117  */
118 @Singleton
119 @Named
120 public class DefaultSiteRenderer implements Renderer {
121     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSiteRenderer.class);
122 
123     // ----------------------------------------------------------------------
124     // Requirements
125     // ----------------------------------------------------------------------
126 
127     @Inject
128     private VelocityComponent velocity;
129 
130     @Inject
131     private ParserModuleManager parserModuleManager;
132 
133     @Inject
134     private Doxia doxia;
135 
136     @Inject
137     private PlexusContainer plexus;
138 
139     private static final String SKIN_TEMPLATE_LOCATION = "META-INF/maven/site.vm";
140 
141     private static final String TOOLS_LOCATION = "META-INF/maven/site-tools.xml";
142 
143     private static final String DOXIA_SITE_RENDERER_VERSION = getSiteRendererVersion();
144 
145     // ----------------------------------------------------------------------
146     // SiteRenderer implementation
147     // ----------------------------------------------------------------------
148 
149     public Map<String, DocumentRenderer> locateDocumentFiles(SiteRenderingContext siteRenderingContext)
150             throws IOException, RendererException {
151         return locateDocumentFiles(siteRenderingContext, false);
152     }
153 
154     /** {@inheritDoc} */
155     public Map<String, DocumentRenderer> locateDocumentFiles(
156             SiteRenderingContext siteRenderingContext, boolean editable) throws IOException, RendererException {
157         Map<String, DocumentRenderer> files = new LinkedHashMap<String, DocumentRenderer>();
158         Map<String, String> moduleExcludes = siteRenderingContext.getModuleExcludes();
159 
160         // look in every site directory (in general src/site or target/generated-site)
161         for (File siteDirectory : siteRenderingContext.getSiteDirectories()) {
162             if (siteDirectory.exists()) {
163                 Collection<ParserModule> modules = parserModuleManager.getParserModules();
164                 // use every Doxia parser module
165                 for (ParserModule module : modules) {
166                     File moduleBasedir = new File(siteDirectory, module.getSourceDirectory());
167 
168                     String excludes = (moduleExcludes == null) ? null : moduleExcludes.get(module.getParserId());
169 
170                     addModuleFiles(
171                             siteRenderingContext.getRootDirectory(), moduleBasedir, module, excludes, files, editable);
172                 }
173             }
174         }
175 
176         return files;
177     }
178 
179     private List<String> filterExtensionIgnoreCase(List<String> fileNames, String extension) {
180         List<String> filtered = new LinkedList<String>(fileNames);
181         for (Iterator<String> it = filtered.iterator(); it.hasNext(); ) {
182             String name = it.next();
183 
184             // Take care of extension case
185             if (!endsWithIgnoreCase(name, extension)) {
186                 it.remove();
187             }
188         }
189         return filtered;
190     }
191 
192     private void addModuleFiles(
193             File rootDir,
194             File moduleBasedir,
195             ParserModule module,
196             String excludes,
197             Map<String, DocumentRenderer> files,
198             boolean editable)
199             throws IOException, RendererException {
200         if (!moduleBasedir.exists() || ArrayUtils.isEmpty(module.getExtensions())) {
201             return;
202         }
203 
204         String moduleRelativePath =
205                 PathTool.getRelativeFilePath(rootDir.getAbsolutePath(), moduleBasedir.getAbsolutePath());
206 
207         List<String> allFiles = FileUtils.getFileNames(moduleBasedir, "**/*", excludes, false);
208 
209         for (String extension : module.getExtensions()) {
210             String fullExtension = "." + extension;
211 
212             List<String> docs = filterExtensionIgnoreCase(allFiles, fullExtension);
213 
214             // *.<extension>.vm
215             List<String> velocityFiles = filterExtensionIgnoreCase(allFiles, fullExtension + ".vm");
216 
217             docs.addAll(velocityFiles);
218 
219             for (String doc : docs) {
220                 DocumentRenderingContext docRenderingContext = new DocumentRenderingContext(
221                         moduleBasedir, moduleRelativePath, doc, module.getParserId(), extension, editable);
222 
223                 // TODO: DOXIA-111: we need a general filter here that knows how to alter the context
224                 if (endsWithIgnoreCase(doc, ".vm")) {
225                     docRenderingContext.setAttribute("velocity", "true");
226                 }
227 
228                 String key = docRenderingContext.getOutputName();
229 
230                 if (files.containsKey(key)) {
231                     DocumentRenderer docRenderer = files.get(key);
232 
233                     DocumentRenderingContext originalDocRenderingContext = docRenderer.getRenderingContext();
234 
235                     File originalDoc = new File(
236                             originalDocRenderingContext.getBasedir(), originalDocRenderingContext.getInputName());
237 
238                     throw new RendererException("File '" + module.getSourceDirectory() + File.separator + doc
239                             + "' clashes with existing '" + originalDoc + "'.");
240                 }
241                 // -----------------------------------------------------------------------
242                 // Handle key without case differences
243                 // -----------------------------------------------------------------------
244                 for (Map.Entry<String, DocumentRenderer> entry : files.entrySet()) {
245                     if (entry.getKey().equalsIgnoreCase(key)) {
246                         DocumentRenderingContext originalDocRenderingContext =
247                                 entry.getValue().getRenderingContext();
248 
249                         File originalDoc = new File(
250                                 originalDocRenderingContext.getBasedir(), originalDocRenderingContext.getInputName());
251 
252                         if (Os.isFamily(Os.FAMILY_WINDOWS)) {
253                             throw new RendererException("File '" + module.getSourceDirectory() + File.separator + doc
254                                     + "' clashes with existing '" + originalDoc + "'.");
255                         }
256 
257                         if (LOGGER.isWarnEnabled()) {
258                             LOGGER.warn("File '" + module.getSourceDirectory() + File.separator + doc
259                                     + "' could clash with existing '" + originalDoc + "'.");
260                         }
261                     }
262                 }
263 
264                 files.put(key, new DoxiaDocumentRenderer(docRenderingContext));
265             }
266         }
267     }
268 
269     /** {@inheritDoc} */
270     public void render(
271             Collection<DocumentRenderer> documents, SiteRenderingContext siteRenderingContext, File outputDirectory)
272             throws RendererException, IOException {
273         for (DocumentRenderer docRenderer : documents) {
274             DocumentRenderingContext docRenderingContext = docRenderer.getRenderingContext();
275 
276             File outputFile = new File(outputDirectory, docRenderer.getOutputName());
277 
278             File inputFile = new File(docRenderingContext.getBasedir(), docRenderingContext.getInputName());
279 
280             boolean modified = !outputFile.exists()
281                     || (inputFile.lastModified() > outputFile.lastModified())
282                     || (siteRenderingContext.getSiteModel().getLastModified() > outputFile.lastModified());
283 
284             if (modified || docRenderer.isOverwrite()) {
285                 if (!outputFile.getParentFile().exists()) {
286                     outputFile.getParentFile().mkdirs();
287                 }
288 
289                 if (LOGGER.isDebugEnabled()) {
290                     LOGGER.debug("Generating " + outputFile);
291                 }
292 
293                 Writer writer = null;
294                 try {
295                     if (!docRenderer.isExternalReport()) {
296                         writer = WriterFactory.newWriter(outputFile, siteRenderingContext.getOutputEncoding());
297                     }
298                     docRenderer.renderDocument(writer, this, siteRenderingContext);
299                 } finally {
300                     IOUtil.close(writer);
301                 }
302             } else {
303                 if (LOGGER.isDebugEnabled()) {
304                     LOGGER.debug(inputFile + " unchanged, not regenerating...");
305                 }
306             }
307         }
308     }
309 
310     /** {@inheritDoc} */
311     public void renderDocument(
312             Writer writer, DocumentRenderingContext docRenderingContext, SiteRenderingContext siteContext)
313             throws RendererException {
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             DocumentRenderingContext docRenderingContext, SiteRenderingContext siteContext, String doxiaContent)
393             throws IOException {
394         if (!siteContext.getProcessedContentOutput().exists()) {
395             siteContext.getProcessedContentOutput().mkdirs();
396         }
397 
398         String inputName = docRenderingContext.getInputName();
399         // Remove .vm suffix
400         File outputFile =
401                 new File(siteContext.getProcessedContentOutput(), inputName.substring(0, inputName.length() - 3));
402 
403         File outputParent = outputFile.getParentFile();
404         if (!outputParent.exists()) {
405             outputParent.mkdirs();
406         }
407 
408         FileUtils.fileWrite(outputFile, siteContext.getInputEncoding(), doxiaContent);
409     }
410 
411     /**
412      * Creates a Velocity Context with all generic tools configured wit the site rendering context.
413      *
414      * @param siteRenderingContext the site rendering context
415      * @return a Velocity tools managed context
416      */
417     protected Context createToolManagedVelocityContext(SiteRenderingContext siteRenderingContext) {
418         Locale locale = siteRenderingContext.getLocale();
419         String dateFormat = siteRenderingContext.getSiteModel().getPublishDate().getFormat();
420         String timeZoneId = siteRenderingContext.getSiteModel().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 docRenderingContext the document's rendering context
466      * @param siteRenderingContext the site rendering context
467      * @return a Velocity tools managed context
468      */
469     protected Context createDocumentVelocityContext(
470             DocumentRenderingContext docRenderingContext, SiteRenderingContext siteRenderingContext) {
471         Context context = createToolManagedVelocityContext(siteRenderingContext);
472         // ----------------------------------------------------------------------
473         // Data objects
474         // ----------------------------------------------------------------------
475 
476         context.put("relativePath", docRenderingContext.getRelativePath());
477 
478         String currentFileName = docRenderingContext.getOutputName();
479         context.put("currentFileName", currentFileName);
480 
481         context.put("alignedFileName", PathTool.calculateLink(currentFileName, docRenderingContext.getRelativePath()));
482 
483         context.put("site", siteRenderingContext.getSiteModel());
484         // TODO Deprecated -- will be removed!
485         context.put("decoration", siteRenderingContext.getSiteModel());
486 
487         context.put("locale", siteRenderingContext.getLocale());
488         context.put("supportedLocales", Collections.unmodifiableList(siteRenderingContext.getSiteLocales()));
489 
490         context.put("publishDate", siteRenderingContext.getPublishDate());
491 
492         if (DOXIA_SITE_RENDERER_VERSION != null) {
493             context.put("doxiaSiteRendererVersion", DOXIA_SITE_RENDERER_VERSION);
494         }
495 
496         // Add user properties
497         Map<String, ?> templateProperties = siteRenderingContext.getTemplateProperties();
498 
499         if (templateProperties != null) {
500             for (Map.Entry<String, ?> entry : templateProperties.entrySet()) {
501                 context.put(entry.getKey(), entry.getValue());
502             }
503         }
504 
505         // ----------------------------------------------------------------------
506         // Tools
507         // ----------------------------------------------------------------------
508 
509         context.put("PathTool", new PathTool());
510 
511         context.put("StringUtils", new StringUtils());
512 
513         context.put("plexus", plexus);
514         return context;
515     }
516 
517     /**
518      * Create a Velocity Context for the site template decorating the document. In addition to all the informations
519      * from the document, this context contains data gathered in {@link SiteRendererSink} during document rendering.
520      *
521      * @param content the document content to be merged into the template
522      * @param siteRenderingContext the site rendering context
523      * @return a Velocity tools managed context
524      */
525     protected Context createSiteTemplateVelocityContext(
526             DocumentContent content, SiteRenderingContext siteRenderingContext) {
527         // first get the context from document
528         Context context = createDocumentVelocityContext(content.getRenderingContext(), siteRenderingContext);
529 
530         // then add data objects from rendered document
531 
532         // Add infos from document
533         context.put("authors", content.getAuthors());
534 
535         context.put("shortTitle", content.getTitle());
536 
537         // DOXIASITETOOLS-70: Prepend the project name to the title, if any
538         StringBuilder title = new StringBuilder();
539         if (siteRenderingContext.getSiteModel() != null
540                 && StringUtils.isNotEmpty(siteRenderingContext.getSiteModel().getName())) {
541             title.append(siteRenderingContext.getSiteModel().getName());
542         } else if (StringUtils.isNotEmpty(siteRenderingContext.getDefaultTitle())) {
543             title.append(siteRenderingContext.getDefaultTitle());
544         }
545 
546         if (title.length() > 0 && StringUtils.isNotEmpty(content.getTitle())) {
547             title.append(" \u2013 "); // Symbol Name: En Dash
548         }
549         if (StringUtils.isNotEmpty(content.getTitle())) {
550             title.append(content.getTitle());
551         }
552 
553         context.put("title", title.length() > 0 ? title.toString() : null);
554 
555         context.put("headContent", content.getHead());
556 
557         context.put("bodyContent", content.getBody());
558 
559         // document date (got from Doxia Sink date() API)
560         context.put("documentDate", content.getDate());
561 
562         // document rendering context, to get eventual inputName
563         context.put("docRenderingContext", content.getRenderingContext());
564 
565         return context;
566     }
567 
568     public void generateDocument(Writer writer, SiteRendererSink sink, SiteRenderingContext siteRenderingContext)
569             throws RendererException {
570         mergeDocumentIntoSite(writer, sink, siteRenderingContext);
571     }
572 
573     /** {@inheritDoc} */
574     public void mergeDocumentIntoSite(Writer writer, DocumentContent content, SiteRenderingContext siteRenderingContext)
575             throws RendererException {
576         String templateName = siteRenderingContext.getTemplateName();
577 
578         LOGGER.debug("Processing Velocity for template " + templateName + " on "
579                 + content.getRenderingContext().getInputName());
580 
581         Context context = createSiteTemplateVelocityContext(content, siteRenderingContext);
582 
583         ClassLoader old = null;
584 
585         if (siteRenderingContext.getTemplateClassLoader() != null) {
586             // -------------------------------------------------------------------------
587             // If no template classloader was set we'll just use the context classloader
588             // -------------------------------------------------------------------------
589 
590             old = Thread.currentThread().getContextClassLoader();
591 
592             Thread.currentThread().setContextClassLoader(siteRenderingContext.getTemplateClassLoader());
593         }
594 
595         try {
596             Template template;
597             Artifact skin = siteRenderingContext.getSkin();
598 
599             try {
600                 SkinModel skinModel = siteRenderingContext.getSkinModel();
601                 String encoding = (skinModel == null) ? null : skinModel.getEncoding();
602 
603                 template = (encoding == null)
604                         ? velocity.getEngine().getTemplate(templateName)
605                         : velocity.getEngine().getTemplate(templateName, encoding);
606             } catch (ParseErrorException pee) {
607                 throw new RendererException(
608                         "Velocity parsing error while reading the site template " + "from " + skin.getId() + " skin",
609                         pee);
610             } catch (ResourceNotFoundException rnfe) {
611                 throw new RendererException(
612                         "Could not find the site 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 template.", ve);
621             } catch (IOException ioe) {
622                 throw new RendererException("IO exception while merging site 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, SiteModel siteModel, String defaultTitle, Locale locale) {
635         SiteRenderingContext context = new SiteRenderingContext();
636 
637         context.setTemplateProperties(attributes);
638         context.setLocale(locale);
639         context.setSiteModel(siteModel);
640         context.setDefaultTitle(defaultTitle);
641 
642         return context;
643     }
644 
645     /** {@inheritDoc} */
646     public SiteRenderingContext createContextForSkin(
647             Artifact skin, Map<String, ?> attributes, SiteModel siteModel, String defaultTitle, Locale locale)
648             throws IOException, RendererException {
649         SiteRenderingContext context = createSiteRenderingContext(attributes, siteModel, 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 outputDirectory) throws IOException {
733         ZipFile file = getZipFile(siteRenderingContext.getSkin().getFile());
734 
735         try {
736             for (Enumeration<? extends ZipEntry> e = file.entries(); e.hasMoreElements(); ) {
737                 ZipEntry entry = e.nextElement();
738 
739                 if (!entry.getName().startsWith("META-INF/")) {
740                     File destFile = new File(outputDirectory, entry.getName());
741                     if (!entry.isDirectory()) {
742                         if (destFile.exists()) {
743                             // don't override existing content: avoids extra rewrite with same content or extra site
744                             // resource
745                             continue;
746                         }
747 
748                         destFile.getParentFile().mkdirs();
749 
750                         copyFileFromZip(file, entry, destFile);
751                     } else {
752                         destFile.mkdirs();
753                     }
754                 }
755             }
756         } finally {
757             closeZipFile(file);
758         }
759 
760         // Copy extra site resources
761         for (File siteDirectory : siteRenderingContext.getSiteDirectories()) {
762             File resourcesDirectory = new File(siteDirectory, "resources");
763 
764             if (resourcesDirectory != null && resourcesDirectory.exists()) {
765                 copyDirectory(resourcesDirectory, outputDirectory);
766             }
767         }
768 
769         // Check for the existence of /css/site.css
770         File siteCssFile = new File(outputDirectory, "/css/site.css");
771         if (!siteCssFile.exists()) {
772             // Create the subdirectory css if it doesn't exist, DOXIA-151
773             File cssDirectory = new File(outputDirectory, "/css/");
774             boolean created = cssDirectory.mkdirs();
775             if (created && LOGGER.isDebugEnabled()) {
776                 LOGGER.debug("The directory '" + cssDirectory.getAbsolutePath() + "' did not exist. It was created.");
777             }
778 
779             // If the file is not there - create an empty file, DOXIA-86
780             if (LOGGER.isDebugEnabled()) {
781                 LOGGER.debug(
782                         "The file '" + siteCssFile.getAbsolutePath() + "' does not exist. Creating an empty file.");
783             }
784             Writer writer = null;
785             try {
786                 writer = WriterFactory.newWriter(siteCssFile, siteRenderingContext.getOutputEncoding());
787                 // DOXIA-290...the file should not be 0 bytes.
788                 writer.write("/* You can override this file with your own styles */");
789             } finally {
790                 IOUtil.close(writer);
791             }
792         }
793     }
794 
795     private static void copyFileFromZip(ZipFile file, ZipEntry entry, File destFile) throws IOException {
796         FileOutputStream fos = new FileOutputStream(destFile);
797 
798         try {
799             IOUtil.copy(file.getInputStream(entry), fos);
800         } finally {
801             IOUtil.close(fos);
802         }
803     }
804 
805     /**
806      * Copy the directory
807      *
808      * @param source      source file to be copied
809      * @param destination destination file
810      * @throws java.io.IOException if any
811      */
812     protected void copyDirectory(File source, File destination) throws IOException {
813         if (source.exists()) {
814             DirectoryScanner scanner = new DirectoryScanner();
815 
816             String[] includedResources = {"**/*"};
817 
818             scanner.setIncludes(includedResources);
819 
820             scanner.addDefaultExcludes();
821 
822             scanner.setBasedir(source);
823 
824             scanner.scan();
825 
826             List<String> includedFiles = Arrays.asList(scanner.getIncludedFiles());
827 
828             for (String name : includedFiles) {
829                 File sourceFile = new File(source, name);
830 
831                 File destinationFile = new File(destination, name);
832 
833                 FileUtils.copyFile(sourceFile, destinationFile);
834             }
835         }
836     }
837 
838     private Reader validate(Reader source, String resource) throws ParseException, IOException {
839         LOGGER.debug("Validating: " + resource);
840 
841         try {
842             String content = IOUtil.toString(new BufferedReader(source));
843 
844             new XmlValidator().validate(content);
845 
846             return new StringReader(content);
847         } finally {
848             IOUtil.close(source);
849         }
850     }
851 
852     // TODO replace with StringUtils.endsWithIgnoreCase() from maven-shared-utils 0.7
853     static boolean endsWithIgnoreCase(String str, String searchStr) {
854         if (str.length() < searchStr.length()) {
855             return false;
856         }
857 
858         return str.regionMatches(true, str.length() - searchStr.length(), searchStr, 0, searchStr.length());
859     }
860 
861     private static ZipFile getZipFile(File file) throws IOException {
862         if (file == null) {
863             throw new IOException("Error opening ZipFile: null");
864         }
865 
866         try {
867             // TODO: plexus-archiver, if it could do the excludes
868             return new ZipFile(file);
869         } catch (ZipException ex) {
870             IOException ioe = new IOException("Error opening ZipFile: " + file.getAbsolutePath());
871             ioe.initCause(ex);
872             throw ioe;
873         }
874     }
875 
876     private static void closeZipFile(ZipFile zipFile) {
877         // TODO: move to plexus utils
878         try {
879             zipFile.close();
880         } catch (IOException e) {
881             // ignore
882         }
883     }
884 
885     private static String getSiteRendererVersion() {
886         InputStream inputStream = DefaultSiteRenderer.class.getResourceAsStream(
887                 "/META-INF/" + "maven/org.apache.maven.doxia/doxia-site-renderer/pom.properties");
888         if (inputStream == null) {
889             LOGGER.debug("pom.properties for doxia-site-renderer not found");
890         } else {
891             Properties properties = new Properties();
892             try (InputStream in = inputStream) {
893                 properties.load(in);
894                 return properties.getProperty("version");
895             } catch (IOException e) {
896                 LOGGER.debug("Failed to load pom.properties, so Doxia SiteRenderer version will not be available", e);
897             }
898         }
899 
900         return null;
901     }
902 }