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