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