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.zip.ZipEntry;
50  import java.util.zip.ZipException;
51  import java.util.zip.ZipFile;
52  
53  import org.apache.commons.lang3.ArrayUtils;
54  import org.apache.commons.lang3.SystemUtils;
55  import org.apache.maven.artifact.Artifact;
56  import org.apache.maven.artifact.versioning.ArtifactVersion;
57  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
58  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
59  import org.apache.maven.artifact.versioning.Restriction;
60  import org.apache.maven.artifact.versioning.VersionRange;
61  import org.apache.maven.doxia.Doxia;
62  import org.apache.maven.doxia.parser.ParseException;
63  import org.apache.maven.doxia.parser.Parser;
64  import org.apache.maven.doxia.parser.manager.ParserNotFoundException;
65  import org.apache.maven.doxia.parser.module.ParserModule;
66  import org.apache.maven.doxia.parser.module.ParserModuleManager;
67  import org.apache.maven.doxia.parser.module.ParserModuleNotFoundException;
68  import org.apache.maven.doxia.site.decoration.DecorationModel;
69  import org.apache.maven.doxia.site.skin.SkinModel;
70  import org.apache.maven.doxia.site.skin.io.xpp3.SkinXpp3Reader;
71  import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
72  import org.apache.maven.doxia.util.XmlValidator;
73  import org.apache.velocity.Template;
74  import org.apache.velocity.context.Context;
75  import org.apache.velocity.exception.ParseErrorException;
76  import org.apache.velocity.exception.ResourceNotFoundException;
77  import org.apache.velocity.exception.VelocityException;
78  import org.apache.velocity.tools.Scope;
79  import org.apache.velocity.tools.ToolManager;
80  import org.apache.velocity.tools.config.ConfigurationUtils;
81  import org.apache.velocity.tools.config.EasyFactoryConfiguration;
82  import org.apache.velocity.tools.config.FactoryConfiguration;
83  import org.apache.velocity.tools.generic.AlternatorTool;
84  import org.apache.velocity.tools.generic.ClassTool;
85  import org.apache.velocity.tools.generic.ComparisonDateTool;
86  import org.apache.velocity.tools.generic.ContextTool;
87  import org.apache.velocity.tools.generic.ConversionTool;
88  import org.apache.velocity.tools.generic.DisplayTool;
89  import org.apache.velocity.tools.generic.EscapeTool;
90  import org.apache.velocity.tools.generic.FieldTool;
91  import org.apache.velocity.tools.generic.LinkTool;
92  import org.apache.velocity.tools.generic.LoopTool;
93  import org.apache.velocity.tools.generic.MathTool;
94  import org.apache.velocity.tools.generic.NumberTool;
95  import org.apache.velocity.tools.generic.RenderTool;
96  import org.apache.velocity.tools.generic.ResourceTool;
97  import org.apache.velocity.tools.generic.SortTool;
98  import org.apache.velocity.tools.generic.XmlTool;
99  import org.codehaus.plexus.PlexusContainer;
100 import org.codehaus.plexus.util.DirectoryScanner;
101 import org.codehaus.plexus.util.FileUtils;
102 import org.codehaus.plexus.util.IOUtil;
103 import org.codehaus.plexus.util.Os;
104 import org.codehaus.plexus.util.PathTool;
105 import org.codehaus.plexus.util.ReaderFactory;
106 import org.codehaus.plexus.util.StringUtils;
107 import org.codehaus.plexus.util.WriterFactory;
108 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
109 import org.codehaus.plexus.velocity.VelocityComponent;
110 import org.slf4j.Logger;
111 import org.slf4j.LoggerFactory;
112 
113 /**
114  * <p>DefaultSiteRenderer class.</p>
115  *
116  * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
117  * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
118  * @since 1.0
119  */
120 @Singleton
121 @Named
122 public class DefaultSiteRenderer implements Renderer {
123     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSiteRenderer.class);
124 
125     // ----------------------------------------------------------------------
126     // Requirements
127     // ----------------------------------------------------------------------
128 
129     @Inject
130     private VelocityComponent velocity;
131 
132     @Inject
133     private ParserModuleManager parserModuleManager;
134 
135     @Inject
136     private Doxia doxia;
137 
138     @Inject
139     private PlexusContainer plexus;
140 
141     private static final String SKIN_TEMPLATE_LOCATION = "META-INF/maven/site.vm";
142 
143     private static final String TOOLS_LOCATION = "META-INF/maven/site-tools.xml";
144 
145     private static final String DOXIA_SITE_RENDERER_VERSION = getSiteRendererVersion();
146 
147     // ----------------------------------------------------------------------
148     // Renderer implementation
149     // ----------------------------------------------------------------------
150 
151     /** {@inheritDoc} */
152     public Map<String, DocumentRenderer> locateDocumentFiles(SiteRenderingContext siteRenderingContext)
153             throws IOException, RendererException {
154         return locateDocumentFiles(siteRenderingContext, false);
155     }
156 
157     /** {@inheritDoc} */
158     public Map<String, DocumentRenderer> locateDocumentFiles(
159             SiteRenderingContext siteRenderingContext, boolean editable) throws IOException, RendererException {
160         Map<String, DocumentRenderer> files = new LinkedHashMap<String, DocumentRenderer>();
161         Map<String, String> moduleExcludes = siteRenderingContext.getModuleExcludes();
162 
163         // look in every site directory (in general src/site or target/generated-site)
164         for (File siteDirectory : siteRenderingContext.getSiteDirectories()) {
165             if (siteDirectory.exists()) {
166                 Collection<ParserModule> modules = parserModuleManager.getParserModules();
167                 // use every Doxia parser module
168                 for (ParserModule module : modules) {
169                     File moduleBasedir = new File(siteDirectory, module.getSourceDirectory());
170 
171                     String excludes = (moduleExcludes == null) ? null : moduleExcludes.get(module.getParserId());
172 
173                     addModuleFiles(
174                             siteRenderingContext.getRootDirectory(), moduleBasedir, module, excludes, files, editable);
175                 }
176             }
177         }
178 
179         // look in specific modules directories (used for old Maven 1.x site layout: xdoc and fml docs in /xdocs)
180         for (ExtraDoxiaModuleReference module : siteRenderingContext.getModules()) {
181             try {
182                 ParserModule parserModule = parserModuleManager.getParserModule(module.getParserId());
183 
184                 String excludes = (moduleExcludes == null) ? null : moduleExcludes.get(module.getParserId());
185 
186                 addModuleFiles(
187                         siteRenderingContext.getRootDirectory(),
188                         module.getBasedir(),
189                         parserModule,
190                         excludes,
191                         files,
192                         editable);
193             } catch (ParserModuleNotFoundException e) {
194                 throw new RendererException("Unable to find module", e);
195             }
196         }
197         return files;
198     }
199 
200     private List<String> filterExtensionIgnoreCase(List<String> fileNames, String extension) {
201         List<String> filtered = new LinkedList<String>(fileNames);
202         for (Iterator<String> it = filtered.iterator(); it.hasNext(); ) {
203             String name = it.next();
204 
205             // Take care of extension case
206             if (!endsWithIgnoreCase(name, extension)) {
207                 it.remove();
208             }
209         }
210         return filtered;
211     }
212 
213     private void addModuleFiles(
214             File rootDir,
215             File moduleBasedir,
216             ParserModule module,
217             String excludes,
218             Map<String, DocumentRenderer> files,
219             boolean editable)
220             throws IOException, RendererException {
221         if (!moduleBasedir.exists() || ArrayUtils.isEmpty(module.getExtensions())) {
222             return;
223         }
224 
225         String moduleRelativePath =
226                 PathTool.getRelativeFilePath(rootDir.getAbsolutePath(), moduleBasedir.getAbsolutePath());
227 
228         List<String> allFiles = FileUtils.getFileNames(moduleBasedir, "**/*", excludes, false);
229 
230         for (String extension : module.getExtensions()) {
231             String fullExtension = "." + extension;
232 
233             List<String> docs = filterExtensionIgnoreCase(allFiles, fullExtension);
234 
235             // *.<extension>.vm
236             List<String> velocityFiles = filterExtensionIgnoreCase(allFiles, fullExtension + ".vm");
237 
238             docs.addAll(velocityFiles);
239 
240             for (String doc : docs) {
241                 RenderingContext context = new RenderingContext(
242                         moduleBasedir, moduleRelativePath, doc, module.getParserId(), extension, editable);
243 
244                 // TODO: DOXIA-111: we need a general filter here that knows how to alter the context
245                 if (endsWithIgnoreCase(doc, ".vm")) {
246                     context.setAttribute("velocity", "true");
247                 }
248 
249                 String key = context.getOutputName();
250                 key = StringUtils.replace(key, "\\", "/");
251 
252                 if (files.containsKey(key)) {
253                     DocumentRenderer renderer = files.get(key);
254 
255                     RenderingContext originalContext = renderer.getRenderingContext();
256 
257                     File originalDoc = new File(originalContext.getBasedir(), originalContext.getInputName());
258 
259                     throw new RendererException("File '" + module.getSourceDirectory() + File.separator + doc
260                             + "' clashes with existing '" + originalDoc + "'.");
261                 }
262                 // -----------------------------------------------------------------------
263                 // Handle key without case differences
264                 // -----------------------------------------------------------------------
265                 for (Map.Entry<String, DocumentRenderer> entry : files.entrySet()) {
266                     if (entry.getKey().equalsIgnoreCase(key)) {
267                         RenderingContext originalContext = entry.getValue().getRenderingContext();
268 
269                         File originalDoc = new File(originalContext.getBasedir(), originalContext.getInputName());
270 
271                         if (Os.isFamily(Os.FAMILY_WINDOWS)) {
272                             throw new RendererException("File '" + module.getSourceDirectory() + File.separator + doc
273                                     + "' clashes with existing '" + originalDoc + "'.");
274                         }
275 
276                         if (LOGGER.isWarnEnabled()) {
277                             LOGGER.warn("File '" + module.getSourceDirectory() + File.separator + doc
278                                     + "' could clash with existing '" + originalDoc + "'.");
279                         }
280                     }
281                 }
282 
283                 files.put(key, new DoxiaDocumentRenderer(context));
284             }
285         }
286     }
287 
288     /** {@inheritDoc} */
289     public void render(
290             Collection<DocumentRenderer> documents, SiteRenderingContext siteRenderingContext, File outputDirectory)
291             throws RendererException, IOException {
292         for (DocumentRenderer docRenderer : documents) {
293             RenderingContext renderingContext = docRenderer.getRenderingContext();
294 
295             File outputFile = new File(outputDirectory, docRenderer.getOutputName());
296 
297             File inputFile = new File(renderingContext.getBasedir(), renderingContext.getInputName());
298 
299             boolean modified = !outputFile.exists()
300                     || (inputFile.lastModified() > outputFile.lastModified())
301                     || (siteRenderingContext.getDecoration().getLastModified() > outputFile.lastModified());
302 
303             if (modified || docRenderer.isOverwrite()) {
304                 if (!outputFile.getParentFile().exists()) {
305                     outputFile.getParentFile().mkdirs();
306                 }
307 
308                 if (LOGGER.isDebugEnabled()) {
309                     LOGGER.debug("Generating " + outputFile);
310                 }
311 
312                 Writer writer = null;
313                 try {
314                     if (!docRenderer.isExternalReport()) {
315                         writer = WriterFactory.newWriter(outputFile, siteRenderingContext.getOutputEncoding());
316                     }
317                     docRenderer.renderDocument(writer, this, siteRenderingContext);
318                 } finally {
319                     IOUtil.close(writer);
320                 }
321             } else {
322                 if (LOGGER.isDebugEnabled()) {
323                     LOGGER.debug(inputFile + " unchanged, not regenerating...");
324                 }
325             }
326         }
327     }
328 
329     /** {@inheritDoc} */
330     public void renderDocument(Writer writer, RenderingContext docRenderingContext, SiteRenderingContext siteContext)
331             throws RendererException, FileNotFoundException, UnsupportedEncodingException {
332         SiteRendererSink sink = new SiteRendererSink(docRenderingContext);
333 
334         File doc = new File(docRenderingContext.getBasedir(), docRenderingContext.getInputName());
335 
336         Reader reader = null;
337         try {
338             String resource = doc.getAbsolutePath();
339 
340             Parser parser = doxia.getParser(docRenderingContext.getParserId());
341             // DOXIASITETOOLS-146 don't render comments from source markup
342             parser.setEmitComments(false);
343 
344             // TODO: DOXIA-111: the filter used here must be checked generally.
345             if (docRenderingContext.getAttribute("velocity") != null) {
346                 LOGGER.debug("Processing Velocity for " + docRenderingContext.getDoxiaSourcePath());
347                 try {
348                     Context vc = createDocumentVelocityContext(docRenderingContext, siteContext);
349 
350                     StringWriter sw = new StringWriter();
351 
352                     velocity.getEngine().mergeTemplate(resource, siteContext.getInputEncoding(), vc, sw);
353 
354                     String doxiaContent = sw.toString();
355 
356                     if (siteContext.getProcessedContentOutput() != null) {
357                         // save Velocity processing result, ie the Doxia content that will be parsed after
358                         saveVelocityProcessedContent(docRenderingContext, siteContext, doxiaContent);
359                     }
360 
361                     reader = new StringReader(doxiaContent);
362                 } catch (VelocityException e) {
363                     throw new RendererException(
364                             "Error parsing " + docRenderingContext.getDoxiaSourcePath() + " as a Velocity template", e);
365                 }
366 
367                 if (parser.getType() == Parser.XML_TYPE && siteContext.isValidate()) {
368                     reader = validate(reader, resource);
369                 }
370             } else {
371                 switch (parser.getType()) {
372                     case Parser.XML_TYPE:
373                         reader = ReaderFactory.newXmlReader(doc);
374                         if (siteContext.isValidate()) {
375                             reader = validate(reader, resource);
376                         }
377                         break;
378 
379                     case Parser.TXT_TYPE:
380                     case Parser.UNKNOWN_TYPE:
381                     default:
382                         reader = ReaderFactory.newReader(doc, siteContext.getInputEncoding());
383                 }
384             }
385 
386             doxia.parse(reader, docRenderingContext.getParserId(), sink, docRenderingContext.getInputName());
387         } catch (ParserNotFoundException e) {
388             throw new RendererException("Error getting a parser for '" + doc + "'", e);
389         } catch (ParseException e) {
390             StringBuilder errorMsgBuilder = new StringBuilder();
391             errorMsgBuilder.append("Error parsing '").append(doc).append("'");
392             if (e.getLineNumber() > 0) {
393                 errorMsgBuilder.append(", line ").append(e.getLineNumber());
394             }
395             throw new RendererException(errorMsgBuilder.toString(), e);
396         } catch (IOException e) {
397             throw new RendererException("Error while processing '" + doc + "'", e);
398         } finally {
399             sink.flush();
400 
401             sink.close();
402 
403             IOUtil.close(reader);
404         }
405 
406         mergeDocumentIntoSite(writer, (DocumentContent) sink, siteContext);
407     }
408 
409     private void saveVelocityProcessedContent(
410             RenderingContext docRenderingContext, SiteRenderingContext siteContext, String doxiaContent)
411             throws IOException {
412         if (!siteContext.getProcessedContentOutput().exists()) {
413             siteContext.getProcessedContentOutput().mkdirs();
414         }
415 
416         String input = docRenderingContext.getInputName();
417         File outputFile = new File(siteContext.getProcessedContentOutput(), input.substring(0, input.length() - 3));
418 
419         File outputParent = outputFile.getParentFile();
420         if (!outputParent.exists()) {
421             outputParent.mkdirs();
422         }
423 
424         FileUtils.fileWrite(outputFile, siteContext.getInputEncoding(), doxiaContent);
425     }
426 
427     /**
428      * Creates a Velocity Context with all generic tools configured wit the site rendering context.
429      *
430      * @param siteRenderingContext the site rendering context
431      * @return a Velocity tools managed context
432      */
433     protected Context createToolManagedVelocityContext(SiteRenderingContext siteRenderingContext) {
434         Locale locale = siteRenderingContext.getLocale();
435         String dateFormat =
436                 siteRenderingContext.getDecoration().getPublishDate().getFormat();
437 
438         EasyFactoryConfiguration config = new EasyFactoryConfiguration(false);
439         config.property("safeMode", Boolean.FALSE);
440         config.toolbox(Scope.REQUEST)
441                 .tool(ContextTool.class)
442                 .tool(LinkTool.class)
443                 .tool(LoopTool.class)
444                 .tool(RenderTool.class);
445         config.toolbox(Scope.APPLICATION)
446                 .property("locale", locale)
447                 .tool(AlternatorTool.class)
448                 .tool(ClassTool.class)
449                 .tool(ComparisonDateTool.class)
450                 .property("format", dateFormat)
451                 .tool(ConversionTool.class)
452                 .property("dateFormat", dateFormat)
453                 .tool(DisplayTool.class)
454                 .tool(EscapeTool.class)
455                 .tool(FieldTool.class)
456                 .tool(MathTool.class)
457                 .tool(NumberTool.class)
458                 .tool(ResourceTool.class)
459                 .property("bundles", new String[] {"site-renderer"})
460                 .tool(SortTool.class)
461                 .tool(XmlTool.class);
462 
463         FactoryConfiguration customConfig = ConfigurationUtils.findInClasspath(TOOLS_LOCATION);
464 
465         if (customConfig != null) {
466             config.addConfiguration(customConfig);
467         }
468 
469         ToolManager manager = new ToolManager(false, false);
470         manager.configure(config);
471 
472         return manager.createContext();
473     }
474 
475     /**
476      * Create a Velocity Context for a Doxia document, containing every information about rendered document.
477      *
478      * @param renderingContext the document's RenderingContext
479      * @param siteRenderingContext the site rendering context
480      * @return a Velocity tools managed context
481      */
482     protected Context createDocumentVelocityContext(
483             RenderingContext renderingContext, SiteRenderingContext siteRenderingContext) {
484         Context context = createToolManagedVelocityContext(siteRenderingContext);
485         // ----------------------------------------------------------------------
486         // Data objects
487         // ----------------------------------------------------------------------
488 
489         context.put("relativePath", renderingContext.getRelativePath());
490 
491         String currentFileName = renderingContext.getOutputName().replace('\\', '/');
492         context.put("currentFileName", currentFileName);
493 
494         context.put("alignedFileName", PathTool.calculateLink(currentFileName, renderingContext.getRelativePath()));
495 
496         context.put("decoration", siteRenderingContext.getDecoration());
497 
498         Locale locale = siteRenderingContext.getLocale();
499         context.put("locale", locale);
500         context.put("supportedLocales", Collections.unmodifiableList(siteRenderingContext.getSiteLocales()));
501 
502         context.put("publishDate", siteRenderingContext.getPublishDate());
503 
504         if (DOXIA_SITE_RENDERER_VERSION != null) {
505             context.put("doxiaSiteRendererVersion", DOXIA_SITE_RENDERER_VERSION);
506         }
507 
508         // Add user properties
509         Map<String, ?> templateProperties = siteRenderingContext.getTemplateProperties();
510 
511         if (templateProperties != null) {
512             for (Map.Entry<String, ?> entry : templateProperties.entrySet()) {
513                 context.put(entry.getKey(), entry.getValue());
514             }
515         }
516 
517         // ----------------------------------------------------------------------
518         // Tools
519         // ----------------------------------------------------------------------
520 
521         context.put("PathTool", new PathTool());
522 
523         context.put("StringUtils", new StringUtils());
524 
525         context.put("plexus", plexus);
526         return context;
527     }
528 
529     /**
530      * Create a Velocity Context for the site template decorating the document. In addition to all the informations
531      * from the document, this context contains data gathered in {@link SiteRendererSink} during document rendering.
532      *
533      * @param content the document content to be merged into the template
534      * @param siteRenderingContext the site rendering context
535      * @return a Velocity tools managed context
536      */
537     protected Context createSiteTemplateVelocityContext(
538             DocumentContent content, SiteRenderingContext siteRenderingContext) {
539         // first get the context from document
540         Context context = createDocumentVelocityContext(content.getRenderingContext(), siteRenderingContext);
541 
542         // then add data objects from rendered document
543 
544         // Add infos from document
545         context.put("authors", content.getAuthors());
546 
547         context.put("shortTitle", content.getTitle());
548 
549         // DOXIASITETOOLS-70: Prepend the project name to the title, if any
550         StringBuilder title = new StringBuilder();
551         if (siteRenderingContext.getDecoration() != null
552                 && StringUtils.isNotEmpty(siteRenderingContext.getDecoration().getName())) {
553             title.append(siteRenderingContext.getDecoration().getName());
554         } else if (StringUtils.isNotEmpty(siteRenderingContext.getDefaultTitle())) {
555             title.append(siteRenderingContext.getDefaultTitle());
556         }
557 
558         if (title.length() > 0 && StringUtils.isNotEmpty(content.getTitle())) {
559             title.append(" &#x2013; "); // Symbol Name: En Dash, Html Entity: &ndash;
560         }
561         if (StringUtils.isNotEmpty(content.getTitle())) {
562             title.append(content.getTitle());
563         }
564 
565         context.put("title", title.length() > 0 ? title.toString() : null);
566 
567         context.put("headContent", content.getHead());
568 
569         context.put("bodyContent", content.getBody());
570 
571         // document date (got from Doxia Sink date() API)
572         context.put("documentDate", content.getDate());
573 
574         // document rendering context, to get eventual inputName
575         context.put("docRenderingContext", content.getRenderingContext());
576 
577         return context;
578     }
579 
580     /** {@inheritDoc} */
581     public void generateDocument(Writer writer, SiteRendererSink sink, SiteRenderingContext siteRenderingContext)
582             throws RendererException {
583         mergeDocumentIntoSite(writer, sink, siteRenderingContext);
584     }
585 
586     /** {@inheritDoc} */
587     public void mergeDocumentIntoSite(Writer writer, DocumentContent content, SiteRenderingContext siteRenderingContext)
588             throws RendererException {
589         String templateName = siteRenderingContext.getTemplateName();
590 
591         LOGGER.debug("Processing Velocity for template " + templateName + " on "
592                 + content.getRenderingContext().getInputName());
593 
594         Context context = createSiteTemplateVelocityContext(content, siteRenderingContext);
595 
596         ClassLoader old = null;
597 
598         if (siteRenderingContext.getTemplateClassLoader() != null) {
599             // -------------------------------------------------------------------------
600             // If no template classloader was set we'll just use the context classloader
601             // -------------------------------------------------------------------------
602 
603             old = Thread.currentThread().getContextClassLoader();
604 
605             Thread.currentThread().setContextClassLoader(siteRenderingContext.getTemplateClassLoader());
606         }
607 
608         try {
609             Template template;
610             Artifact skin = siteRenderingContext.getSkin();
611 
612             try {
613                 SkinModel skinModel = siteRenderingContext.getSkinModel();
614                 String encoding = (skinModel == null) ? null : skinModel.getEncoding();
615 
616                 template = (encoding == null)
617                         ? velocity.getEngine().getTemplate(templateName)
618                         : velocity.getEngine().getTemplate(templateName, encoding);
619             } catch (ParseErrorException pee) {
620                 throw new RendererException(
621                         "Velocity parsing error while reading the site decoration template " + "from " + skin.getId()
622                                 + " skin",
623                         pee);
624             } catch (ResourceNotFoundException rnfe) {
625                 throw new RendererException(
626                         "Could not find the site decoration template " + "from " + skin.getId() + " skin", rnfe);
627             }
628 
629             try {
630                 StringWriter sw = new StringWriter();
631                 template.merge(context, sw);
632                 writer.write(sw.toString().replaceAll("\r?\n", SystemUtils.LINE_SEPARATOR));
633             } catch (VelocityException ve) {
634                 throw new RendererException("Velocity error while merging site decoration template.", ve);
635             } catch (IOException ioe) {
636                 throw new RendererException("IO exception while merging site decoration template.", ioe);
637             }
638         } finally {
639             IOUtil.close(writer);
640 
641             if (old != null) {
642                 Thread.currentThread().setContextClassLoader(old);
643             }
644         }
645     }
646 
647     private SiteRenderingContext createSiteRenderingContext(
648             Map<String, ?> attributes, DecorationModel decoration, String defaultTitle, Locale locale) {
649         SiteRenderingContext context = new SiteRenderingContext();
650 
651         context.setTemplateProperties(attributes);
652         context.setLocale(locale);
653         context.setDecoration(decoration);
654         context.setDefaultTitle(defaultTitle);
655 
656         return context;
657     }
658 
659     /** {@inheritDoc} */
660     public SiteRenderingContext createContextForSkin(
661             Artifact skin, Map<String, ?> attributes, DecorationModel decoration, String defaultTitle, Locale locale)
662             throws IOException, RendererException {
663         SiteRenderingContext context = createSiteRenderingContext(attributes, decoration, defaultTitle, locale);
664 
665         context.setSkin(skin);
666 
667         ZipFile zipFile = getZipFile(skin.getFile());
668         InputStream in = null;
669 
670         try {
671             if (zipFile.getEntry(SKIN_TEMPLATE_LOCATION) == null) {
672                 throw new RendererException("Skin does not contain template at " + SKIN_TEMPLATE_LOCATION);
673             }
674             context.setTemplateName(SKIN_TEMPLATE_LOCATION);
675             context.setTemplateClassLoader(
676                     new URLClassLoader(new URL[] {skin.getFile().toURI().toURL()}));
677 
678             ZipEntry skinDescriptorEntry = zipFile.getEntry(SkinModel.SKIN_DESCRIPTOR_LOCATION);
679             if (skinDescriptorEntry != null) {
680                 in = zipFile.getInputStream(skinDescriptorEntry);
681 
682                 SkinModel skinModel = new SkinXpp3Reader().read(in);
683                 context.setSkinModel(skinModel);
684 
685                 String toolsPrerequisite = skinModel.getPrerequisites() == null
686                         ? null
687                         : skinModel.getPrerequisites().getDoxiaSitetools();
688 
689                 Package p = DefaultSiteRenderer.class.getPackage();
690                 String current = (p == null) ? null : p.getImplementationVersion();
691 
692                 if (StringUtils.isNotBlank(toolsPrerequisite)
693                         && (current != null)
694                         && !matchVersion(current, toolsPrerequisite)) {
695                     throw new RendererException("Cannot use skin: has " + toolsPrerequisite
696                             + " Doxia Sitetools prerequisite, but current is " + current);
697                 }
698             }
699         } catch (XmlPullParserException e) {
700             throw new RendererException(
701                     "Failed to parse " + SkinModel.SKIN_DESCRIPTOR_LOCATION + " skin descriptor from " + skin.getId()
702                             + " skin",
703                     e);
704         } finally {
705             IOUtil.close(in);
706             closeZipFile(zipFile);
707         }
708 
709         return context;
710     }
711 
712     boolean matchVersion(String current, String prerequisite) throws RendererException {
713         try {
714             ArtifactVersion v = new DefaultArtifactVersion(current);
715             VersionRange vr = VersionRange.createFromVersionSpec(prerequisite);
716 
717             boolean matched = false;
718             ArtifactVersion recommendedVersion = vr.getRecommendedVersion();
719             if (recommendedVersion == null) {
720                 List<Restriction> restrictions = vr.getRestrictions();
721                 for (Restriction restriction : restrictions) {
722                     if (restriction.containsVersion(v)) {
723                         matched = true;
724                         break;
725                     }
726                 }
727             } else {
728                 // only singular versions ever have a recommendedVersion
729                 @SuppressWarnings("unchecked")
730                 int compareTo = recommendedVersion.compareTo(v);
731                 matched = (compareTo <= 0);
732             }
733 
734             if (LOGGER.isDebugEnabled()) {
735                 LOGGER.debug("Skin doxia-sitetools prerequisite: " + prerequisite + ", current: " + current
736                         + ", matched = " + matched);
737             }
738 
739             return matched;
740         } catch (InvalidVersionSpecificationException e) {
741             throw new RendererException("Invalid skin doxia-sitetools prerequisite: " + prerequisite, e);
742         }
743     }
744 
745     /** {@inheritDoc} */
746     public void copyResources(SiteRenderingContext siteRenderingContext, File resourcesDirectory, File outputDirectory)
747             throws IOException {
748         throw new AssertionError("copyResources( SiteRenderingContext, File, File ) is deprecated.");
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 (File siteDirectory : siteRenderingContext.getSiteDirectories()) {
782             File resourcesDirectory = new File(siteDirectory, "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 }