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.SiteModel;
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     // SiteRenderer 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                 DocumentRenderingContext docRenderingContext = new DocumentRenderingContext(
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                     docRenderingContext.setAttribute("velocity", "true");
229                 }
230 
231                 String key = docRenderingContext.getOutputName();
232 
233                 if (files.containsKey(key)) {
234                     DocumentRenderer docRenderer = files.get(key);
235 
236                     DocumentRenderingContext originalDocRenderingContext = docRenderer.getRenderingContext();
237 
238                     File originalDoc = new File(
239                             originalDocRenderingContext.getBasedir(), originalDocRenderingContext.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                         DocumentRenderingContext originalDocRenderingContext =
250                                 entry.getValue().getRenderingContext();
251 
252                         File originalDoc = new File(
253                                 originalDocRenderingContext.getBasedir(), originalDocRenderingContext.getInputName());
254 
255                         if (Os.isFamily(Os.FAMILY_WINDOWS)) {
256                             throw new RendererException("File '" + module.getSourceDirectory() + File.separator + doc
257                                     + "' clashes with existing '" + originalDoc + "'.");
258                         }
259 
260                         if (LOGGER.isWarnEnabled()) {
261                             LOGGER.warn("File '" + module.getSourceDirectory() + File.separator + doc
262                                     + "' could clash with existing '" + originalDoc + "'.");
263                         }
264                     }
265                 }
266 
267                 files.put(key, new DoxiaDocumentRenderer(docRenderingContext));
268             }
269         }
270     }
271 
272     /** {@inheritDoc} */
273     public void render(
274             Collection<DocumentRenderer> documents, SiteRenderingContext siteRenderingContext, File outputDirectory)
275             throws RendererException, IOException {
276         for (DocumentRenderer docRenderer : documents) {
277             DocumentRenderingContext docRenderingContext = docRenderer.getRenderingContext();
278 
279             File outputFile = new File(outputDirectory, docRenderer.getOutputName());
280 
281             File inputFile = new File(docRenderingContext.getBasedir(), docRenderingContext.getInputName());
282 
283             boolean modified = !outputFile.exists()
284                     || (inputFile.lastModified() > outputFile.lastModified())
285                     || (siteRenderingContext.getSiteModel().getLastModified() > outputFile.lastModified());
286 
287             if (modified || docRenderer.isOverwrite()) {
288                 if (!outputFile.getParentFile().exists()) {
289                     outputFile.getParentFile().mkdirs();
290                 }
291 
292                 if (LOGGER.isDebugEnabled()) {
293                     LOGGER.debug("Generating " + outputFile);
294                 }
295 
296                 Writer writer = null;
297                 try {
298                     if (!docRenderer.isExternalReport()) {
299                         writer = WriterFactory.newWriter(outputFile, siteRenderingContext.getOutputEncoding());
300                     }
301                     docRenderer.renderDocument(writer, this, siteRenderingContext);
302                 } finally {
303                     IOUtil.close(writer);
304                 }
305             } else {
306                 if (LOGGER.isDebugEnabled()) {
307                     LOGGER.debug(inputFile + " unchanged, not regenerating...");
308                 }
309             }
310         }
311     }
312 
313     /** {@inheritDoc} */
314     public void renderDocument(
315             Writer writer, DocumentRenderingContext docRenderingContext, SiteRenderingContext siteContext)
316             throws RendererException, FileNotFoundException, UnsupportedEncodingException {
317         SiteRendererSink sink = new SiteRendererSink(docRenderingContext);
318 
319         File doc = new File(docRenderingContext.getBasedir(), docRenderingContext.getInputName());
320 
321         Reader reader = null;
322         try {
323             String resource = doc.getAbsolutePath();
324 
325             Parser parser = doxia.getParser(docRenderingContext.getParserId());
326             // DOXIASITETOOLS-146 don't render comments from source markup
327             parser.setEmitComments(false);
328 
329             // TODO: DOXIA-111: the filter used here must be checked generally.
330             if (docRenderingContext.getAttribute("velocity") != null) {
331                 LOGGER.debug("Processing Velocity for " + docRenderingContext.getDoxiaSourcePath());
332                 try {
333                     Context vc = createDocumentVelocityContext(docRenderingContext, siteContext);
334 
335                     StringWriter sw = new StringWriter();
336 
337                     velocity.getEngine().mergeTemplate(resource, siteContext.getInputEncoding(), vc, sw);
338 
339                     String doxiaContent = sw.toString();
340 
341                     if (siteContext.getProcessedContentOutput() != null) {
342                         // save Velocity processing result, ie the Doxia content that will be parsed after
343                         saveVelocityProcessedContent(docRenderingContext, siteContext, doxiaContent);
344                     }
345 
346                     reader = new StringReader(doxiaContent);
347                 } catch (VelocityException e) {
348                     throw new RendererException(
349                             "Error parsing " + docRenderingContext.getDoxiaSourcePath() + " as a Velocity template", e);
350                 }
351 
352                 if (parser.getType() == Parser.XML_TYPE && siteContext.isValidate()) {
353                     reader = validate(reader, resource);
354                 }
355             } else {
356                 switch (parser.getType()) {
357                     case Parser.XML_TYPE:
358                         reader = ReaderFactory.newXmlReader(doc);
359                         if (siteContext.isValidate()) {
360                             reader = validate(reader, resource);
361                         }
362                         break;
363 
364                     case Parser.TXT_TYPE:
365                     case Parser.UNKNOWN_TYPE:
366                     default:
367                         reader = ReaderFactory.newReader(doc, siteContext.getInputEncoding());
368                 }
369             }
370 
371             doxia.parse(reader, docRenderingContext.getParserId(), sink, docRenderingContext.getInputName());
372         } catch (ParserNotFoundException e) {
373             throw new RendererException("Error getting a parser for '" + doc + "'", e);
374         } catch (ParseException e) {
375             StringBuilder errorMsgBuilder = new StringBuilder();
376             errorMsgBuilder.append("Error parsing '").append(doc).append("'");
377             if (e.getLineNumber() > 0) {
378                 errorMsgBuilder.append(", line ").append(e.getLineNumber());
379             }
380             throw new RendererException(errorMsgBuilder.toString(), e);
381         } catch (IOException e) {
382             throw new RendererException("Error while processing '" + doc + "'", e);
383         } finally {
384             sink.flush();
385 
386             sink.close();
387 
388             IOUtil.close(reader);
389         }
390 
391         mergeDocumentIntoSite(writer, (DocumentContent) sink, siteContext);
392     }
393 
394     private void saveVelocityProcessedContent(
395             DocumentRenderingContext docRenderingContext, SiteRenderingContext siteContext, String doxiaContent)
396             throws IOException {
397         if (!siteContext.getProcessedContentOutput().exists()) {
398             siteContext.getProcessedContentOutput().mkdirs();
399         }
400 
401         String inputName = docRenderingContext.getInputName();
402         // Remove .vm suffix
403         File outputFile =
404                 new File(siteContext.getProcessedContentOutput(), inputName.substring(0, inputName.length() - 3));
405 
406         File outputParent = outputFile.getParentFile();
407         if (!outputParent.exists()) {
408             outputParent.mkdirs();
409         }
410 
411         FileUtils.fileWrite(outputFile, siteContext.getInputEncoding(), doxiaContent);
412     }
413 
414     /**
415      * Creates a Velocity Context with all generic tools configured wit the site rendering context.
416      *
417      * @param siteRenderingContext the site rendering context
418      * @return a Velocity tools managed context
419      */
420     protected Context createToolManagedVelocityContext(SiteRenderingContext siteRenderingContext) {
421         Locale locale = siteRenderingContext.getLocale();
422         String dateFormat = siteRenderingContext.getSiteModel().getPublishDate().getFormat();
423         String timeZoneId = siteRenderingContext.getSiteModel().getPublishDate().getTimezone();
424         TimeZone timeZone =
425                 "system".equalsIgnoreCase(timeZoneId) ? TimeZone.getDefault() : TimeZone.getTimeZone(timeZoneId);
426 
427         EasyFactoryConfiguration config = new EasyFactoryConfiguration(false);
428         config.property("safeMode", Boolean.FALSE);
429         config.toolbox(Scope.REQUEST)
430                 .tool(ContextTool.class)
431                 .tool(LinkTool.class)
432                 .tool(LoopTool.class)
433                 .tool(RenderTool.class);
434         config.toolbox(Scope.APPLICATION)
435                 .property("locale", locale)
436                 .tool(AlternatorTool.class)
437                 .tool(ClassTool.class)
438                 .tool(ComparisonDateTool.class)
439                 .property("format", dateFormat)
440                 .property("timezone", timeZone)
441                 .tool(ConversionTool.class)
442                 .property("dateFormat", dateFormat)
443                 .tool(DisplayTool.class)
444                 .tool(EscapeTool.class)
445                 .tool(FieldTool.class)
446                 .tool(MathTool.class)
447                 .tool(NumberTool.class)
448                 .tool(ResourceTool.class)
449                 .property("bundles", new String[] {"site-renderer"})
450                 .tool(SortTool.class)
451                 .tool(XmlTool.class);
452 
453         FactoryConfiguration customConfig = ConfigurationUtils.findInClasspath(TOOLS_LOCATION);
454 
455         if (customConfig != null) {
456             config.addConfiguration(customConfig);
457         }
458 
459         ToolManager manager = new ToolManager(false, false);
460         manager.configure(config);
461 
462         return manager.createContext();
463     }
464 
465     /**
466      * Create a Velocity Context for a Doxia document, containing every information about rendered document.
467      *
468      * @param docRenderingContext the document's rendering context
469      * @param siteRenderingContext the site rendering context
470      * @return a Velocity tools managed context
471      */
472     protected Context createDocumentVelocityContext(
473             DocumentRenderingContext docRenderingContext, SiteRenderingContext siteRenderingContext) {
474         Context context = createToolManagedVelocityContext(siteRenderingContext);
475         // ----------------------------------------------------------------------
476         // Data objects
477         // ----------------------------------------------------------------------
478 
479         context.put("relativePath", docRenderingContext.getRelativePath());
480 
481         String currentFileName = docRenderingContext.getOutputName();
482         context.put("currentFileName", currentFileName);
483 
484         context.put("alignedFileName", PathTool.calculateLink(currentFileName, docRenderingContext.getRelativePath()));
485 
486         context.put("site", siteRenderingContext.getSiteModel());
487         // TODO Deprecated -- will be removed!
488         context.put("decoration", siteRenderingContext.getSiteModel());
489 
490         context.put("locale", siteRenderingContext.getLocale());
491         context.put("supportedLocales", Collections.unmodifiableList(siteRenderingContext.getSiteLocales()));
492 
493         context.put("publishDate", siteRenderingContext.getPublishDate());
494 
495         if (DOXIA_SITE_RENDERER_VERSION != null) {
496             context.put("doxiaSiteRendererVersion", DOXIA_SITE_RENDERER_VERSION);
497         }
498 
499         // Add user properties
500         Map<String, ?> templateProperties = siteRenderingContext.getTemplateProperties();
501 
502         if (templateProperties != null) {
503             for (Map.Entry<String, ?> entry : templateProperties.entrySet()) {
504                 context.put(entry.getKey(), entry.getValue());
505             }
506         }
507 
508         // ----------------------------------------------------------------------
509         // Tools
510         // ----------------------------------------------------------------------
511 
512         context.put("PathTool", new PathTool());
513 
514         context.put("StringUtils", new StringUtils());
515 
516         context.put("plexus", plexus);
517         return context;
518     }
519 
520     /**
521      * Create a Velocity Context for the site template decorating the document. In addition to all the informations
522      * from the document, this context contains data gathered in {@link SiteRendererSink} during document rendering.
523      *
524      * @param content the document content to be merged into the template
525      * @param siteRenderingContext the site rendering context
526      * @return a Velocity tools managed context
527      */
528     protected Context createSiteTemplateVelocityContext(
529             DocumentContent content, SiteRenderingContext siteRenderingContext) {
530         // first get the context from document
531         Context context = createDocumentVelocityContext(content.getRenderingContext(), siteRenderingContext);
532 
533         // then add data objects from rendered document
534 
535         // Add infos from document
536         context.put("authors", content.getAuthors());
537 
538         context.put("shortTitle", content.getTitle());
539 
540         // DOXIASITETOOLS-70: Prepend the project name to the title, if any
541         StringBuilder title = new StringBuilder();
542         if (siteRenderingContext.getSiteModel() != null
543                 && StringUtils.isNotEmpty(siteRenderingContext.getSiteModel().getName())) {
544             title.append(siteRenderingContext.getSiteModel().getName());
545         } else if (StringUtils.isNotEmpty(siteRenderingContext.getDefaultTitle())) {
546             title.append(siteRenderingContext.getDefaultTitle());
547         }
548 
549         if (title.length() > 0 && StringUtils.isNotEmpty(content.getTitle())) {
550             title.append(" \u2013 "); // Symbol Name: En Dash
551         }
552         if (StringUtils.isNotEmpty(content.getTitle())) {
553             title.append(content.getTitle());
554         }
555 
556         context.put("title", title.length() > 0 ? title.toString() : null);
557 
558         context.put("headContent", content.getHead());
559 
560         context.put("bodyContent", content.getBody());
561 
562         // document date (got from Doxia Sink date() API)
563         context.put("documentDate", content.getDate());
564 
565         // document rendering context, to get eventual inputName
566         context.put("docRenderingContext", content.getRenderingContext());
567 
568         return context;
569     }
570 
571     /** {@inheritDoc} */
572     public void generateDocument(Writer writer, SiteRendererSink sink, SiteRenderingContext siteRenderingContext)
573             throws RendererException {
574         mergeDocumentIntoSite(writer, sink, siteRenderingContext);
575     }
576 
577     /** {@inheritDoc} */
578     public void mergeDocumentIntoSite(Writer writer, DocumentContent content, SiteRenderingContext siteRenderingContext)
579             throws RendererException {
580         String templateName = siteRenderingContext.getTemplateName();
581 
582         LOGGER.debug("Processing Velocity for template " + templateName + " on "
583                 + content.getRenderingContext().getInputName());
584 
585         Context context = createSiteTemplateVelocityContext(content, siteRenderingContext);
586 
587         ClassLoader old = null;
588 
589         if (siteRenderingContext.getTemplateClassLoader() != null) {
590             // -------------------------------------------------------------------------
591             // If no template classloader was set we'll just use the context classloader
592             // -------------------------------------------------------------------------
593 
594             old = Thread.currentThread().getContextClassLoader();
595 
596             Thread.currentThread().setContextClassLoader(siteRenderingContext.getTemplateClassLoader());
597         }
598 
599         try {
600             Template template;
601             Artifact skin = siteRenderingContext.getSkin();
602 
603             try {
604                 SkinModel skinModel = siteRenderingContext.getSkinModel();
605                 String encoding = (skinModel == null) ? null : skinModel.getEncoding();
606 
607                 template = (encoding == null)
608                         ? velocity.getEngine().getTemplate(templateName)
609                         : velocity.getEngine().getTemplate(templateName, encoding);
610             } catch (ParseErrorException pee) {
611                 throw new RendererException(
612                         "Velocity parsing error while reading the site template " + "from " + skin.getId() + " skin",
613                         pee);
614             } catch (ResourceNotFoundException rnfe) {
615                 throw new RendererException(
616                         "Could not find the site template " + "from " + skin.getId() + " skin", rnfe);
617             }
618 
619             try {
620                 StringWriter sw = new StringWriter();
621                 template.merge(context, sw);
622                 writer.write(sw.toString().replaceAll("\r?\n", SystemUtils.LINE_SEPARATOR));
623             } catch (VelocityException ve) {
624                 throw new RendererException("Velocity error while merging site template.", ve);
625             } catch (IOException ioe) {
626                 throw new RendererException("IO exception while merging site template.", ioe);
627             }
628         } finally {
629             IOUtil.close(writer);
630 
631             if (old != null) {
632                 Thread.currentThread().setContextClassLoader(old);
633             }
634         }
635     }
636 
637     private SiteRenderingContext createSiteRenderingContext(
638             Map<String, ?> attributes, SiteModel siteModel, String defaultTitle, Locale locale) {
639         SiteRenderingContext context = new SiteRenderingContext();
640 
641         context.setTemplateProperties(attributes);
642         context.setLocale(locale);
643         context.setSiteModel(siteModel);
644         context.setDefaultTitle(defaultTitle);
645 
646         return context;
647     }
648 
649     /** {@inheritDoc} */
650     public SiteRenderingContext createContextForSkin(
651             Artifact skin, Map<String, ?> attributes, SiteModel siteModel, String defaultTitle, Locale locale)
652             throws IOException, RendererException {
653         SiteRenderingContext context = createSiteRenderingContext(attributes, siteModel, defaultTitle, locale);
654 
655         context.setSkin(skin);
656 
657         ZipFile zipFile = getZipFile(skin.getFile());
658         InputStream in = null;
659 
660         try {
661             if (zipFile.getEntry(SKIN_TEMPLATE_LOCATION) == null) {
662                 throw new RendererException("Skin does not contain template at " + SKIN_TEMPLATE_LOCATION);
663             }
664             context.setTemplateName(SKIN_TEMPLATE_LOCATION);
665             context.setTemplateClassLoader(
666                     new URLClassLoader(new URL[] {skin.getFile().toURI().toURL()}));
667 
668             ZipEntry skinDescriptorEntry = zipFile.getEntry(SkinModel.SKIN_DESCRIPTOR_LOCATION);
669             if (skinDescriptorEntry != null) {
670                 in = zipFile.getInputStream(skinDescriptorEntry);
671 
672                 SkinModel skinModel = new SkinXpp3Reader().read(in);
673                 context.setSkinModel(skinModel);
674 
675                 String toolsPrerequisite = skinModel.getPrerequisites() == null
676                         ? null
677                         : skinModel.getPrerequisites().getDoxiaSitetools();
678 
679                 Package p = DefaultSiteRenderer.class.getPackage();
680                 String current = (p == null) ? null : p.getImplementationVersion();
681 
682                 if (StringUtils.isNotBlank(toolsPrerequisite)
683                         && (current != null)
684                         && !matchVersion(current, toolsPrerequisite)) {
685                     throw new RendererException("Cannot use skin: has " + toolsPrerequisite
686                             + " Doxia Sitetools prerequisite, but current is " + current);
687                 }
688             }
689         } catch (XmlPullParserException e) {
690             throw new RendererException(
691                     "Failed to parse " + SkinModel.SKIN_DESCRIPTOR_LOCATION + " skin descriptor from " + skin.getId()
692                             + " skin",
693                     e);
694         } finally {
695             IOUtil.close(in);
696             closeZipFile(zipFile);
697         }
698 
699         return context;
700     }
701 
702     boolean matchVersion(String current, String prerequisite) throws RendererException {
703         try {
704             ArtifactVersion v = new DefaultArtifactVersion(current);
705             VersionRange vr = VersionRange.createFromVersionSpec(prerequisite);
706 
707             boolean matched = false;
708             ArtifactVersion recommendedVersion = vr.getRecommendedVersion();
709             if (recommendedVersion == null) {
710                 List<Restriction> restrictions = vr.getRestrictions();
711                 for (Restriction restriction : restrictions) {
712                     if (restriction.containsVersion(v)) {
713                         matched = true;
714                         break;
715                     }
716                 }
717             } else {
718                 // only singular versions ever have a recommendedVersion
719                 @SuppressWarnings("unchecked")
720                 int compareTo = recommendedVersion.compareTo(v);
721                 matched = (compareTo <= 0);
722             }
723 
724             if (LOGGER.isDebugEnabled()) {
725                 LOGGER.debug("Skin doxia-sitetools prerequisite: " + prerequisite + ", current: " + current
726                         + ", matched = " + matched);
727             }
728 
729             return matched;
730         } catch (InvalidVersionSpecificationException e) {
731             throw new RendererException("Invalid skin doxia-sitetools prerequisite: " + prerequisite, e);
732         }
733     }
734 
735     /** {@inheritDoc} */
736     public void copyResources(SiteRenderingContext siteRenderingContext, File resourcesDirectory, File outputDirectory)
737             throws IOException {
738         throw new AssertionError("copyResources( SiteRenderingContext, File, File ) is deprecated.");
739     }
740 
741     /** {@inheritDoc} */
742     public void copyResources(SiteRenderingContext siteRenderingContext, File outputDirectory) throws IOException {
743         ZipFile file = getZipFile(siteRenderingContext.getSkin().getFile());
744 
745         try {
746             for (Enumeration<? extends ZipEntry> e = file.entries(); e.hasMoreElements(); ) {
747                 ZipEntry entry = e.nextElement();
748 
749                 if (!entry.getName().startsWith("META-INF/")) {
750                     File destFile = new File(outputDirectory, entry.getName());
751                     if (!entry.isDirectory()) {
752                         if (destFile.exists()) {
753                             // don't override existing content: avoids extra rewrite with same content or extra site
754                             // resource
755                             continue;
756                         }
757 
758                         destFile.getParentFile().mkdirs();
759 
760                         copyFileFromZip(file, entry, destFile);
761                     } else {
762                         destFile.mkdirs();
763                     }
764                 }
765             }
766         } finally {
767             closeZipFile(file);
768         }
769 
770         // Copy extra site resources
771         for (File siteDirectory : siteRenderingContext.getSiteDirectories()) {
772             File resourcesDirectory = new File(siteDirectory, "resources");
773 
774             if (resourcesDirectory != null && resourcesDirectory.exists()) {
775                 copyDirectory(resourcesDirectory, outputDirectory);
776             }
777         }
778 
779         // Check for the existence of /css/site.css
780         File siteCssFile = new File(outputDirectory, "/css/site.css");
781         if (!siteCssFile.exists()) {
782             // Create the subdirectory css if it doesn't exist, DOXIA-151
783             File cssDirectory = new File(outputDirectory, "/css/");
784             boolean created = cssDirectory.mkdirs();
785             if (created && LOGGER.isDebugEnabled()) {
786                 LOGGER.debug("The directory '" + cssDirectory.getAbsolutePath() + "' did not exist. It was created.");
787             }
788 
789             // If the file is not there - create an empty file, DOXIA-86
790             if (LOGGER.isDebugEnabled()) {
791                 LOGGER.debug(
792                         "The file '" + siteCssFile.getAbsolutePath() + "' does not exist. Creating an empty file.");
793             }
794             Writer writer = null;
795             try {
796                 writer = WriterFactory.newWriter(siteCssFile, siteRenderingContext.getOutputEncoding());
797                 // DOXIA-290...the file should not be 0 bytes.
798                 writer.write("/* You can override this file with your own styles */");
799             } finally {
800                 IOUtil.close(writer);
801             }
802         }
803     }
804 
805     private static void copyFileFromZip(ZipFile file, ZipEntry entry, File destFile) throws IOException {
806         FileOutputStream fos = new FileOutputStream(destFile);
807 
808         try {
809             IOUtil.copy(file.getInputStream(entry), fos);
810         } finally {
811             IOUtil.close(fos);
812         }
813     }
814 
815     /**
816      * Copy the directory
817      *
818      * @param source      source file to be copied
819      * @param destination destination file
820      * @throws java.io.IOException if any
821      */
822     protected void copyDirectory(File source, File destination) throws IOException {
823         if (source.exists()) {
824             DirectoryScanner scanner = new DirectoryScanner();
825 
826             String[] includedResources = {"**/*"};
827 
828             scanner.setIncludes(includedResources);
829 
830             scanner.addDefaultExcludes();
831 
832             scanner.setBasedir(source);
833 
834             scanner.scan();
835 
836             List<String> includedFiles = Arrays.asList(scanner.getIncludedFiles());
837 
838             for (String name : includedFiles) {
839                 File sourceFile = new File(source, name);
840 
841                 File destinationFile = new File(destination, name);
842 
843                 FileUtils.copyFile(sourceFile, destinationFile);
844             }
845         }
846     }
847 
848     private Reader validate(Reader source, String resource) throws ParseException, IOException {
849         LOGGER.debug("Validating: " + resource);
850 
851         try {
852             String content = IOUtil.toString(new BufferedReader(source));
853 
854             new XmlValidator().validate(content);
855 
856             return new StringReader(content);
857         } finally {
858             IOUtil.close(source);
859         }
860     }
861 
862     // TODO replace with StringUtils.endsWithIgnoreCase() from maven-shared-utils 0.7
863     static boolean endsWithIgnoreCase(String str, String searchStr) {
864         if (str.length() < searchStr.length()) {
865             return false;
866         }
867 
868         return str.regionMatches(true, str.length() - searchStr.length(), searchStr, 0, searchStr.length());
869     }
870 
871     private static ZipFile getZipFile(File file) throws IOException {
872         if (file == null) {
873             throw new IOException("Error opening ZipFile: null");
874         }
875 
876         try {
877             // TODO: plexus-archiver, if it could do the excludes
878             return new ZipFile(file);
879         } catch (ZipException ex) {
880             IOException ioe = new IOException("Error opening ZipFile: " + file.getAbsolutePath());
881             ioe.initCause(ex);
882             throw ioe;
883         }
884     }
885 
886     private static void closeZipFile(ZipFile zipFile) {
887         // TODO: move to plexus utils
888         try {
889             zipFile.close();
890         } catch (IOException e) {
891             // ignore
892         }
893     }
894 
895     private static String getSiteRendererVersion() {
896         InputStream inputStream = DefaultSiteRenderer.class.getResourceAsStream(
897                 "/META-INF/" + "maven/org.apache.maven.doxia/doxia-site-renderer/pom.properties");
898         if (inputStream == null) {
899             LOGGER.debug("pom.properties for doxia-site-renderer not found");
900         } else {
901             Properties properties = new Properties();
902             try (InputStream in = inputStream) {
903                 properties.load(in);
904                 return properties.getProperty("version");
905             } catch (IOException e) {
906                 LOGGER.debug("Failed to load pom.properties, so Doxia SiteRenderer version will not be available", e);
907             }
908         }
909 
910         return null;
911     }
912 }