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.ByteArrayInputStream;
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.Reader;
30  import java.io.StringWriter;
31  import java.nio.charset.StandardCharsets;
32  import java.util.Collections;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Map;
37  import java.util.jar.JarOutputStream;
38  import java.util.zip.ZipEntry;
39  
40  import org.apache.commons.io.IOUtils;
41  import org.apache.maven.artifact.Artifact;
42  import org.apache.maven.artifact.DefaultArtifact;
43  import org.apache.maven.artifact.versioning.VersionRange;
44  import org.apache.maven.doxia.Doxia;
45  import org.apache.maven.doxia.parser.ParseException;
46  import org.apache.maven.doxia.sink.Sink;
47  import org.apache.maven.doxia.site.SiteModel;
48  import org.apache.maven.doxia.site.io.xpp3.SiteXpp3Reader;
49  import org.apache.maven.doxia.siterenderer.SiteRenderingContext.SiteDirectory;
50  import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
51  import org.apache.maven.doxia.xsd.AbstractXmlValidator;
52  import org.codehaus.plexus.PlexusContainer;
53  import org.codehaus.plexus.testing.PlexusTest;
54  import org.codehaus.plexus.util.FileUtils;
55  import org.codehaus.plexus.util.IOUtil;
56  import org.codehaus.plexus.util.ReaderFactory;
57  import org.codehaus.plexus.util.ReflectionUtils;
58  import org.codehaus.plexus.util.StringUtils;
59  import org.junit.jupiter.api.AfterEach;
60  import org.junit.jupiter.api.BeforeEach;
61  import org.junit.jupiter.api.Test;
62  import org.mockito.Mockito;
63  import org.xml.sax.EntityResolver;
64  
65  import static org.codehaus.plexus.testing.PlexusExtension.getBasedir;
66  import static org.codehaus.plexus.testing.PlexusExtension.getTestFile;
67  import static org.junit.jupiter.api.Assertions.assertEquals;
68  import static org.junit.jupiter.api.Assertions.assertFalse;
69  import static org.junit.jupiter.api.Assertions.assertNotNull;
70  import static org.junit.jupiter.api.Assertions.assertTrue;
71  import static org.junit.jupiter.api.Assertions.fail;
72  import static org.mockito.Mockito.*;
73  
74  /**
75   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
76   * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
77   */
78  @PlexusTest
79  public class DefaultSiteRendererTest {
80      /**
81       * All output produced by this test will go here.
82       */
83      private static final String OUTPUT = "target/output";
84  
85      /**
86       * The site renderer used to produce output.
87       */
88      private SiteRenderer siteRenderer;
89  
90      @Inject
91      private PlexusContainer container;
92  
93      private File skinJar = new File(getBasedir(), "target/test-classes/skin.jar");
94  
95      private File minimalSkinJar = new File(getBasedir(), "target/test-classes/minimal-skin.jar");
96  
97      /**
98       * @throws java.lang.Exception if something goes wrong.
99       */
100     @BeforeEach
101     protected void setUp() throws Exception {
102         siteRenderer = (SiteRenderer) container.lookup(SiteRenderer.class);
103 
104         InputStream skinIS = getClass().getResourceAsStream("velocity-toolmanager.vm");
105         JarOutputStream jarOS = new JarOutputStream(new FileOutputStream(skinJar));
106         try {
107             jarOS.putNextEntry(new ZipEntry("META-INF/maven/site.vm"));
108             IOUtil.copy(skinIS, jarOS);
109             jarOS.closeEntry();
110         } finally {
111             IOUtil.close(skinIS);
112             IOUtil.close(jarOS);
113         }
114 
115         skinIS = new ByteArrayInputStream(
116                 "<main id=\"contentBox\">$bodyContent</main>".getBytes(StandardCharsets.UTF_8));
117         jarOS = new JarOutputStream(new FileOutputStream(minimalSkinJar));
118         try {
119             jarOS.putNextEntry(new ZipEntry("META-INF/maven/site.vm"));
120             IOUtil.copy(skinIS, jarOS);
121             jarOS.closeEntry();
122         } finally {
123             IOUtil.close(skinIS);
124             IOUtil.close(jarOS);
125         }
126     }
127 
128     /**
129      * @throws java.lang.Exception if something goes wrong.
130      */
131     @AfterEach
132     protected void tearDown() throws Exception {
133         container.release(siteRenderer);
134     }
135 
136     /**
137      * @throws Exception if something goes wrong.
138      */
139     @Test
140     public void testRenderExceptionMessageWhenLineNumberIsNotAvailable() throws Exception {
141         final File testBasedir = getTestFile("src/test/resources/site/xdoc");
142         final String testDocument = "head.xml";
143         final String exceptionMessage = "parse error occurred";
144 
145         Doxia doxiaInstance = container.lookup(Doxia.class);
146         Doxia doxiaSpy = spy(doxiaInstance);
147         Mockito.doThrow(new ParseException(exceptionMessage))
148                 .when(doxiaSpy)
149                 .parse(Mockito.<Reader>any(), Mockito.anyString(), Mockito.<Sink>any(), Mockito.nullable(String.class));
150         SiteRenderer siteRenderer = container.lookup(SiteRenderer.class);
151         ReflectionUtils.setVariableValueInObject(siteRenderer, "doxia", doxiaSpy);
152 
153         DocumentRenderingContext docRenderingContext =
154                 new DocumentRenderingContext(testBasedir, "", testDocument, "xdoc", "", false);
155 
156         try {
157             siteRenderer.renderDocument(null, docRenderingContext, new SiteRenderingContext());
158             fail("should fail with exception");
159         } catch (RendererException e) {
160             assertEquals(
161                     String.format(
162                             "Error parsing '%s%s%s'", testBasedir.getAbsolutePath(), File.separator, testDocument),
163                     e.getMessage());
164         }
165     }
166 
167     /**
168      * @throws Exception if something goes wrong.
169      */
170     @Test
171     public void testRenderExceptionMessageWhenLineNumberIsAvailable() throws Exception {
172         final File testBasedir = getTestFile("src/test/resources/site/xdoc");
173         final String testDocumentName = "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, 42, 36))
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 =
185                 new DocumentRenderingContext(testBasedir, "", testDocumentName, "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', line 42",
194                             testBasedir.getAbsolutePath(), File.separator, testDocumentName),
195                     e.getMessage());
196         }
197     }
198 
199     /**
200      * @throws Exception if something goes wrong.
201      */
202     @Test
203     public void testRender() throws Exception {
204         // Safety
205         FileUtils.deleteDirectory(getTestFile(OUTPUT));
206 
207         // ----------------------------------------------------------------------
208         // Render the site from src/test/resources/site to OUTPUT
209         // ----------------------------------------------------------------------
210         SiteModel siteModel =
211                 new SiteXpp3Reader().read(new FileInputStream(getTestFile("src/test/resources/site/site.xml")));
212 
213         SiteRenderingContext ctxt = getSiteRenderingContext(siteModel, "src/test/resources/site", false);
214         ctxt.setRootDirectory(getTestFile(""));
215         siteRenderer.render(siteRenderer.locateDocumentFiles(ctxt, true).values(), ctxt, getTestFile(OUTPUT));
216 
217         ctxt = getSiteRenderingContext(siteModel, "src/test/resources/site-validate", true);
218         ctxt.setRootDirectory(getTestFile(""));
219         siteRenderer.render(siteRenderer.locateDocumentFiles(ctxt, true).values(), ctxt, getTestFile(OUTPUT));
220 
221         // ----------------------------------------------------------------------
222         // Verify specific pages
223         // ----------------------------------------------------------------------
224         verifyCdcPage();
225         verifyNestedItemsPage();
226         verifyMultipleBlock();
227         verifyMacro();
228         verifyEntitiesPage();
229         verifyJavascriptPage();
230         verifyFaqPage();
231         verifyAttributes();
232         verifyApt();
233         verifyExtensionInFilename();
234         verifyNewlines();
235 
236         // ----------------------------------------------------------------------
237         // Validate the rendering pages
238         // ----------------------------------------------------------------------
239         validatePages();
240     }
241 
242     @Test
243     public void testExternalReport() throws Exception {
244         DocumentRenderer docRenderer = mock(DocumentRenderer.class);
245         when(docRenderer.isExternalReport()).thenReturn(true);
246         when(docRenderer.getOutputName()).thenReturn("external/index");
247         when(docRenderer.getRenderingContext())
248                 .thenReturn(new DocumentRenderingContext(new File(""), "index.html", "generator:external"));
249 
250         SiteRenderingContext context = new SiteRenderingContext();
251 
252         siteRenderer.render(Collections.singletonList(docRenderer), context, new File("target/output"));
253 
254         verify(docRenderer).renderDocument(isNull(), eq(siteRenderer), eq(context));
255     }
256 
257     @Test
258     public void testVelocityToolManager() throws Exception {
259         StringWriter writer = new StringWriter();
260 
261         SiteRenderingContext siteRenderingContext = new SiteRenderingContext();
262         siteRenderingContext.setSiteModel(new SiteModel());
263 
264         Map<String, Object> attributes = new HashMap<>();
265 
266         /*
267          * We need to add doxiaSiteRendererVersion manually because version property from pom.properties
268          * is not available at test time in some cases.
269          */
270         attributes.put("doxiaSiteRendererVersion", "1.7-bogus");
271 
272         siteRenderingContext.setTemplateProperties(attributes);
273 
274         siteRenderingContext.setTemplateName("org/apache/maven/doxia/siterenderer/velocity-toolmanager.vm");
275         DocumentRenderingContext docRenderingContext =
276                 new DocumentRenderingContext(new File(""), "document.html", "generator");
277         SiteRendererSink sink = new SiteRendererSink(docRenderingContext);
278         siteRenderer.mergeDocumentIntoSite(writer, sink, siteRenderingContext);
279 
280         String renderResult = writer.toString();
281         String expectedResult = IOUtils.toString(
282                 getClass().getResourceAsStream("velocity-toolmanager.expected.txt"), StandardCharsets.UTF_8);
283         expectedResult = StringUtils.unifyLineSeparators(expectedResult);
284         assertEquals(expectedResult, renderResult);
285     }
286 
287     @Test
288     public void testVelocityToolManagerForSkin() throws Exception {
289         StringWriter writer = new StringWriter();
290 
291         File skinFile = skinJar;
292 
293         Map<String, Object> attributes = new HashMap<>();
294 
295         /*
296          * We need to add doxiaSiteRendererVersion manually because version property from pom.properties
297          * is not available at test time in some cases.
298          */
299         attributes.put("doxiaSiteRendererVersion", "1.7-bogus");
300 
301         Artifact skin = new DefaultArtifact(
302                 "org.group", "artifact", VersionRange.createFromVersion("1.1"), null, "jar", "", null);
303         skin.setFile(skinFile);
304         SiteRenderingContext siteRenderingContext =
305                 siteRenderer.createContextForSkin(skin, attributes, new SiteModel(), "defaultitle", Locale.ROOT);
306         DocumentRenderingContext context = new DocumentRenderingContext(new File(""), "document.html", "generator");
307         SiteRendererSink sink = new SiteRendererSink(context);
308         siteRenderer.mergeDocumentIntoSite(writer, sink, siteRenderingContext);
309         String renderResult = writer.toString();
310         String expectedResult = StringUtils.unifyLineSeparators(IOUtils.toString(
311                 getClass().getResourceAsStream("velocity-toolmanager.expected.txt"), StandardCharsets.UTF_8));
312         assertEquals(expectedResult, renderResult);
313     }
314 
315     @Test
316     public void testMatchVersion() throws Exception {
317         DefaultSiteRenderer r = (DefaultSiteRenderer) siteRenderer;
318         assertTrue(r.matchVersion("1.7", "1.7"));
319         assertFalse(r.matchVersion("1.7", "1.8"));
320     }
321 
322     private SiteRenderingContext getSiteRenderingContext(SiteModel siteModel, String siteDir, boolean validate)
323             throws RendererException, IOException {
324         File skinFile = minimalSkinJar;
325 
326         final Map<String, String> attributes = new HashMap<>();
327         attributes.put("outputEncoding", "UTF-8");
328 
329         Artifact skin = new DefaultArtifact(
330                 "org.group", "artifact", VersionRange.createFromVersion("1.1"), null, "jar", "", null);
331         skin.setFile(skinFile);
332         SiteRenderingContext siteRenderingContext =
333                 siteRenderer.createContextForSkin(skin, attributes, siteModel, "defaultTitle", Locale.ROOT);
334         siteRenderingContext.addSiteDirectory(new SiteDirectory(getTestFile(siteDir), true));
335         siteRenderingContext.setValidate(validate);
336 
337         return siteRenderingContext;
338     }
339 
340     /**
341      * @throws Exception if something goes wrong.
342      */
343     public void verifyCdcPage() throws Exception {
344         File nestedItems = getTestFile("target/output/cdc.html");
345         assertNotNull(nestedItems);
346         assertTrue(nestedItems.exists());
347     }
348 
349     /**
350      * @throws Exception if something goes wrong.
351      */
352     public void verifyNestedItemsPage() throws Exception {
353         NestedItemsVerifier verifier = new NestedItemsVerifier();
354         verifier.verify("target/output/nestedItems.html");
355     }
356 
357     /**
358      * @throws Exception if something goes wrong.
359      */
360     public void verifyMultipleBlock() throws Exception {
361         MultipleBlockVerifier verifier = new MultipleBlockVerifier();
362         verifier.verify("target/output/multipleblock.html");
363     }
364 
365     /**
366      * @throws Exception if something goes wrong.
367      */
368     public void verifyMacro() throws Exception {
369         File macro = getTestFile("target/output/macro.html");
370         assertNotNull(macro);
371         assertTrue(macro.exists());
372 
373         Reader reader = null;
374         try {
375             reader = ReaderFactory.newXmlReader(macro);
376             String content = IOUtil.toString(reader);
377             assertEquals(content.indexOf("</macro>"), -1);
378         } finally {
379             IOUtil.close(reader);
380         }
381     }
382 
383     /**
384      * @throws Exception if something goes wrong.
385      */
386     public void verifyEntitiesPage() throws Exception {
387         EntitiesVerifier verifier = new EntitiesVerifier();
388         verifier.verify("target/output/entityTest.html");
389     }
390 
391     /**
392      * @throws Exception if something goes wrong.
393      */
394     public void verifyJavascriptPage() throws Exception {
395         JavascriptVerifier verifier = new JavascriptVerifier();
396         verifier.verify("target/output/javascript.html");
397     }
398 
399     /**
400      * @throws Exception if something goes wrong.
401      */
402     public void verifyFaqPage() throws Exception {
403         FaqVerifier verifier = new FaqVerifier();
404         verifier.verify("target/output/faq.html");
405     }
406 
407     /**
408      * @throws Exception if something goes wrong.
409      */
410     public void verifyAttributes() throws Exception {
411         AttributesVerifier verifier = new AttributesVerifier();
412         verifier.verify("target/output/attributes.html");
413     }
414 
415     /**
416      * @throws Exception if something goes wrong.
417      */
418     public void verifyApt() throws Exception {
419         AbstractVerifier verifier = new AptVerifier();
420         verifier.verify("target/output/apt.html");
421 
422         verifier = new CommentsVerifier();
423         verifier.verify("target/output/apt.html");
424     }
425 
426     /**
427      * @throws Exception if something goes wrong.
428      */
429     public void verifyExtensionInFilename() throws Exception {
430         File output = getTestFile("target/output/extension.apt.not.at.end.html");
431         assertNotNull(output);
432         assertTrue(output.exists());
433     }
434 
435     /**
436      * @throws Exception if something goes wrong.
437      */
438     public void verifyNewlines() throws Exception {
439         /* apt */
440         checkNewlines(FileUtils.fileRead(getTestFile("target/output/apt.html"), "ISO-8859-1"));
441         checkNewlines(FileUtils.fileRead(getTestFile("target/output/cdc.html"), "ISO-8859-1"));
442         checkNewlines(FileUtils.fileRead(getTestFile("target/output/interpolation.html"), "ISO-8859-1"));
443         /* fml */
444         checkNewlines(FileUtils.fileRead(getTestFile("target/output/faq.html"), "ISO-8859-1"));
445         /* xdoc */
446         checkNewlines(FileUtils.fileRead(getTestFile("target/output/attributes.html"), "ISO-8859-1"));
447         checkNewlines(FileUtils.fileRead(getTestFile("target/output/javascript.html"), "ISO-8859-1"));
448         checkNewlines(FileUtils.fileRead(getTestFile("target/output/head.html"), "ISO-8859-1"));
449         checkNewlines(FileUtils.fileRead(getTestFile("target/output/macro.html"), "ISO-8859-1"));
450     }
451 
452     private void checkNewlines(String content) {
453         int cr = StringUtils.countMatches(content, "\r");
454         int lf = StringUtils.countMatches(content, "\n");
455         assertTrue(
456                 (cr == 0) || (cr == lf), "Should contain only Windows or Unix newlines: cr = " + cr + ", lf = " + lf);
457     }
458 
459     /**
460      * Validate the generated pages.
461      *
462      * @throws Exception if something goes wrong.
463      * @since 1.1.1
464      */
465     public void validatePages() throws Exception {
466         new Xhtml5ValidatorTest().validateGeneratedPages();
467     }
468 
469     protected static class Xhtml5ValidatorTest extends AbstractXmlValidator {
470 
471         /**
472          * Validate the generated documents.
473          *
474          * @throws Exception
475          */
476         public void validateGeneratedPages() throws Exception {
477             setValidate(false);
478             try {
479                 testValidateFiles();
480             } finally {
481                 tearDown();
482             }
483         }
484 
485         private static String[] getIncludes() {
486             return new String[] {"**/*.html"};
487         }
488 
489         /** {@inheritDoc} */
490         protected String addNamespaces(String content) {
491             return content;
492         }
493 
494         /** {@inheritDoc} */
495         protected EntityResolver getEntityResolver() {
496             /* HTML5 restricts use of entities to XML only */
497             return null;
498         }
499 
500         /** {@inheritDoc} */
501         protected Map<String, String> getTestDocuments() throws IOException {
502             Map<String, String> testDocs = new HashMap<>();
503 
504             File dir = new File(getBasedir(), "target/output");
505 
506             List<String> l =
507                     FileUtils.getFileNames(dir, getIncludes()[0], FileUtils.getDefaultExcludesAsString(), true);
508 
509             for (String file : l) {
510                 file = StringUtils.replace(file, "\\", "/");
511 
512                 Reader reader = ReaderFactory.newXmlReader(new File(file));
513                 try {
514                     testDocs.put(file, IOUtil.toString(reader));
515                 } finally {
516                     IOUtil.close(reader);
517                 }
518             }
519 
520             return testDocs;
521         }
522 
523         /** {@inheritDoc} */
524         @Override
525         protected boolean isFailErrorMessage(String message) {
526             return true;
527         }
528     }
529 }