1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
120
121
122
123
124
125 @Singleton
126 @Named
127 public class DefaultSiteRenderer implements Renderer {
128 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSiteRenderer.class);
129
130
131
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
167
168
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
183
184
185
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
192 for (SiteDirectory siteDirectory : siteRenderingContext.getSiteDirectories()) {
193 File siteDirectoryPath = siteDirectory.getPath();
194 if (siteDirectoryPath.exists()) {
195 Collection<ParserModule> modules = parserModuleManager.getParserModules();
196
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
222 if (!endsWithIgnoreCase(name, extension)) {
223 it.remove();
224 }
225 }
226 return filtered;
227 }
228
229
230
231
232
233
234
235
236
237
238
239
240
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
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
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
293
294
295
296
297 boolean onDuplicate(String message) throws RendererException;
298 }
299
300
301
302
303
304
305
306
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
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
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
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
429 parser.setEmitComments(false);
430 parser.setEmitAnchorsForIndexableEntries(true);
431 }
432
433
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
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
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
520
521
522
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
571
572
573
574
575
576 protected Context createDocumentVelocityContext(
577 DocumentRenderingContext docRenderingContext, SiteRenderingContext siteRenderingContext) {
578 Context context = createToolManagedVelocityContext(siteRenderingContext);
579 if (docRenderingContext != null) {
580
581
582
583
584 context.put("relativePath", docRenderingContext.getRelativePath());
585
586 String currentFilePath = docRenderingContext.getOutputName();
587 context.put("currentFilePath", currentFilePath);
588
589 context.put("currentFileName", currentFilePath);
590
591 String alignedFilePath = PathTool.calculateLink(currentFilePath, docRenderingContext.getRelativePath());
592 context.put("alignedFilePath", alignedFilePath);
593
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
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
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
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
644
645
646
647
648
649
650 protected Context createSiteTemplateVelocityContext(
651 DocumentContent content, SiteRenderingContext siteRenderingContext) {
652
653 Context context = createDocumentVelocityContext(content.getRenderingContext(), siteRenderingContext);
654
655
656
657
658 Collection<String> authors = content.getAuthors();
659 if (authors != null && !authors.isEmpty()) {
660 context.put("authors", authors);
661 } else {
662
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 ");
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
696 if (content.getDate() != null) {
697 context.put("documentDate", content.getDate());
698 } else {
699
700 context.put("documentDate", context.get("scmModifiedDate"));
701 }
702
703
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
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
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
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
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
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
889
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
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
917 File siteCssFile = new File(outputDirectory, "/css/site.css");
918 if (!siteCssFile.exists()) {
919
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
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
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
946
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
1032
1033
1034
1035
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
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
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
1103 try {
1104 zipFile.close();
1105 } catch (IOException e) {
1106
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 }