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         context.put("shortTitle", content.getTitle());
551 
552         // DOXIASITETOOLS-70: Prepend the project name to the title, if any
553         StringBuilder title = new StringBuilder();
554         if (siteRenderingContext.getSiteModel() != null
555                 && StringUtils.isNotEmpty(siteRenderingContext.getSiteModel().getName())) {
556             title.append(siteRenderingContext.getSiteModel().getName());
557         } else if (StringUtils.isNotEmpty(siteRenderingContext.getDefaultTitle())) {
558             title.append(siteRenderingContext.getDefaultTitle());
559         }
560 
561         if (title.length() > 0 && StringUtils.isNotEmpty(content.getTitle())) {
562             title.append(" \u2013 "); // Symbol Name: En Dash
563         }
564         if (StringUtils.isNotEmpty(content.getTitle())) {
565             title.append(content.getTitle());
566         }
567 
568         context.put("title", title.length() > 0 ? title.toString() : null);
569 
570         context.put("headContent", content.getHead());
571 
572         context.put("bodyContent", content.getBody());
573 
574         // document date (got from Doxia Sink date() API)
575         context.put("documentDate", content.getDate());
576 
577         // document rendering context, to get eventual inputPath
578         context.put("docRenderingContext", content.getRenderingContext());
579 
580         return context;
581     }
582 
583     public void generateDocument(Writer writer, SiteRendererSink sink, SiteRenderingContext siteRenderingContext)
584             throws RendererException {
585         mergeDocumentIntoSite(writer, sink, siteRenderingContext);
586     }
587 
588     /** {@inheritDoc} */
589     public void mergeDocumentIntoSite(Writer writer, DocumentContent content, SiteRenderingContext siteRenderingContext)
590             throws RendererException {
591         String templateName = siteRenderingContext.getTemplateName();
592 
593         LOGGER.debug("Processing Velocity for template " + templateName + " on "
594                 + content.getRenderingContext().getDoxiaSourcePath());
595 
596         Context context = createSiteTemplateVelocityContext(content, siteRenderingContext);
597 
598         ClassLoader old = null;
599 
600         if (siteRenderingContext.getTemplateClassLoader() != null) {
601             // -------------------------------------------------------------------------
602             // If no template classloader was set we'll just use the context classloader
603             // -------------------------------------------------------------------------
604 
605             old = Thread.currentThread().getContextClassLoader();
606 
607             Thread.currentThread().setContextClassLoader(siteRenderingContext.getTemplateClassLoader());
608         }
609 
610         try {
611             Template template;
612             Artifact skin = siteRenderingContext.getSkin();
613 
614             try {
615                 SkinModel skinModel = siteRenderingContext.getSkinModel();
616                 String encoding = (skinModel == null) ? null : skinModel.getEncoding();
617 
618                 template = (encoding == null)
619                         ? velocity.getEngine().getTemplate(templateName)
620                         : velocity.getEngine().getTemplate(templateName, encoding);
621             } catch (ParseErrorException pee) {
622                 throw new RendererException(
623                         "Velocity parsing error while reading the site template " + "from " + skin.getId() + " skin",
624                         pee);
625             } catch (ResourceNotFoundException rnfe) {
626                 throw new RendererException(
627                         "Could not find the site template " + "from " + skin.getId() + " skin", rnfe);
628             }
629 
630             try {
631                 StringWriter sw = new StringWriter();
632                 template.merge(context, sw);
633                 writer.write(sw.toString().replaceAll("\r?\n", SystemUtils.LINE_SEPARATOR));
634             } catch (VelocityException ve) {
635                 throw new RendererException("Velocity error while merging site template.", ve);
636             } catch (IOException ioe) {
637                 throw new RendererException("IO exception while merging site template.", ioe);
638             }
639         } finally {
640             IOUtil.close(writer);
641 
642             if (old != null) {
643                 Thread.currentThread().setContextClassLoader(old);
644             }
645         }
646     }
647 
648     private SiteRenderingContext createSiteRenderingContext(
649             Map<String, ?> attributes, SiteModel siteModel, String defaultTitle, Locale locale) {
650         SiteRenderingContext context = new SiteRenderingContext();
651 
652         context.setTemplateProperties(attributes);
653         context.setLocale(locale);
654         context.setSiteModel(siteModel);
655         context.setDefaultTitle(defaultTitle);
656 
657         return context;
658     }
659 
660     /** {@inheritDoc} */
661     public SiteRenderingContext createContextForSkin(
662             Artifact skin, Map<String, ?> attributes, SiteModel siteModel, String defaultTitle, Locale locale)
663             throws IOException, RendererException {
664         SiteRenderingContext context = createSiteRenderingContext(attributes, siteModel, defaultTitle, locale);
665 
666         context.setSkin(skin);
667 
668         ZipFile zipFile = getZipFile(skin.getFile());
669         InputStream in = null;
670 
671         try {
672             if (zipFile.getEntry(SKIN_TEMPLATE_LOCATION) == null) {
673                 throw new RendererException("Skin does not contain template at " + SKIN_TEMPLATE_LOCATION);
674             }
675             context.setTemplateName(SKIN_TEMPLATE_LOCATION);
676             context.setTemplateClassLoader(
677                     new URLClassLoader(new URL[] {skin.getFile().toURI().toURL()}));
678 
679             ZipEntry skinDescriptorEntry = zipFile.getEntry(SkinModel.SKIN_DESCRIPTOR_LOCATION);
680             if (skinDescriptorEntry != null) {
681                 in = zipFile.getInputStream(skinDescriptorEntry);
682 
683                 SkinModel skinModel = new SkinXpp3Reader().read(in);
684                 context.setSkinModel(skinModel);
685 
686                 String toolsPrerequisite = skinModel.getPrerequisites() == null
687                         ? null
688                         : skinModel.getPrerequisites().getDoxiaSitetools();
689 
690                 Package p = DefaultSiteRenderer.class.getPackage();
691                 String current = (p == null) ? null : p.getImplementationVersion();
692 
693                 if (StringUtils.isNotBlank(toolsPrerequisite)
694                         && (current != null)
695                         && !matchVersion(current, toolsPrerequisite)) {
696                     throw new RendererException("Cannot use skin: has " + toolsPrerequisite
697                             + " Doxia Sitetools prerequisite, but current is " + current);
698                 }
699             }
700         } catch (XmlPullParserException e) {
701             throw new RendererException(
702                     "Failed to parse " + SkinModel.SKIN_DESCRIPTOR_LOCATION + " skin descriptor from " + skin.getId()
703                             + " skin",
704                     e);
705         } finally {
706             IOUtil.close(in);
707             closeZipFile(zipFile);
708         }
709 
710         return context;
711     }
712 
713     boolean matchVersion(String current, String prerequisite) throws RendererException {
714         try {
715             ArtifactVersion v = new DefaultArtifactVersion(current);
716             VersionRange vr = VersionRange.createFromVersionSpec(prerequisite);
717 
718             boolean matched = false;
719             ArtifactVersion recommendedVersion = vr.getRecommendedVersion();
720             if (recommendedVersion == null) {
721                 List<Restriction> restrictions = vr.getRestrictions();
722                 for (Restriction restriction : restrictions) {
723                     if (restriction.containsVersion(v)) {
724                         matched = true;
725                         break;
726                     }
727                 }
728             } else {
729                 // only singular versions ever have a recommendedVersion
730                 @SuppressWarnings("unchecked")
731                 int compareTo = recommendedVersion.compareTo(v);
732                 matched = (compareTo <= 0);
733             }
734 
735             if (LOGGER.isDebugEnabled()) {
736                 LOGGER.debug("Skin doxia-sitetools prerequisite: " + prerequisite + ", current: " + current
737                         + ", matched = " + matched);
738             }
739 
740             return matched;
741         } catch (InvalidVersionSpecificationException e) {
742             throw new RendererException("Invalid skin doxia-sitetools prerequisite: " + prerequisite, e);
743         }
744     }
745 
746     /** {@inheritDoc} */
747     public void copyResources(SiteRenderingContext siteRenderingContext, File outputDirectory) throws IOException {
748         ZipFile file = getZipFile(siteRenderingContext.getSkin().getFile());
749 
750         try {
751             for (Enumeration<? extends ZipEntry> e = file.entries(); e.hasMoreElements(); ) {
752                 ZipEntry entry = e.nextElement();
753 
754                 if (!entry.getName().startsWith("META-INF/")) {
755                     File destFile = new File(outputDirectory, entry.getName());
756                     if (!entry.isDirectory()) {
757                         if (destFile.exists()) {
758                             // don't override existing content: avoids extra rewrite with same content or extra site
759                             // resource
760                             continue;
761                         }
762 
763                         destFile.getParentFile().mkdirs();
764 
765                         copyFileFromZip(file, entry, destFile);
766                     } else {
767                         destFile.mkdirs();
768                     }
769                 }
770             }
771         } finally {
772             closeZipFile(file);
773         }
774 
775         // Copy extra site resources
776         for (SiteDirectory siteDirectory : siteRenderingContext.getSiteDirectories()) {
777             File resourcesDirectory = new File(siteDirectory.getPath(), "resources");
778 
779             if (resourcesDirectory != null && resourcesDirectory.exists()) {
780                 copyDirectory(resourcesDirectory, outputDirectory);
781             }
782         }
783 
784         // Check for the existence of /css/site.css
785         File siteCssFile = new File(outputDirectory, "/css/site.css");
786         if (!siteCssFile.exists()) {
787             // Create the subdirectory css if it doesn't exist, DOXIA-151
788             File cssDirectory = new File(outputDirectory, "/css/");
789             boolean created = cssDirectory.mkdirs();
790             if (created && LOGGER.isDebugEnabled()) {
791                 LOGGER.debug("The directory '" + cssDirectory.getAbsolutePath() + "' did not exist. It was created.");
792             }
793 
794             // If the file is not there - create an empty file, DOXIA-86
795             if (LOGGER.isDebugEnabled()) {
796                 LOGGER.debug(
797                         "The file '" + siteCssFile.getAbsolutePath() + "' does not exist. Creating an empty file.");
798             }
799             Writer writer = null;
800             try {
801                 writer = WriterFactory.newWriter(siteCssFile, siteRenderingContext.getOutputEncoding());
802                 // DOXIA-290...the file should not be 0 bytes.
803                 writer.write("/* You can override this file with your own styles */");
804             } finally {
805                 IOUtil.close(writer);
806             }
807         }
808     }
809 
810     private static void copyFileFromZip(ZipFile file, ZipEntry entry, File destFile) throws IOException {
811         FileOutputStream fos = new FileOutputStream(destFile);
812 
813         try {
814             IOUtil.copy(file.getInputStream(entry), fos);
815         } finally {
816             IOUtil.close(fos);
817         }
818     }
819 
820     /**
821      * Copy the directory
822      *
823      * @param source      source file to be copied
824      * @param destination destination file
825      * @throws java.io.IOException if any
826      */
827     protected void copyDirectory(File source, File destination) throws IOException {
828         if (source.exists()) {
829             DirectoryScanner scanner = new DirectoryScanner();
830 
831             String[] includedResources = {"**/*"};
832 
833             scanner.setIncludes(includedResources);
834 
835             scanner.addDefaultExcludes();
836 
837             scanner.setBasedir(source);
838 
839             scanner.scan();
840 
841             List<String> includedFiles = Arrays.asList(scanner.getIncludedFiles());
842 
843             for (String name : includedFiles) {
844                 File sourceFile = new File(source, name);
845 
846                 File destinationFile = new File(destination, name);
847 
848                 FileUtils.copyFile(sourceFile, destinationFile);
849             }
850         }
851     }
852 
853     private Reader validate(Reader source, String resource) throws ParseException, IOException {
854         LOGGER.debug("Validating: " + resource);
855 
856         try {
857             String content = IOUtil.toString(new BufferedReader(source));
858 
859             new XmlValidator().validate(content);
860 
861             return new StringReader(content);
862         } finally {
863             IOUtil.close(source);
864         }
865     }
866 
867     // TODO replace with StringUtils.endsWithIgnoreCase() from maven-shared-utils 0.7
868     static boolean endsWithIgnoreCase(String str, String searchStr) {
869         if (str.length() < searchStr.length()) {
870             return false;
871         }
872 
873         return str.regionMatches(true, str.length() - searchStr.length(), searchStr, 0, searchStr.length());
874     }
875 
876     private static ZipFile getZipFile(File file) throws IOException {
877         if (file == null) {
878             throw new IOException("Error opening ZipFile: null");
879         }
880 
881         try {
882             // TODO: plexus-archiver, if it could do the excludes
883             return new ZipFile(file);
884         } catch (ZipException ex) {
885             IOException ioe = new IOException("Error opening ZipFile: " + file.getAbsolutePath());
886             ioe.initCause(ex);
887             throw ioe;
888         }
889     }
890 
891     private static void closeZipFile(ZipFile zipFile) {
892         // TODO: move to plexus utils
893         try {
894             zipFile.close();
895         } catch (IOException e) {
896             // ignore
897         }
898     }
899 
900     private static String getSiteRendererVersion() {
901         InputStream inputStream = DefaultSiteRenderer.class.getResourceAsStream(
902                 "/META-INF/" + "maven/org.apache.maven.doxia/doxia-site-renderer/pom.properties");
903         if (inputStream == null) {
904             LOGGER.debug("pom.properties for doxia-site-renderer not found");
905         } else {
906             Properties properties = new Properties();
907             try (InputStream in = inputStream) {
908                 properties.load(in);
909                 return properties.getProperty("version");
910             } catch (IOException e) {
911                 LOGGER.debug("Failed to load pom.properties, so Doxia SiteRenderer version will not be available", e);
912             }
913         }
914 
915         return null;
916     }
917 }