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