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  
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   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
92   * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
93   */
94  @PlexusTest
95  public class DefaultSiteRendererTest {
96      /**
97       * All output produced by this test will go here.
98       */
99      private static final String OUTPUT = "target/output";
100 
101     /**
102      * The site renderer used to produce output.
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         // only create once, as otherwise Windows might have issues overwriting/deleting it while MS Defender is
114         // scanning it
115         // as multiple test are using it
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      * @throws java.lang.Exception if something goes wrong.
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                     // skip root
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      * @throws java.lang.Exception if something goes wrong.
161      */
162     @AfterEach
163     protected void tearDown() throws Exception {
164         container.release(siteRenderer);
165     }
166 
167     /**
168      * @throws Exception if something goes wrong.
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      * @throws Exception if something goes wrong.
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      * @throws Exception if something goes wrong.
232      */
233     @Test
234     void render() throws Exception {
235         File outputDirectory = getTestFile(OUTPUT);
236         // Safety
237         org.apache.commons.io.FileUtils.deleteDirectory(outputDirectory);
238 
239         // ----------------------------------------------------------------------
240         // Render the site from src/test/resources/site to OUTPUT
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         // Verify specific pages
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         // Validate the rendering pages
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         // Safety
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          * We need to add doxiaSiteRendererVersion manually because version property from pom.properties
330          * is not available at test time in some cases.
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          * We need to add doxiaSiteRendererVersion manually because version property from pom.properties
365          * is not available at test time in some cases.
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      * @throws Exception if something goes wrong.
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      * @throws Exception if something goes wrong.
475      */
476     public void verifyNestedItemsPage() throws Exception {
477         NestedItemsVerifier verifier = new NestedItemsVerifier();
478         verifier.verify("target/output/nestedItems.html");
479     }
480 
481     /**
482      * @throws Exception if something goes wrong.
483      */
484     public void verifyMultipleBlock() throws Exception {
485         MultipleBlockVerifier verifier = new MultipleBlockVerifier();
486         verifier.verify("target/output/multipleblock.html");
487     }
488 
489     /**
490      * @throws Exception if something goes wrong.
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      * @throws Exception if something goes wrong.
509      */
510     public void verifyEntitiesPage() throws Exception {
511         EntitiesVerifier verifier = new EntitiesVerifier();
512         verifier.verify("target/output/entityTest.html");
513     }
514 
515     /**
516      * @throws Exception if something goes wrong.
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      * @throws Exception if something goes wrong.
530      */
531     public void verifyFaqPage() throws Exception {
532         FaqVerifier verifier = new FaqVerifier();
533         verifier.verify("target/output/faq.html");
534     }
535 
536     /**
537      * @throws Exception if something goes wrong.
538      */
539     public void verifyAttributes() throws Exception {
540         AttributesVerifier verifier = new AttributesVerifier();
541         verifier.verify("target/output/attributes.html");
542     }
543 
544     /**
545      * @throws Exception if something goes wrong.
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      * @throws Exception if something goes wrong.
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      * @throws Exception if something goes wrong.
566      */
567     public void verifyNewlines() throws Exception {
568         /* apt */
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         /* fml */
573         checkNewlines(FileUtils.fileRead(getTestFile("target/output/faq.html"), "ISO-8859-1"));
574         /* xdoc */
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      * Validate the generated pages.
590      *
591      * @throws Exception if something goes wrong.
592      * @since 1.1.1
593      */
594     public void validatePages() throws Exception {
595         new Xhtml5ValidatorTest().validateGeneratedPages();
596     }
597 
598     protected static class Xhtml5ValidatorTest extends AbstractXmlValidator {
599 
600         /**
601          * Validate the generated documents.
602          *
603          * @throws Exception
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         /** {@inheritDoc} */
619         protected String addNamespaces(String content) {
620             return content;
621         }
622 
623         /** {@inheritDoc} */
624         protected EntityResolver getEntityResolver() {
625             /* HTML5 restricts use of entities to XML only */
626             return null;
627         }
628 
629         /** {@inheritDoc} */
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         /** {@inheritDoc} */
653         @Override
654         protected boolean isFailErrorMessage(String message) {
655             return true;
656         }
657     }
658 }