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