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
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.Reader;
29 import java.io.StringWriter;
30 import java.io.UncheckedIOException;
31 import java.net.URI;
32 import java.nio.charset.StandardCharsets;
33 import java.nio.file.FileSystem;
34 import java.nio.file.FileSystems;
35 import java.nio.file.Files;
36 import java.nio.file.Path;
37 import java.nio.file.StandardCopyOption;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.Locale;
43 import java.util.Map;
44 import java.util.Set;
45 import java.util.jar.JarOutputStream;
46 import java.util.stream.Stream;
47 import java.util.zip.ZipEntry;
48
49 import org.apache.commons.io.IOUtils;
50 import org.apache.maven.artifact.Artifact;
51 import org.apache.maven.artifact.DefaultArtifact;
52 import org.apache.maven.artifact.versioning.VersionRange;
53 import org.apache.maven.doxia.Doxia;
54 import org.apache.maven.doxia.parser.ParseException;
55 import org.apache.maven.doxia.sink.Sink;
56 import org.apache.maven.doxia.site.SiteModel;
57 import org.apache.maven.doxia.site.io.xpp3.SiteXpp3Reader;
58 import org.apache.maven.doxia.siterenderer.SiteRenderingContext.SiteDirectory;
59 import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
60 import org.apache.maven.doxia.xsd.AbstractXmlValidator;
61 import org.codehaus.plexus.PlexusContainer;
62 import org.codehaus.plexus.testing.PlexusTest;
63 import org.codehaus.plexus.util.FileUtils;
64 import org.codehaus.plexus.util.IOUtil;
65 import org.codehaus.plexus.util.ReaderFactory;
66 import org.codehaus.plexus.util.ReflectionUtils;
67 import org.codehaus.plexus.util.StringUtils;
68 import org.junit.jupiter.api.AfterEach;
69 import org.junit.jupiter.api.BeforeAll;
70 import org.junit.jupiter.api.BeforeEach;
71 import org.junit.jupiter.api.Test;
72 import org.mockito.Mockito;
73 import org.xml.sax.EntityResolver;
74
75 import static org.codehaus.plexus.testing.PlexusExtension.getBasedir;
76 import static org.codehaus.plexus.testing.PlexusExtension.getTestFile;
77 import static org.junit.jupiter.api.Assertions.assertEquals;
78 import static org.junit.jupiter.api.Assertions.assertFalse;
79 import static org.junit.jupiter.api.Assertions.assertNotNull;
80 import static org.junit.jupiter.api.Assertions.assertThrows;
81 import static org.junit.jupiter.api.Assertions.assertTrue;
82 import static org.junit.jupiter.api.Assertions.fail;
83 import static org.mockito.ArgumentMatchers.eq;
84 import static org.mockito.ArgumentMatchers.isNull;
85 import static org.mockito.Mockito.mock;
86 import static org.mockito.Mockito.spy;
87 import static org.mockito.Mockito.verify;
88 import static org.mockito.Mockito.when;
89
90
91
92
93
94 @PlexusTest
95 public class DefaultSiteRendererTest {
96
97
98
99 private static final String OUTPUT = "target/output";
100
101
102
103
104 private SiteRenderer siteRenderer;
105
106 @Inject
107 private PlexusContainer container;
108
109 private static File minimalSkinJar;
110
111 @BeforeAll
112 static void beforeAll() throws IOException {
113
114
115
116 minimalSkinJar = new File(getBasedir(), "target/test-classes/minimal-skin.jar");
117 createJarFromDirectory(getTestFile("src/test/resources/skin-minimal").toPath(), minimalSkinJar.toPath());
118 }
119
120
121
122
123 @BeforeEach
124 protected void setUp() throws Exception {
125 siteRenderer = (SiteRenderer) container.lookup(SiteRenderer.class);
126 }
127
128 private static void createJarFromDirectory(Path srcDirectory, Path jarfile) throws IOException {
129 Map<String, String> env = new HashMap<>();
130 env.put("create", "true");
131 try (FileSystem zipfs = FileSystems.newFileSystem(URI.create("jar:" + jarfile.toUri()), env)) {
132 deepCopy(srcDirectory, zipfs);
133 }
134 }
135
136 private static void deepCopy(Path src, FileSystem zipfs) throws IOException {
137 try (Stream<Path> stream = Files.walk(src)) {
138 stream.forEach(source -> {
139 try {
140
141 if (!src.equals(source)) {
142 Path relativeSrcPath = src.relativize(source);
143 if (Files.isDirectory(source)) {
144 Files.createDirectories(zipfs.getPath(relativeSrcPath.toString()));
145 } else {
146 Files.copy(
147 source,
148 zipfs.getPath(relativeSrcPath.toString()),
149 StandardCopyOption.REPLACE_EXISTING);
150 }
151 }
152 } catch (IOException e) {
153 throw new UncheckedIOException(e);
154 }
155 });
156 }
157 }
158
159
160
161
162 @AfterEach
163 protected void tearDown() throws Exception {
164 container.release(siteRenderer);
165 }
166
167
168
169
170 @Test
171 void renderExceptionMessageWhenLineNumberIsNotAvailable() throws Exception {
172 final File testBasedir = getTestFile("src/test/resources/site/xdoc");
173 final String testDocument = "head.xml";
174 final String exceptionMessage = "parse error occurred";
175
176 Doxia doxiaInstance = container.lookup(Doxia.class);
177 Doxia doxiaSpy = spy(doxiaInstance);
178 Mockito.doThrow(new ParseException(exceptionMessage))
179 .when(doxiaSpy)
180 .parse(Mockito.<Reader>any(), Mockito.anyString(), Mockito.<Sink>any(), Mockito.nullable(String.class));
181 SiteRenderer siteRenderer = container.lookup(SiteRenderer.class);
182 ReflectionUtils.setVariableValueInObject(siteRenderer, "doxia", doxiaSpy);
183
184 DocumentRenderingContext docRenderingContext = new DocumentRenderingContext(
185 testBasedir, "src/test/resources/site/xdoc", testDocument, "xdoc", "", false);
186
187 try {
188 siteRenderer.renderDocument(null, docRenderingContext, new SiteRenderingContext());
189 fail("should fail with exception");
190 } catch (RendererException e) {
191 assertEquals(
192 String.format(
193 "Error parsing '%s%s%s'", testBasedir.getAbsolutePath(), File.separator, testDocument),
194 e.getMessage());
195 }
196 }
197
198
199
200
201 @Test
202 void renderExceptionMessageWhenLineNumberIsAvailable() throws Exception {
203 final File testBasedir = getTestFile("src/test/resources/site/xdoc");
204 final String testDocumentName = "head.xml";
205 final String exceptionMessage = "parse error occurred";
206
207 Doxia doxiaInstance = container.lookup(Doxia.class);
208 Doxia doxiaSpy = spy(doxiaInstance);
209 Mockito.doThrow(new ParseException(exceptionMessage, 42, 36))
210 .when(doxiaSpy)
211 .parse(Mockito.<Reader>any(), Mockito.anyString(), Mockito.<Sink>any(), Mockito.nullable(String.class));
212 SiteRenderer siteRenderer = container.lookup(SiteRenderer.class);
213 ReflectionUtils.setVariableValueInObject(siteRenderer, "doxia", doxiaSpy);
214
215 DocumentRenderingContext docRenderingContext = new DocumentRenderingContext(
216 testBasedir, "src/test/resources/site/xdoc", testDocumentName, "xdoc", "", false);
217
218 try {
219 siteRenderer.renderDocument(null, docRenderingContext, new SiteRenderingContext());
220 fail("should fail with exception");
221 } catch (RendererException e) {
222 assertEquals(
223 String.format(
224 "Error parsing '%s%s%s', line 42",
225 testBasedir.getAbsolutePath(), File.separator, testDocumentName),
226 e.getMessage());
227 }
228 }
229
230
231
232
233 @Test
234 void render() throws Exception {
235 File outputDirectory = getTestFile(OUTPUT);
236
237 org.apache.commons.io.FileUtils.deleteDirectory(outputDirectory);
238
239
240
241
242 SiteModel siteModel =
243 new SiteXpp3Reader().read(new FileInputStream(getTestFile("src/test/resources/site/site.xml")));
244
245 SiteRenderingContext ctxt =
246 getSiteRenderingContext(siteModel, minimalSkinJar, "src/test/resources/site", false);
247 ctxt.setRootDirectory(getTestFile(""));
248 siteRenderer.render(siteRenderer.locateDocumentFiles(ctxt, true).values(), ctxt, outputDirectory);
249
250 ctxt = getSiteRenderingContext(siteModel, minimalSkinJar, "src/test/resources/site-validate", true);
251 ctxt.setRootDirectory(getTestFile(""));
252 siteRenderer.render(siteRenderer.locateDocumentFiles(ctxt, true).values(), ctxt, outputDirectory);
253
254 siteRenderer.copyResources(ctxt, outputDirectory);
255
256
257
258 verifyCdcPage();
259 verifyNestedItemsPage();
260 verifyMultipleBlock();
261 verifyMacro();
262 verifyEntitiesPage();
263 verifyJavascriptPage();
264 verifyFaqPage();
265 verifyAttributes();
266 verifyApt();
267 verifyExtensionInFilename();
268 verifyNewlines();
269 verifyMermaidPage(null);
270
271
272
273 validatePages();
274 }
275
276 void renderDocument(
277 SiteRenderingContext context, String baseDir, String document, String extension, String parserId)
278 throws Exception {
279 File outputDirectory = getTestFile(OUTPUT);
280
281 org.apache.commons.io.FileUtils.deleteDirectory(outputDirectory);
282
283 DocumentRenderingContext docRenderingContext =
284 new DocumentRenderingContext(getTestFile(baseDir), baseDir, document, parserId, extension, false);
285 DocumentRenderer docRenderer = new DoxiaDocumentRenderer(docRenderingContext);
286
287 siteRenderer.render(Collections.singletonList(docRenderer), context, outputDirectory);
288 siteRenderer.copyResources(context, outputDirectory);
289 }
290
291 @Test
292 void mermaidWithExternalJs() throws Exception {
293 SiteModel siteModel = new SiteXpp3Reader()
294 .read(new FileInputStream(getTestFile("src/test/resources/site-mermaid-externaljs/site.xml")));
295 SiteRenderingContext context =
296 getSiteRenderingContext(siteModel, minimalSkinJar, "src/test/resources/site", false);
297
298 renderDocument(context, "src/test/resources/site/markdown", "mermaid.md", "md", "markdown");
299
300 verifyMermaidPage(
301 "<script src=\"https://cdn.jsdelivr.net/npm/mermaid@11.12.3/dist/mermaid.min.js\" integrity=\"sha512-/yppbXbmq0NnMz+OhfzrvQDSePwPAixE8Ywi8U7KayJPv5ZFVYo7bzBgkR5V6OQ/8YHEh/gPMarWD+6ZIuNj5A==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\" fetchpriority=\"high\"></script>");
302 }
303
304 @Test
305 void externalReport() throws Exception {
306 DocumentRenderer docRenderer = mock(DocumentRenderer.class);
307 when(docRenderer.isExternalReport()).thenReturn(true);
308 when(docRenderer.getOutputName()).thenReturn("external/index");
309 when(docRenderer.getRenderingContext())
310 .thenReturn(new DocumentRenderingContext(new File(""), "index.html", "generator:external"));
311
312 SiteRenderingContext context = new SiteRenderingContext();
313
314 siteRenderer.render(Collections.singletonList(docRenderer), context, new File("target/output"));
315
316 verify(docRenderer).renderDocument(isNull(), eq(siteRenderer), eq(context));
317 }
318
319 @Test
320 void velocityToolManager() throws Exception {
321 StringWriter writer = new StringWriter();
322
323 SiteRenderingContext siteRenderingContext = new SiteRenderingContext();
324 siteRenderingContext.setSiteModel(new SiteModel());
325
326 Map<String, Object> attributes = new HashMap<>();
327
328
329
330
331
332 attributes.put("doxiaSiteRendererVersion", "1.7-bogus");
333
334 siteRenderingContext.setTemplateProperties(attributes);
335
336 siteRenderingContext.setTemplateName("org/apache/maven/doxia/siterenderer/velocity-toolmanager.vm");
337 DocumentRenderingContext docRenderingContext =
338 new DocumentRenderingContext(new File("."), "document.html", "generator");
339 SiteRendererSink sink = new SiteRendererSink(docRenderingContext);
340 siteRenderer.mergeDocumentIntoSite(writer, sink, siteRenderingContext);
341
342 String renderResult = writer.toString();
343 String expectedResult = IOUtils.toString(
344 getClass().getResourceAsStream("velocity-toolmanager.expected.txt"), StandardCharsets.UTF_8);
345 expectedResult = StringUtils.unifyLineSeparators(expectedResult);
346 assertEquals(expectedResult, renderResult);
347 }
348
349 @Test
350 void velocityToolManagerForSkin() throws Exception {
351 StringWriter writer = new StringWriter();
352
353 File skinFile = new File(getBasedir(), "target/test-classes/skin.jar");
354 try (InputStream skinIS = getClass().getResourceAsStream("velocity-toolmanager.vm");
355 JarOutputStream jarOS = new JarOutputStream(new FileOutputStream(skinFile))) {
356 jarOS.putNextEntry(new ZipEntry("META-INF/maven/site.vm"));
357 IOUtil.copy(skinIS, jarOS);
358 jarOS.closeEntry();
359 }
360
361 Map<String, Object> attributes = new HashMap<>();
362
363
364
365
366
367 attributes.put("doxiaSiteRendererVersion", "1.7-bogus");
368
369 Artifact skin = new DefaultArtifact(
370 "org.group", "artifact", VersionRange.createFromVersion("1.1"), null, "jar", "", null);
371 skin.setFile(skinFile);
372 SiteRenderingContext siteRenderingContext =
373 siteRenderer.createContextForSkin(skin, attributes, new SiteModel(), "defaultitle", Locale.ROOT);
374 DocumentRenderingContext context = new DocumentRenderingContext(new File(""), "document.html", "generator");
375 SiteRendererSink sink = new SiteRendererSink(context);
376 siteRenderer.mergeDocumentIntoSite(writer, sink, siteRenderingContext);
377 String renderResult = writer.toString();
378 String expectedResult = StringUtils.unifyLineSeparators(IOUtils.toString(
379 getClass().getResourceAsStream("velocity-toolmanager.expected.txt"), StandardCharsets.UTF_8));
380 assertEquals(expectedResult, renderResult);
381 }
382
383 @Test
384 void matchVersion() throws Exception {
385 DefaultSiteRenderer r = (DefaultSiteRenderer) siteRenderer;
386 assertTrue(r.matchVersion("1.7", "1.7"));
387 assertFalse(r.matchVersion("1.7", "1.8"));
388 }
389
390 @Test
391 void locateDocumentFiles() throws Exception {
392 SiteRenderingContext context = new SiteRenderingContext();
393 File sourceDirectory = getTestFile("src/test/resources/site-validate");
394 context.setRootDirectory(sourceDirectory);
395 context.addSiteDirectory(new SiteDirectory(sourceDirectory, true));
396 Set<String> outputFiles = siteRenderer.locateDocumentFiles(context).keySet();
397 Set<String> expectedOutputFiles = new HashSet<>();
398 expectedOutputFiles.add("entityTest.html");
399 assertEquals(expectedOutputFiles, outputFiles);
400 }
401
402 @Test
403 void locateDocumentFilesWithNameClashes() throws Exception {
404 SiteRenderingContext context = new SiteRenderingContext();
405 File sourceDirectory = getTestFile("src/test/resources/site-validate");
406 context.setRootDirectory(sourceDirectory);
407 context.addSiteDirectory(new SiteDirectory(sourceDirectory, true));
408 context.addSiteDirectory(new SiteDirectory(sourceDirectory, true));
409 assertThrows(RendererException.class, () -> siteRenderer.locateDocumentFiles(context));
410 }
411
412 @Test
413 void locateDocumentFilesWithNameClashesInSkippingDuplicatesDirectory() throws Exception {
414 SiteRenderingContext context = new SiteRenderingContext();
415 File sourceDirectory = getTestFile("src/test/resources/site-validate");
416 context.setRootDirectory(sourceDirectory);
417 context.addSiteDirectory(new SiteDirectory(sourceDirectory, true));
418 context.addSiteDirectory(new SiteDirectory(sourceDirectory, true, true));
419 Set<String> outputFiles = siteRenderer.locateDocumentFiles(context).keySet();
420 Set<String> expectedOutputFiles = new HashSet<>();
421 expectedOutputFiles.add("entityTest.html");
422 assertEquals(expectedOutputFiles, outputFiles);
423 }
424
425 @Test
426 void copyResourcesConditionally() throws Exception {
427 File skinJar = new File(getBasedir(), "target/test-classes/skin-with-conditional-resources.jar");
428 createJarFromDirectory(
429 getTestFile("src/test/resources/skin-with-conditional-resources")
430 .toPath(),
431 skinJar.toPath());
432
433 SiteModel siteModel =
434 new SiteXpp3Reader().read(new FileInputStream(getTestFile("src/test/resources/site/site.xml")));
435 SiteRenderingContext context = getSiteRenderingContext(siteModel, skinJar, "src/test/resources/site", false);
436 File sourceDirectory = getTestFile("src/test/resources/site-validate");
437 context.setRootDirectory(sourceDirectory);
438 Path outputDirectory = Files.createTempDirectory("site-output-");
439 siteRenderer.copyResources(context, outputDirectory.toFile());
440 assertTrue(Files.exists(outputDirectory.resolve("js/include.js")));
441 assertTrue(Files.exists(outputDirectory.resolve("js/include2.js")));
442 assertTrue(Files.exists(outputDirectory.resolve("js/include3.js")));
443 assertFalse(Files.exists(outputDirectory.resolve("js/exclude.js")));
444 }
445
446 private SiteRenderingContext getSiteRenderingContext(
447 SiteModel siteModel, File skinFile, String siteDir, boolean validate)
448 throws RendererException, IOException {
449
450 final Map<String, String> attributes = new HashMap<>();
451 attributes.put("outputEncoding", "UTF-8");
452
453 Artifact skin = new DefaultArtifact(
454 "org.group", "artifact", VersionRange.createFromVersion("1.1"), null, "jar", "", null);
455 skin.setFile(skinFile);
456 SiteRenderingContext siteRenderingContext =
457 siteRenderer.createContextForSkin(skin, attributes, siteModel, "defaultTitle", Locale.ROOT);
458 siteRenderingContext.addSiteDirectory(new SiteDirectory(getTestFile(siteDir), true));
459 siteRenderingContext.setValidate(validate);
460
461 return siteRenderingContext;
462 }
463
464
465
466
467 public void verifyCdcPage() throws Exception {
468 File nestedItems = getTestFile("target/output/cdc.html");
469 assertNotNull(nestedItems);
470 assertTrue(nestedItems.exists());
471 }
472
473
474
475
476 public void verifyNestedItemsPage() throws Exception {
477 NestedItemsVerifier verifier = new NestedItemsVerifier();
478 verifier.verify("target/output/nestedItems.html");
479 }
480
481
482
483
484 public void verifyMultipleBlock() throws Exception {
485 MultipleBlockVerifier verifier = new MultipleBlockVerifier();
486 verifier.verify("target/output/multipleblock.html");
487 }
488
489
490
491
492 public void verifyMacro() throws Exception {
493 File macro = getTestFile("target/output/macro.html");
494 assertNotNull(macro);
495 assertTrue(macro.exists());
496
497 Reader reader = null;
498 try {
499 reader = ReaderFactory.newXmlReader(macro);
500 String content = IOUtil.toString(reader);
501 assertEquals(-1, content.indexOf("</macro>"));
502 } finally {
503 IOUtil.close(reader);
504 }
505 }
506
507
508
509
510 public void verifyEntitiesPage() throws Exception {
511 EntitiesVerifier verifier = new EntitiesVerifier();
512 verifier.verify("target/output/entityTest.html");
513 }
514
515
516
517
518 public void verifyJavascriptPage() throws Exception {
519 JavascriptVerifier verifier = new JavascriptVerifier();
520 verifier.verify("target/output/javascript.html");
521 }
522
523 private void verifyMermaidPage(String externalJsScript) throws Exception {
524 MermaidVerifier verifier = new MermaidVerifier(externalJsScript);
525 verifier.verify("target/output/mermaid.html");
526 }
527
528
529
530
531 public void verifyFaqPage() throws Exception {
532 FaqVerifier verifier = new FaqVerifier();
533 verifier.verify("target/output/faq.html");
534 }
535
536
537
538
539 public void verifyAttributes() throws Exception {
540 AttributesVerifier verifier = new AttributesVerifier();
541 verifier.verify("target/output/attributes.html");
542 }
543
544
545
546
547 public void verifyApt() throws Exception {
548 AbstractVerifier verifier = new AptVerifier();
549 verifier.verify("target/output/apt.html");
550
551 verifier = new CommentsVerifier();
552 verifier.verify("target/output/apt.html");
553 }
554
555
556
557
558 public void verifyExtensionInFilename() throws Exception {
559 File output = getTestFile("target/output/extension.apt.not.at.end.html");
560 assertNotNull(output);
561 assertTrue(output.exists());
562 }
563
564
565
566
567 public void verifyNewlines() throws Exception {
568
569 checkNewlines(FileUtils.fileRead(getTestFile("target/output/apt.html"), "ISO-8859-1"));
570 checkNewlines(FileUtils.fileRead(getTestFile("target/output/cdc.html"), "ISO-8859-1"));
571 checkNewlines(FileUtils.fileRead(getTestFile("target/output/interpolation.html"), "ISO-8859-1"));
572
573 checkNewlines(FileUtils.fileRead(getTestFile("target/output/faq.html"), "ISO-8859-1"));
574
575 checkNewlines(FileUtils.fileRead(getTestFile("target/output/attributes.html"), "ISO-8859-1"));
576 checkNewlines(FileUtils.fileRead(getTestFile("target/output/javascript.html"), "ISO-8859-1"));
577 checkNewlines(FileUtils.fileRead(getTestFile("target/output/head.html"), "ISO-8859-1"));
578 checkNewlines(FileUtils.fileRead(getTestFile("target/output/macro.html"), "ISO-8859-1"));
579 }
580
581 private void checkNewlines(String content) {
582 int cr = StringUtils.countMatches(content, "\r");
583 int lf = StringUtils.countMatches(content, "\n");
584 assertTrue(
585 (cr == 0) || (cr == lf), "Should contain only Windows or Unix newlines: cr = " + cr + ", lf = " + lf);
586 }
587
588
589
590
591
592
593
594 public void validatePages() throws Exception {
595 new Xhtml5ValidatorTest().validateGeneratedPages();
596 }
597
598 protected static class Xhtml5ValidatorTest extends AbstractXmlValidator {
599
600
601
602
603
604
605 public void validateGeneratedPages() throws Exception {
606 setValidate(false);
607 try {
608 validateFiles();
609 } finally {
610 tearDown();
611 }
612 }
613
614 private static String[] getIncludes() {
615 return new String[] {"**/*.html"};
616 }
617
618
619 protected String addNamespaces(String content) {
620 return content;
621 }
622
623
624 protected EntityResolver getEntityResolver() {
625
626 return null;
627 }
628
629
630 protected Map<String, String> getTestDocuments() throws IOException {
631 Map<String, String> testDocs = new HashMap<>();
632
633 File dir = new File(getBasedir(), "target/output");
634
635 List<String> l =
636 FileUtils.getFileNames(dir, getIncludes()[0], FileUtils.getDefaultExcludesAsString(), true);
637
638 for (String file : l) {
639 file = file == null || file.isEmpty() ? file : file.replace("\\", "/");
640
641 Reader reader = ReaderFactory.newXmlReader(new File(file));
642 try {
643 testDocs.put(file, IOUtil.toString(reader));
644 } finally {
645 IOUtil.close(reader);
646 }
647 }
648
649 return testDocs;
650 }
651
652
653 @Override
654 protected boolean isFailErrorMessage(String message) {
655 return true;
656 }
657 }
658 }