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.OutputStream;
31  import java.io.Reader;
32  import java.io.StringReader;
33  import java.io.StringWriter;
34  import java.io.Writer;
35  import java.net.URL;
36  import java.net.URLClassLoader;
37  import java.util.Arrays;
38  import java.util.Collection;
39  import java.util.Collections;
40  import java.util.Enumeration;
41  import java.util.HashMap;
42  import java.util.Iterator;
43  import java.util.LinkedHashMap;
44  import java.util.LinkedList;
45  import java.util.List;
46  import java.util.Locale;
47  import java.util.Map;
48  import java.util.Map.Entry;
49  import java.util.Properties;
50  import java.util.TimeZone;
51  import java.util.function.Function;
52  import java.util.zip.ZipEntry;
53  import java.util.zip.ZipException;
54  import java.util.zip.ZipFile;
55  
56  import org.apache.commons.lang3.ArrayUtils;
57  import org.apache.commons.lang3.SystemUtils;
58  import org.apache.maven.artifact.Artifact;
59  import org.apache.maven.artifact.versioning.ArtifactVersion;
60  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
61  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
62  import org.apache.maven.artifact.versioning.Restriction;
63  import org.apache.maven.artifact.versioning.VersionRange;
64  import org.apache.maven.doxia.Doxia;
65  import org.apache.maven.doxia.parser.ParseException;
66  import org.apache.maven.doxia.parser.Parser;
67  import org.apache.maven.doxia.parser.manager.ParserNotFoundException;
68  import org.apache.maven.doxia.parser.module.ParserModule;
69  import org.apache.maven.doxia.parser.module.ParserModuleManager;
70  import org.apache.maven.doxia.site.SiteModel;
71  import org.apache.maven.doxia.site.skin.ResourceCondition;
72  import org.apache.maven.doxia.site.skin.SkinModel;
73  import org.apache.maven.doxia.site.skin.io.xpp3.SkinXpp3Reader;
74  import org.apache.maven.doxia.siterenderer.SiteRenderingContext.SiteDirectory;
75  import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
76  import org.apache.maven.doxia.util.XmlValidator;
77  import org.apache.velocity.Template;
78  import org.apache.velocity.app.Velocity;
79  import org.apache.velocity.context.Context;
80  import org.apache.velocity.exception.ParseErrorException;
81  import org.apache.velocity.exception.ResourceNotFoundException;
82  import org.apache.velocity.exception.VelocityException;
83  import org.apache.velocity.tools.Scope;
84  import org.apache.velocity.tools.ToolManager;
85  import org.apache.velocity.tools.config.ConfigurationUtils;
86  import org.apache.velocity.tools.config.EasyFactoryConfiguration;
87  import org.apache.velocity.tools.config.FactoryConfiguration;
88  import org.apache.velocity.tools.generic.AlternatorTool;
89  import org.apache.velocity.tools.generic.ClassTool;
90  import org.apache.velocity.tools.generic.ComparisonDateTool;
91  import org.apache.velocity.tools.generic.ContextTool;
92  import org.apache.velocity.tools.generic.ConversionTool;
93  import org.apache.velocity.tools.generic.DisplayTool;
94  import org.apache.velocity.tools.generic.EscapeTool;
95  import org.apache.velocity.tools.generic.FieldTool;
96  import org.apache.velocity.tools.generic.LinkTool;
97  import org.apache.velocity.tools.generic.LoopTool;
98  import org.apache.velocity.tools.generic.MathTool;
99  import org.apache.velocity.tools.generic.NumberTool;
100 import org.apache.velocity.tools.generic.RenderTool;
101 import org.apache.velocity.tools.generic.ResourceTool;
102 import org.apache.velocity.tools.generic.SortTool;
103 import org.apache.velocity.tools.generic.XmlTool;
104 import org.codehaus.plexus.PlexusContainer;
105 import org.codehaus.plexus.util.DirectoryScanner;
106 import org.codehaus.plexus.util.FileUtils;
107 import org.codehaus.plexus.util.IOUtil;
108 import org.codehaus.plexus.util.Os;
109 import org.codehaus.plexus.util.PathTool;
110 import org.codehaus.plexus.util.ReaderFactory;
111 import org.codehaus.plexus.util.StringUtils;
112 import org.codehaus.plexus.util.WriterFactory;
113 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
114 import org.codehaus.plexus.velocity.VelocityComponent;
115 import org.slf4j.Logger;
116 import org.slf4j.LoggerFactory;
117 
118 /**
119  * <p>DefaultSiteRenderer class.</p>
120  *
121  * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
122  * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
123  * @since 1.0
124  */
125 @Singleton
126 @Named
127 public class DefaultSiteRenderer implements Renderer {
128     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSiteRenderer.class);
129 
130     // ----------------------------------------------------------------------
131     // Requirements
132     // ----------------------------------------------------------------------
133 
134     @Inject
135     private VelocityComponent velocity;
136 
137     @Inject
138     private ParserModuleManager parserModuleManager;
139 
140     @Inject
141     private Doxia doxia;
142 
143     @Inject
144     private PlexusContainer plexus;
145 
146     @Inject
147     private Map<String, ContextCustomizer> contextCustomizers;
148 
149     private static final String SKIN_TEMPLATE_LOCATION = "META-INF/maven/site.vm";
150 
151     private static final String TOOLS_LOCATION = "META-INF/maven/site-tools.xml";
152 
153     private static final String DOXIA_SITE_RENDERER_VERSION = getSiteRendererVersion();
154 
155     public static final String MERMAID_VERSION;
156 
157     static {
158         try {
159             MERMAID_VERSION = getMavenProjectProperties().getProperty("mermaidVersion");
160         } catch (IOException e) {
161             throw new IllegalStateException("Failed to load mermaid version from properties", e);
162         }
163     }
164 
165     /**
166      * Properties contain some resolved properties from this project's Maven model
167      * @return some resolved Maven project properties
168      * @throws IOException in case the underlying file could not be accessed
169      */
170     static Properties getMavenProjectProperties() throws IOException {
171         final Properties properties = new Properties();
172         try (InputStream input = DefaultSiteRenderer.class.getResourceAsStream("/maven-project.properties")) {
173             if (input == null) {
174                 throw new IOException("Could not find \"/maven-project.properties\" in classpath");
175             }
176             properties.load(input);
177         }
178         return properties;
179     }
180 
181     // ----------------------------------------------------------------------
182     // SiteRenderer implementation
183     // ----------------------------------------------------------------------
184 
185     /** {@inheritDoc} */
186     public Map<String, DocumentRenderer> locateDocumentFiles(SiteRenderingContext siteRenderingContext)
187             throws IOException, RendererException {
188         Map<String, DocumentRenderer> files = new LinkedHashMap<>();
189         Map<String, String> moduleExcludes = siteRenderingContext.getModuleExcludes();
190 
191         // look in every site directory (in general src/site or target/generated-site)
192         for (SiteDirectory siteDirectory : siteRenderingContext.getSiteDirectories()) {
193             File siteDirectoryPath = siteDirectory.getPath();
194             if (siteDirectoryPath.exists()) {
195                 Collection<ParserModule> modules = parserModuleManager.getParserModules();
196                 // use every Doxia parser module
197                 for (ParserModule module : modules) {
198                     File moduleBasedir = new File(siteDirectoryPath, module.getSourceDirectory());
199 
200                     String excludes = (moduleExcludes == null) ? null : moduleExcludes.get(module.getParserId());
201 
202                     addModuleFiles(
203                             siteRenderingContext.getRootDirectory(),
204                             siteDirectory,
205                             moduleBasedir,
206                             module,
207                             excludes,
208                             files);
209                 }
210             }
211         }
212 
213         return files;
214     }
215 
216     private List<String> filterExtensionIgnoreCase(List<String> fileNames, String extension) {
217         List<String> filtered = new LinkedList<>(fileNames);
218         for (Iterator<String> it = filtered.iterator(); it.hasNext(); ) {
219             String name = it.next();
220 
221             // Take care of extension case
222             if (!endsWithIgnoreCase(name, extension)) {
223                 it.remove();
224             }
225         }
226         return filtered;
227     }
228 
229     /**
230      * Populates the files map with {@link DocumentRenderer}s per output name in parameter {@code files} for all files in the moduleBasedir matching the module extensions,
231      * taking care of duplicates if needed.
232      *
233      * @param siteRootDirectory
234      * @param siteDirectory
235      * @param moduleBasedir
236      * @param module
237      * @param excludes
238      * @param files
239      * @throws IOException
240      * @throws RendererException
241      */
242     private void addModuleFiles(
243             File siteRootDirectory,
244             SiteDirectory siteDirectory,
245             File moduleBasedir,
246             ParserModule module,
247             String excludes,
248             Map<String, DocumentRenderer> files)
249             throws IOException, RendererException {
250         if (!moduleBasedir.exists() || ArrayUtils.isEmpty(module.getExtensions())) {
251             return;
252         }
253 
254         List<String> allFiles = FileUtils.getFileNames(moduleBasedir, "**/*", excludes, false);
255 
256         for (String extension : module.getExtensions()) {
257             String fullExtension = "." + extension;
258 
259             List<String> docs = filterExtensionIgnoreCase(allFiles, fullExtension);
260 
261             // *.<extension>.vm
262             List<String> velocityFiles = filterExtensionIgnoreCase(allFiles, fullExtension + ".vm");
263 
264             docs.addAll(velocityFiles);
265 
266             for (String doc : docs) {
267                 DocumentRenderingContext docRenderingContext = new DocumentRenderingContext(
268                         moduleBasedir,
269                         doc,
270                         module.getParserId(),
271                         extension,
272                         siteRootDirectory,
273                         siteDirectory.getPath(),
274                         siteDirectory.getEditableSourceDirectories());
275 
276                 // TODO: DOXIA-111: we need a general filter here that knows how to alter the context
277                 if (endsWithIgnoreCase(doc, ".vm")) {
278                     docRenderingContext.setAttribute("velocity", "true");
279                 }
280 
281                 if (!checkForDuplicate(docRenderingContext, files, siteDirectory.isSkipDuplicates())) {
282                     String key = docRenderingContext.getOutputName();
283                     files.put(key, new DoxiaDocumentRenderer(docRenderingContext));
284                 }
285             }
286         }
287     }
288 
289     @FunctionalInterface
290     private interface DuplicateCallback {
291         /**
292          * Callback for handling duplicate files.
293          * @param message
294          * @return {@code false} if the duplicate should be ignored, {@code true} otherwise
295          * @throws RendererException
296          */
297         boolean onDuplicate(String message) throws RendererException;
298     }
299 
300     /**
301      * Checks if the newDocRenderingContext clashes with an existing document renderer.
302      * This check involves checking for duplicates both case-sensitive and case-insensitive.
303      * @param newDocRenderingContext the doc rendering context of a new file
304      * @param existingDocumentRenderers the map of already existing renderers
305      * @return {@code true} if no duplicates were found, {@code false} otherwise
306      * @throws RendererException
307      */
308     private boolean checkForDuplicate(
309             DocumentRenderingContext newDocRenderingContext,
310             Map<String, DocumentRenderer> existingDocumentRenderers,
311             boolean skipDuplicates)
312             throws RendererException {
313         DuplicateCallback duplicateCallback = (message) -> {
314             if (skipDuplicates) {
315                 LOGGER.debug(message + " (ignored due to flag 'skipDuplicates').");
316             } else {
317                 throw new RendererException(message + ".");
318             }
319             return true;
320         };
321 
322         DuplicateCallback caseInsensitiveDuplicateCallback = (message) -> {
323             if (Os.isFamily(Os.FAMILY_WINDOWS)) {
324                 return duplicateCallback.onDuplicate(message);
325             } else {
326                 if (LOGGER.isWarnEnabled()) {
327                     LOGGER.warn(message + " in case a case-insensitive filesystem is used.");
328                 }
329                 return false;
330             }
331         };
332 
333         if (!checkForDuplicate(newDocRenderingContext, existingDocumentRenderers::get, duplicateCallback)) {
334             // also check for case-insensitive duplicates
335             return checkForDuplicate(
336                     newDocRenderingContext,
337                     key -> existingDocumentRenderers.entrySet().stream()
338                             .filter(e -> e.getKey().equalsIgnoreCase(key))
339                             .findFirst()
340                             .map(Entry::getValue)
341                             .orElse(null),
342                     caseInsensitiveDuplicateCallback);
343         }
344         return true;
345     }
346 
347     private boolean checkForDuplicate(
348             DocumentRenderingContext newDocRenderingContext,
349             Function<String, DocumentRenderer> lookupFunction,
350             DuplicateCallback callback)
351             throws RendererException {
352         DocumentRenderer originalDocRenderer = lookupFunction.apply(newDocRenderingContext.getOutputName());
353         if (originalDocRenderer != null) {
354             DocumentRenderingContext originalDocRenderingContext = originalDocRenderer.getRenderingContext();
355 
356             File originalFile =
357                     new File(originalDocRenderingContext.getBasedir(), originalDocRenderingContext.getInputName());
358 
359             File newFile = new File(newDocRenderingContext.getBasedir(), newDocRenderingContext.getInputName());
360             String message = "File '" + newFile + "' clashes with existing '" + originalFile + "'";
361             return callback.onDuplicate(message);
362         }
363         return false;
364     }
365 
366     /** {@inheritDoc} */
367     public void render(
368             Collection<DocumentRenderer> documents, SiteRenderingContext siteRenderingContext, File outputDirectory)
369             throws RendererException, IOException {
370         for (DocumentRenderer docRenderer : documents) {
371             DocumentRenderingContext docRenderingContext = docRenderer.getRenderingContext();
372 
373             File outputFile = new File(outputDirectory, docRenderer.getOutputName());
374 
375             File inputFile = new File(docRenderingContext.getBasedir(), docRenderingContext.getInputName());
376 
377             boolean modified = !outputFile.exists()
378                     || (inputFile.lastModified() > outputFile.lastModified())
379                     || (siteRenderingContext.getSiteModel().getLastModified() > outputFile.lastModified());
380 
381             if (modified || docRenderer.isOverwrite()) {
382                 if (!outputFile.getParentFile().exists()) {
383                     outputFile.getParentFile().mkdirs();
384                 }
385 
386                 if (LOGGER.isDebugEnabled()) {
387                     LOGGER.debug("Generating " + outputFile);
388                 }
389 
390                 Writer writer = null;
391                 try {
392                     if (!docRenderer.isExternalReport()) {
393                         writer = WriterFactory.newWriter(outputFile, siteRenderingContext.getOutputEncoding());
394                     }
395                     docRenderer.renderDocument(writer, this, siteRenderingContext);
396                 } finally {
397                     IOUtil.close(writer);
398                 }
399             } else {
400                 if (LOGGER.isDebugEnabled()) {
401                     LOGGER.debug(inputFile + " unchanged, not regenerating...");
402                 }
403             }
404         }
405     }
406 
407     /** {@inheritDoc} */
408     public void renderDocument(
409             Writer writer, DocumentRenderingContext docRenderingContext, SiteRenderingContext siteContext)
410             throws RendererException {
411         SiteRendererSink sink = new SiteRendererSink(
412                 docRenderingContext,
413                 siteContext.getSiteModel() != null ? siteContext.getSiteModel().getMermaid() : null);
414 
415         File doc = new File(docRenderingContext.getBasedir(), docRenderingContext.getInputName());
416 
417         Reader reader = null;
418         try {
419             String resource = doc.getAbsolutePath();
420 
421             Parser parser = doxia.getParser(docRenderingContext.getParserId());
422             ParserConfigurator configurator = siteContext.getParserConfigurator();
423             boolean isConfigured = false;
424             if (configurator != null) {
425                 isConfigured = configurator.configure(docRenderingContext.getParserId(), doc.toPath(), parser);
426             }
427             if (!isConfigured) {
428                 // DOXIASITETOOLS-146 don't render comments from source markup
429                 parser.setEmitComments(false);
430                 parser.setEmitAnchorsForIndexableEntries(true);
431             }
432 
433             // TODO: DOXIA-111: the filter used here must be checked generally.
434             if (docRenderingContext.getAttribute("velocity") != null) {
435                 LOGGER.debug("Processing Velocity for " + docRenderingContext.getDoxiaSourcePath());
436                 try {
437                     Context vc = createDocumentVelocityContext(docRenderingContext, siteContext);
438 
439                     StringWriter sw = new StringWriter();
440 
441                     velocity.getEngine().mergeTemplate(resource, siteContext.getInputEncoding(), vc, sw);
442 
443                     String doxiaContent = sw.toString();
444 
445                     if (siteContext.getProcessedContentOutput() != null) {
446                         // save Velocity processing result, ie the Doxia content that will be parsed after
447                         saveVelocityProcessedContent(docRenderingContext, siteContext, doxiaContent);
448                     }
449 
450                     reader = new StringReader(doxiaContent);
451                 } catch (VelocityException e) {
452                     throw new RendererException(
453                             "Error parsing " + docRenderingContext.getDoxiaSourcePath() + " as a Velocity template", e);
454                 }
455 
456                 if (parser.getType() == Parser.XML_TYPE && siteContext.isValidate()) {
457                     reader = validate(reader, resource);
458                 }
459             } else {
460                 switch (parser.getType()) {
461                     case Parser.XML_TYPE:
462                         reader = ReaderFactory.newXmlReader(doc);
463                         if (siteContext.isValidate()) {
464                             reader = validate(reader, resource);
465                         }
466                         break;
467 
468                     case Parser.TXT_TYPE:
469                     case Parser.UNKNOWN_TYPE:
470                     default:
471                         reader = ReaderFactory.newReader(doc, siteContext.getInputEncoding());
472                 }
473             }
474 
475             doxia.parse(reader, docRenderingContext.getParserId(), sink, docRenderingContext.getDoxiaSourcePath());
476         } catch (ParserNotFoundException e) {
477             throw new RendererException("Error getting a parser for '" + doc + "'", e);
478         } catch (ParseException e) {
479             StringBuilder errorMsgBuilder = new StringBuilder();
480             errorMsgBuilder.append("Error parsing '").append(doc).append("'");
481             if (e.getLineNumber() > 0) {
482                 errorMsgBuilder.append(", line ").append(e.getLineNumber());
483             }
484             throw new RendererException(errorMsgBuilder.toString(), e);
485         } catch (IOException e) {
486             throw new RendererException("Error while processing '" + doc + "'", e);
487         } finally {
488             sink.flush();
489 
490             sink.close();
491 
492             IOUtil.close(reader);
493         }
494 
495         mergeDocumentIntoSite(writer, (DocumentContent) sink, siteContext);
496     }
497 
498     private void saveVelocityProcessedContent(
499             DocumentRenderingContext docRenderingContext, SiteRenderingContext siteContext, String doxiaContent)
500             throws IOException {
501         if (!siteContext.getProcessedContentOutput().exists()) {
502             siteContext.getProcessedContentOutput().mkdirs();
503         }
504 
505         String inputPath = docRenderingContext.getInputName();
506         // Remove .vm suffix
507         File outputFile =
508                 new File(siteContext.getProcessedContentOutput(), inputPath.substring(0, inputPath.length() - 3));
509 
510         File outputParent = outputFile.getParentFile();
511         if (!outputParent.exists()) {
512             outputParent.mkdirs();
513         }
514 
515         FileUtils.fileWrite(outputFile, siteContext.getInputEncoding(), doxiaContent);
516     }
517 
518     /**
519      * Creates a Velocity Context with all generic tools configured wit the site rendering context.
520      *
521      * @param siteRenderingContext the site rendering context
522      * @return a Velocity tools managed context
523      */
524     protected Context createToolManagedVelocityContext(SiteRenderingContext siteRenderingContext) {
525         Locale locale = siteRenderingContext.getLocale();
526         String dateFormat = siteRenderingContext.getSiteModel().getPublishDate().getFormat();
527         String timeZoneId = siteRenderingContext.getSiteModel().getPublishDate().getTimezone();
528         TimeZone timeZone =
529                 "system".equalsIgnoreCase(timeZoneId) ? TimeZone.getDefault() : TimeZone.getTimeZone(timeZoneId);
530 
531         EasyFactoryConfiguration config = new EasyFactoryConfiguration(false);
532         config.property("safeMode", Boolean.FALSE);
533         config.toolbox(Scope.REQUEST)
534                 .tool(ContextTool.class)
535                 .tool(LinkTool.class)
536                 .tool(LoopTool.class)
537                 .tool(RenderTool.class);
538         config.toolbox(Scope.APPLICATION)
539                 .property("locale", locale)
540                 .tool(AlternatorTool.class)
541                 .tool(ClassTool.class)
542                 .tool(ComparisonDateTool.class)
543                 .property("format", dateFormat)
544                 .property("timezone", timeZone)
545                 .tool(ConversionTool.class)
546                 .property("dateFormat", dateFormat)
547                 .tool(DisplayTool.class)
548                 .tool(EscapeTool.class)
549                 .tool(FieldTool.class)
550                 .tool(MathTool.class)
551                 .tool(NumberTool.class)
552                 .tool(ResourceTool.class)
553                 .property("bundles", new String[] {"site-renderer"})
554                 .tool(SortTool.class)
555                 .tool(XmlTool.class);
556 
557         FactoryConfiguration customConfig = ConfigurationUtils.findInClasspath(TOOLS_LOCATION);
558 
559         if (customConfig != null) {
560             config.addConfiguration(customConfig);
561         }
562 
563         ToolManager manager = new ToolManager(false, false);
564         manager.configure(config);
565 
566         return manager.createContext();
567     }
568 
569     /**
570      * Create a Velocity Context for a Doxia document, containing every information about rendered document.
571      *
572      * @param docRenderingContext the document's rendering context (may be {@code null} in which case the context does not contain document-specific information)
573      * @param siteRenderingContext the site rendering context
574      * @return a Velocity tools managed context
575      */
576     protected Context createDocumentVelocityContext(
577             DocumentRenderingContext docRenderingContext, SiteRenderingContext siteRenderingContext) {
578         Context context = createToolManagedVelocityContext(siteRenderingContext);
579         if (docRenderingContext != null) {
580             // ----------------------------------------------------------------------
581             // Data objects
582             // ----------------------------------------------------------------------
583 
584             context.put("relativePath", docRenderingContext.getRelativePath());
585 
586             String currentFilePath = docRenderingContext.getOutputName();
587             context.put("currentFilePath", currentFilePath);
588             // TODO Deprecated -- will be removed!
589             context.put("currentFileName", currentFilePath);
590 
591             String alignedFilePath = PathTool.calculateLink(currentFilePath, docRenderingContext.getRelativePath());
592             context.put("alignedFilePath", alignedFilePath);
593             // TODO Deprecated -- will be removed!
594             context.put("alignedFileName", alignedFilePath);
595 
596             for (Map.Entry<String, ContextCustomizer> entry : contextCustomizers.entrySet()) {
597                 try {
598                     LOGGER.debug("Applying Velocity context customizer '" + entry.getKey() + "'");
599                     entry.getValue().customizeContext(context, docRenderingContext, siteRenderingContext);
600                 } catch (Exception e) {
601                     LOGGER.warn(
602                             "Velocity context customizer '" + entry.getKey()
603                                     + "' threw an exception and will be ignored",
604                             e);
605                 }
606             }
607         }
608         context.put("site", siteRenderingContext.getSiteModel());
609         // TODO Deprecated -- will be removed!
610         context.put("decoration", siteRenderingContext.getSiteModel());
611 
612         context.put("locale", siteRenderingContext.getLocale());
613         context.put("supportedLocales", Collections.unmodifiableList(siteRenderingContext.getSiteLocales()));
614 
615         context.put("publishDate", siteRenderingContext.getPublishDate());
616 
617         if (DOXIA_SITE_RENDERER_VERSION != null) {
618             context.put("doxiaSiteRendererVersion", DOXIA_SITE_RENDERER_VERSION);
619         }
620 
621         // Add user properties
622         Map<String, ?> templateProperties = siteRenderingContext.getTemplateProperties();
623 
624         if (templateProperties != null) {
625             for (Map.Entry<String, ?> entry : templateProperties.entrySet()) {
626                 context.put(entry.getKey(), entry.getValue());
627             }
628         }
629 
630         // ----------------------------------------------------------------------
631         // Tools
632         // ----------------------------------------------------------------------
633 
634         context.put("PathTool", new PathTool());
635 
636         context.put("StringUtils", new StringUtils());
637 
638         context.put("plexus", plexus);
639         return context;
640     }
641 
642     /**
643      * Create a Velocity Context for the site template decorating the document. In addition to all the informations
644      * from the document, this context contains data gathered in {@link SiteRendererSink} during document rendering.
645      *
646      * @param content the document content to be merged into the template
647      * @param siteRenderingContext the site rendering context
648      * @return a Velocity tools managed context
649      */
650     protected Context createSiteTemplateVelocityContext(
651             DocumentContent content, SiteRenderingContext siteRenderingContext) {
652         // first get the context from document
653         Context context = createDocumentVelocityContext(content.getRenderingContext(), siteRenderingContext);
654 
655         // then add data objects from rendered document
656 
657         // Add infos from document
658         Collection<String> authors = content.getAuthors();
659         if (authors != null && !authors.isEmpty()) {
660             context.put("authors", authors);
661         } else {
662             // use scmModifiedDate (if available) as fallback
663             context.put("authors", context.get("scmModifiedAuthor"));
664         }
665 
666         String shortTitle = content.getTitle();
667         context.put("shortTitle", shortTitle);
668 
669         String projectTitle = null;
670         if (StringUtils.isNotEmpty(siteRenderingContext.getSiteModel().getName())) {
671             projectTitle = siteRenderingContext.getSiteModel().getName();
672         } else if (StringUtils.isNotEmpty(siteRenderingContext.getDefaultTitle())) {
673             projectTitle = siteRenderingContext.getDefaultTitle();
674         }
675 
676         StringBuilder title = new StringBuilder();
677         if (StringUtils.isNotEmpty(shortTitle)) {
678             title.append(shortTitle);
679         }
680 
681         if (title.length() > 0 && StringUtils.isNotEmpty(projectTitle)) {
682             title.append(" \u2013 "); // Symbol Name: En Dash
683         }
684 
685         if (StringUtils.isNotEmpty(projectTitle)) {
686             title.append(projectTitle);
687         }
688 
689         context.put("title", title.length() > 0 ? title.toString() : null);
690 
691         context.put("headContent", content.getHead());
692 
693         context.put("bodyContent", content.getBody());
694 
695         // document date (got from Doxia Sink date() API)
696         if (content.getDate() != null) {
697             context.put("documentDate", content.getDate());
698         } else {
699             // use scmModifiedDate (if available) as fallback
700             context.put("documentDate", context.get("scmModifiedDate"));
701         }
702 
703         // document rendering context, to get eventual inputPath
704         context.put("docRenderingContext", content.getRenderingContext());
705 
706         return context;
707     }
708 
709     public void generateDocument(Writer writer, SiteRendererSink sink, SiteRenderingContext siteRenderingContext)
710             throws RendererException {
711         mergeDocumentIntoSite(writer, sink, siteRenderingContext);
712     }
713 
714     /** {@inheritDoc} */
715     public void mergeDocumentIntoSite(Writer writer, DocumentContent content, SiteRenderingContext siteRenderingContext)
716             throws RendererException {
717         String templateName = siteRenderingContext.getTemplateName();
718 
719         LOGGER.debug("Processing Velocity for template " + templateName + " on "
720                 + content.getRenderingContext().getDoxiaSourcePath());
721 
722         Context context = createSiteTemplateVelocityContext(content, siteRenderingContext);
723 
724         ClassLoader old = null;
725 
726         if (siteRenderingContext.getTemplateClassLoader() != null) {
727             // -------------------------------------------------------------------------
728             // If no template classloader was set we'll just use the context classloader
729             // -------------------------------------------------------------------------
730 
731             old = Thread.currentThread().getContextClassLoader();
732 
733             Thread.currentThread().setContextClassLoader(siteRenderingContext.getTemplateClassLoader());
734         }
735 
736         try {
737             Template template;
738             Artifact skin = siteRenderingContext.getSkin();
739 
740             try {
741                 SkinModel skinModel = siteRenderingContext.getSkinModel();
742                 String encoding = (skinModel == null) ? null : skinModel.getEncoding();
743 
744                 template = (encoding == null)
745                         ? velocity.getEngine().getTemplate(templateName)
746                         : velocity.getEngine().getTemplate(templateName, encoding);
747             } catch (ParseErrorException pee) {
748                 throw new RendererException(
749                         "Velocity parsing error while reading the site template " + "from " + skin.getId() + " skin",
750                         pee);
751             } catch (ResourceNotFoundException rnfe) {
752                 throw new RendererException(
753                         "Could not find the site template " + "from " + skin.getId() + " skin", rnfe);
754             }
755 
756             try {
757                 StringWriter sw = new StringWriter();
758                 template.merge(context, sw);
759                 writer.write(sw.toString().replaceAll("\r?\n", SystemUtils.LINE_SEPARATOR));
760             } catch (VelocityException ve) {
761                 throw new RendererException("Velocity error while merging site template.", ve);
762             } catch (IOException ioe) {
763                 throw new RendererException("IO exception while merging site template.", ioe);
764             }
765         } finally {
766             IOUtil.close(writer);
767 
768             if (old != null) {
769                 Thread.currentThread().setContextClassLoader(old);
770             }
771         }
772     }
773 
774     private SiteRenderingContext createSiteRenderingContext(
775             Map<String, ?> attributes, SiteModel siteModel, String defaultTitle, Locale locale) {
776         SiteRenderingContext context = new SiteRenderingContext();
777 
778         context.setTemplateProperties(attributes);
779         context.setLocale(locale);
780         context.setSiteModel(siteModel);
781         context.setDefaultTitle(defaultTitle);
782 
783         return context;
784     }
785 
786     /** {@inheritDoc} */
787     public SiteRenderingContext createContextForSkin(
788             Artifact skin, Map<String, ?> attributes, SiteModel siteModel, String defaultTitle, Locale locale)
789             throws IOException, RendererException {
790         SiteRenderingContext context = createSiteRenderingContext(attributes, siteModel, defaultTitle, locale);
791 
792         context.setSkin(skin);
793 
794         ZipFile zipFile = getZipFile(skin.getFile());
795         InputStream in = null;
796 
797         try {
798             if (zipFile.getEntry(SKIN_TEMPLATE_LOCATION) == null) {
799                 throw new RendererException("Skin does not contain template at " + SKIN_TEMPLATE_LOCATION);
800             }
801             context.setTemplateName(SKIN_TEMPLATE_LOCATION);
802             context.setTemplateClassLoader(
803                     new URLClassLoader(new URL[] {skin.getFile().toURI().toURL()}));
804 
805             ZipEntry skinDescriptorEntry = zipFile.getEntry(SkinModel.SKIN_DESCRIPTOR_LOCATION);
806             if (skinDescriptorEntry != null) {
807                 in = zipFile.getInputStream(skinDescriptorEntry);
808 
809                 SkinModel skinModel = new SkinXpp3Reader().read(in);
810                 context.setSkinModel(skinModel);
811 
812                 String toolsPrerequisite = skinModel.getPrerequisites() == null
813                         ? null
814                         : skinModel.getPrerequisites().getDoxiaSitetools();
815 
816                 Package p = DefaultSiteRenderer.class.getPackage();
817                 String current = (p == null) ? null : p.getImplementationVersion();
818 
819                 if (StringUtils.isNotBlank(toolsPrerequisite)
820                         && (current != null)
821                         && !matchVersion(current, toolsPrerequisite)) {
822                     throw new RendererException("Your current Doxia Sitetools version " + current
823                             + " does not match the requirements of the skin (" + toolsPrerequisite
824                             + "). In order to fix this choose a skin version that is "
825                             + "compatible with this Doxia Sitetools version.");
826                 }
827             }
828         } catch (XmlPullParserException e) {
829             throw new RendererException(
830                     "Failed to parse " + SkinModel.SKIN_DESCRIPTOR_LOCATION + " skin descriptor from " + skin.getId()
831                             + " skin",
832                     e);
833         } finally {
834             IOUtil.close(in);
835             closeZipFile(zipFile);
836         }
837 
838         return context;
839     }
840 
841     boolean matchVersion(String current, String prerequisite) throws RendererException {
842         try {
843             ArtifactVersion v = new DefaultArtifactVersion(current);
844             VersionRange vr = VersionRange.createFromVersionSpec(prerequisite);
845 
846             boolean matched = false;
847             ArtifactVersion recommendedVersion = vr.getRecommendedVersion();
848             if (recommendedVersion == null) {
849                 List<Restriction> restrictions = vr.getRestrictions();
850                 for (Restriction restriction : restrictions) {
851                     if (restriction.containsVersion(v)) {
852                         matched = true;
853                         break;
854                     }
855                 }
856             } else {
857                 // only singular versions ever have a recommendedVersion
858                 @SuppressWarnings("unchecked")
859                 int compareTo = recommendedVersion.compareTo(v);
860                 matched = (compareTo <= 0);
861             }
862 
863             if (LOGGER.isDebugEnabled()) {
864                 LOGGER.debug("Skin doxia-sitetools prerequisite: " + prerequisite + ", current: " + current
865                         + ", matched = " + matched);
866             }
867 
868             return matched;
869         } catch (InvalidVersionSpecificationException e) {
870             throw new RendererException("Invalid skin doxia-sitetools prerequisite: " + prerequisite, e);
871         }
872     }
873 
874     /** {@inheritDoc} */
875     public void copyResources(SiteRenderingContext siteRenderingContext, File outputDirectory) throws IOException {
876         ZipFile file = getZipFile(siteRenderingContext.getSkin().getFile());
877 
878         Context velocityContext = createDocumentVelocityContext(null, siteRenderingContext);
879         Map<String, String> resourceConditions = createResourceConditionsMap(siteRenderingContext.getSkinModel());
880         try {
881             for (Enumeration<? extends ZipEntry> e = file.entries(); e.hasMoreElements(); ) {
882                 ZipEntry entry = e.nextElement();
883 
884                 if (!entry.getName().startsWith("META-INF/")) {
885                     File destFile = new File(outputDirectory, entry.getName());
886                     if (!entry.isDirectory()) {
887                         if (destFile.exists()) {
888                             // don't override existing content: avoids extra rewrite with same content or extra site
889                             // resource
890                             continue;
891                         }
892                         if (!isResourceRelevant(entry.getName(), velocityContext, resourceConditions)) {
893                             continue;
894                         }
895                         destFile.getParentFile().mkdirs();
896 
897                         copyFileFromZip(file, entry, destFile);
898                     } else {
899                         destFile.mkdirs();
900                     }
901                 }
902             }
903         } finally {
904             closeZipFile(file);
905         }
906 
907         // Copy extra site resources
908         for (SiteDirectory siteDirectory : siteRenderingContext.getSiteDirectories()) {
909             File resourcesDirectory = new File(siteDirectory.getPath(), "resources");
910 
911             if (resourcesDirectory != null && resourcesDirectory.exists()) {
912                 copyDirectory(resourcesDirectory, outputDirectory);
913             }
914         }
915 
916         // Check for the existence of /css/site.css
917         File siteCssFile = new File(outputDirectory, "/css/site.css");
918         if (!siteCssFile.exists()) {
919             // Create the subdirectory css if it doesn't exist, DOXIA-151
920             File cssDirectory = new File(outputDirectory, "/css/");
921             boolean created = cssDirectory.mkdirs();
922             if (created && LOGGER.isDebugEnabled()) {
923                 LOGGER.debug("The directory '" + cssDirectory.getAbsolutePath() + "' did not exist. It was created.");
924             }
925 
926             // If the file is not there - create an empty file, DOXIA-86
927             if (LOGGER.isDebugEnabled()) {
928                 LOGGER.debug(
929                         "The file '" + siteCssFile.getAbsolutePath() + "' does not exist. Creating an empty file.");
930             }
931             Writer writer = null;
932             try {
933                 writer = WriterFactory.newWriter(siteCssFile, siteRenderingContext.getOutputEncoding());
934                 // DOXIA-290...the file should not be 0 bytes.
935                 writer.write("/* You can override this file with your own styles */");
936             } finally {
937                 IOUtil.close(writer);
938             }
939         }
940 
941         if (siteRenderingContext.getSiteModel().getMermaid() != null
942                 && siteRenderingContext.getSiteModel().getMermaid().getExternalJs() == null) {
943             final String name;
944             if (siteRenderingContext.getSiteModel().getMermaid().isUseTiny()) {
945                 // use integrated tiny version of mermaid, which is smaller and faster to load, but has some limitations
946                 // (e.g. no sequence diagrams)
947                 name = "/js/mermaid-" + MERMAID_VERSION + ".tiny.min.js";
948             } else {
949                 name = "/js/mermaid-" + MERMAID_VERSION + ".min.js";
950             }
951             copyFileFromResource(name, new File(outputDirectory, name));
952         }
953     }
954 
955     private static void copyFileFromResource(String name, File destFile) throws IOException {
956         destFile.getParentFile().mkdirs();
957         try (InputStream in = DefaultSiteRenderer.class.getResourceAsStream(name)) {
958             if (in == null) {
959                 throw new IllegalArgumentException("Could not find the resource with name " + name);
960             } else {
961                 try (OutputStream out = new FileOutputStream(destFile)) {
962                     IOUtil.copy(in, out);
963                 }
964             }
965         }
966     }
967 
968     private boolean isResourceRelevant(String name, Context velocityContext, Map<String, String> resourceConditions) {
969         if (resourceConditions == null || !resourceConditions.containsKey(name)) {
970             LOGGER.debug("No condition for resource '{}'", name);
971         } else {
972             String condition = resourceConditions.get(name);
973             LOGGER.debug(
974                     "Evaluating condition for resource '{}' with condition '{}'",
975                     name,
976                     escapeLineBreaksForLogging(condition));
977             StringWriter writer = new StringWriter();
978             Velocity.evaluate(velocityContext, writer, "conditional-resource-evaluation", condition);
979             String result = writer.toString().trim();
980             LOGGER.debug("Condition evaluation result: {}", result);
981             if (!Boolean.parseBoolean(result)) {
982                 LOGGER.debug("Excluding resource '{}'", name);
983                 return false;
984             }
985         }
986         return true;
987     }
988 
989     private Map<String, String> createResourceConditionsMap(SkinModel skinModel) {
990         if (skinModel == null) {
991             LOGGER.debug("No skin model provided, so no resource conditions will be applied.");
992             return Collections.emptyMap();
993         }
994         Map<String, String> resourceConditions = new HashMap<>();
995         for (ResourceCondition resource : skinModel.getResourceConditions()) {
996             if (resource.getVtlCondition() != null
997                     && !resource.getVtlCondition().isEmpty()) {
998                 for (String resourceName : resource.getResourceNames()) {
999                     if (resourceConditions.containsKey(resourceName)) {
1000                         LOGGER.warn(
1001                                 "Multiple conditions found for resource '{}'. Only the first one will be used.",
1002                                 resourceName);
1003                         continue;
1004                     }
1005                     LOGGER.debug(
1006                             "Adding condition for resource '{}' with condition '{}'",
1007                             resourceName,
1008                             escapeLineBreaksForLogging(resource.getVtlCondition()));
1009                     resourceConditions.put(resourceName, resource.getVtlCondition());
1010                 }
1011             }
1012         }
1013         return resourceConditions;
1014     }
1015 
1016     private static String escapeLineBreaksForLogging(String input) {
1017         return input.replaceAll("\\r?\\n", "\\\\n");
1018     }
1019 
1020     private static void copyFileFromZip(ZipFile file, ZipEntry entry, File destFile) throws IOException {
1021         FileOutputStream fos = new FileOutputStream(destFile);
1022 
1023         try {
1024             IOUtil.copy(file.getInputStream(entry), fos);
1025         } finally {
1026             IOUtil.close(fos);
1027         }
1028     }
1029 
1030     /**
1031      * Copy the directory
1032      *
1033      * @param source      source file to be copied
1034      * @param destination destination file
1035      * @throws java.io.IOException if any
1036      */
1037     protected void copyDirectory(File source, File destination) throws IOException {
1038         if (source.exists()) {
1039             DirectoryScanner scanner = new DirectoryScanner();
1040 
1041             String[] includedResources = {"**/*"};
1042 
1043             scanner.setIncludes(includedResources);
1044 
1045             scanner.addDefaultExcludes();
1046 
1047             scanner.setBasedir(source);
1048 
1049             scanner.scan();
1050 
1051             List<String> includedFiles = Arrays.asList(scanner.getIncludedFiles());
1052 
1053             for (String name : includedFiles) {
1054                 File sourceFile = new File(source, name);
1055 
1056                 File destinationFile = new File(destination, name);
1057 
1058                 FileUtils.copyFile(sourceFile, destinationFile);
1059             }
1060         }
1061     }
1062 
1063     private Reader validate(Reader source, String resource) throws ParseException, IOException {
1064         LOGGER.debug("Validating: " + resource);
1065 
1066         try {
1067             String content = IOUtil.toString(new BufferedReader(source));
1068 
1069             new XmlValidator().validate(content);
1070 
1071             return new StringReader(content);
1072         } finally {
1073             IOUtil.close(source);
1074         }
1075     }
1076 
1077     // TODO replace with StringUtils.endsWithIgnoreCase() from maven-shared-utils 0.7
1078     static boolean endsWithIgnoreCase(String str, String searchStr) {
1079         if (str.length() < searchStr.length()) {
1080             return false;
1081         }
1082 
1083         return str.regionMatches(true, str.length() - searchStr.length(), searchStr, 0, searchStr.length());
1084     }
1085 
1086     private static ZipFile getZipFile(File file) throws IOException {
1087         if (file == null) {
1088             throw new IOException("Error opening ZipFile: null");
1089         }
1090 
1091         try {
1092             // TODO: plexus-archiver, if it could do the excludes
1093             return new ZipFile(file);
1094         } catch (ZipException ex) {
1095             IOException ioe = new IOException("Error opening ZipFile: " + file.getAbsolutePath());
1096             ioe.initCause(ex);
1097             throw ioe;
1098         }
1099     }
1100 
1101     private static void closeZipFile(ZipFile zipFile) {
1102         // TODO: move to plexus utils
1103         try {
1104             zipFile.close();
1105         } catch (IOException e) {
1106             // ignore
1107         }
1108     }
1109 
1110     private static String getSiteRendererVersion() {
1111         InputStream inputStream = DefaultSiteRenderer.class.getResourceAsStream(
1112                 "/META-INF/" + "maven/org.apache.maven.doxia/doxia-site-renderer/pom.properties");
1113         if (inputStream == null) {
1114             LOGGER.debug("pom.properties for doxia-site-renderer not found");
1115         } else {
1116             Properties properties = new Properties();
1117             try (InputStream in = inputStream) {
1118                 properties.load(in);
1119                 return properties.getProperty("version");
1120             } catch (IOException e) {
1121                 LOGGER.debug("Failed to load pom.properties, so Doxia SiteRenderer version will not be available", e);
1122             }
1123         }
1124 
1125         return null;
1126     }
1127 }