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