1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 /* $Id$ */
19 
20 package org.apache.fop.complexscripts.layout;
21 
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.HashMap;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Map;
31 
32 import javax.xml.parsers.ParserConfigurationException;
33 import javax.xml.transform.Source;
34 import javax.xml.transform.Transformer;
35 import javax.xml.transform.TransformerException;
36 import javax.xml.transform.TransformerFactory;
37 import javax.xml.transform.dom.DOMResult;
38 import javax.xml.transform.dom.DOMSource;
39 import javax.xml.transform.sax.SAXResult;
40 import javax.xml.transform.sax.TransformerHandler;
41 
42 import org.junit.BeforeClass;
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 import org.junit.runners.Parameterized;
46 import org.junit.runners.Parameterized.Parameters;
47 import org.w3c.dom.Document;
48 import org.w3c.dom.Element;
49 import org.w3c.dom.NamedNodeMap;
50 import org.w3c.dom.Node;
51 import org.w3c.dom.NodeList;
52 import org.xml.sax.ContentHandler;
53 import org.xml.sax.SAXException;
54 
55 import static org.junit.Assert.assertEquals;
56 import static org.junit.Assert.assertTrue;
57 import static org.junit.Assert.fail;
58 
59 import org.apache.commons.io.FileUtils;
60 import org.apache.commons.io.filefilter.AndFileFilter;
61 import org.apache.commons.io.filefilter.IOFileFilter;
62 import org.apache.commons.io.filefilter.NameFileFilter;
63 import org.apache.commons.io.filefilter.PrefixFileFilter;
64 import org.apache.commons.io.filefilter.SuffixFileFilter;
65 import org.apache.commons.io.filefilter.TrueFileFilter;
66 
67 import org.apache.fop.DebugHelper;
68 import org.apache.fop.apps.EnvironmentProfile;
69 import org.apache.fop.apps.EnvironmentalProfileFactory;
70 import org.apache.fop.apps.FOUserAgent;
71 import org.apache.fop.apps.Fop;
72 import org.apache.fop.apps.FopConfBuilder;
73 import org.apache.fop.apps.FopConfParser;
74 import org.apache.fop.apps.FopFactory;
75 import org.apache.fop.apps.FopFactoryBuilder;
76 import org.apache.fop.apps.FormattingResults;
77 import org.apache.fop.apps.MimeConstants;
78 import org.apache.fop.apps.PDFRendererConfBuilder;
79 import org.apache.fop.apps.io.ResourceResolverFactory;
80 import org.apache.fop.area.AreaTreeModel;
81 import org.apache.fop.area.AreaTreeParser;
82 import org.apache.fop.area.RenderPagesModel;
83 import org.apache.fop.events.Event;
84 import org.apache.fop.events.EventListener;
85 import org.apache.fop.events.model.EventSeverity;
86 import org.apache.fop.fonts.FontInfo;
87 import org.apache.fop.intermediate.IFTester;
88 import org.apache.fop.intermediate.TestAssistant;
89 import org.apache.fop.layoutengine.ElementListCollector;
90 import org.apache.fop.layoutengine.LayoutEngineCheck;
91 import org.apache.fop.layoutengine.LayoutEngineChecksFactory;
92 import org.apache.fop.layoutengine.LayoutResult;
93 import org.apache.fop.layoutengine.TestFilesConfiguration;
94 import org.apache.fop.layoutmgr.ElementListObserver;
95 import org.apache.fop.render.Renderer;
96 import org.apache.fop.render.intermediate.IFContext;
97 import org.apache.fop.render.intermediate.IFRenderer;
98 import org.apache.fop.render.intermediate.IFSerializer;
99 import org.apache.fop.render.xml.XMLRenderer;
100 import org.apache.fop.util.ConsoleEventListenerForTests;
101 import org.apache.fop.util.DelegatingContentHandler;
102 
103 // CSOFF: LineLengthCheck
104 
105 /**
106  * Test complex script layout (end-to-end) functionality.
107  */
108 @RunWith(Parameterized.class)
109 public class ComplexScriptsLayoutTestCase {
110 
111     private static final boolean DEBUG = false;
112     private static final String AREA_TREE_OUTPUT_DIRECTORY = "build/test-results/complexscripts";
113     private static File areaTreeOutputDir;
114 
115     private TestAssistant testAssistant = new TestAssistant();
116     private LayoutEngineChecksFactory layoutEngineChecksFactory = new LayoutEngineChecksFactory();
117     private TestFilesConfiguration testConfig;
118     private File testFile;
119     private IFTester ifTester;
120     private TransformerFactory tfactory = TransformerFactory.newInstance();
121 
ComplexScriptsLayoutTestCase(TestFilesConfiguration testConfig, File testFile)122     public ComplexScriptsLayoutTestCase(TestFilesConfiguration testConfig, File testFile) {
123         this.testConfig = testConfig;
124         this.testFile = testFile;
125         this.ifTester = new IFTester(tfactory, areaTreeOutputDir);
126     }
127 
128     @Parameters
getParameters()129     public static Collection<Object[]> getParameters() throws IOException {
130         return getTestFiles();
131     }
132 
133     @BeforeClass
makeDirAndRegisterDebugHelper()134     public static void makeDirAndRegisterDebugHelper() throws IOException {
135         DebugHelper.registerStandardElementListObservers();
136         areaTreeOutputDir = new File(AREA_TREE_OUTPUT_DIRECTORY);
137         if (!areaTreeOutputDir.mkdirs() && !areaTreeOutputDir.exists()) {
138             throw new IOException("Failed to create the AT output directory at " + AREA_TREE_OUTPUT_DIRECTORY);
139         }
140     }
141 
142     @Test
runTest()143     public void runTest() throws TransformerException, SAXException, IOException, ParserConfigurationException {
144         DOMResult domres = new DOMResult();
145         ElementListCollector elCollector = new ElementListCollector();
146         ElementListObserver.addObserver(elCollector);
147         Fop fop;
148         FopFactory effFactory;
149         EventsChecker eventsChecker = new EventsChecker(new ConsoleEventListenerForTests(testFile.getName(), EventSeverity.WARN));
150         try {
151             Document testDoc = testAssistant.loadTestCase(testFile);
152             effFactory = getFopFactory(testConfig, testDoc);
153             // Setup Transformer to convert the testcase XML to XSL-FO
154             Transformer transformer = testAssistant.getTestcase2FOStylesheet().newTransformer();
155             Source src = new DOMSource(testDoc);
156             // Setup Transformer to convert the area tree to a DOM
157             TransformerHandler athandler;
158             athandler = testAssistant.getTransformerFactory().newTransformerHandler();
159             athandler.setResult(domres);
160             // Setup FOP for area tree rendering
161             FOUserAgent ua = effFactory.newFOUserAgent();
162             ua.getEventBroadcaster().addEventListener(eventsChecker);
163             XMLRenderer atrenderer = new XMLRenderer(ua);
164             Renderer targetRenderer = ua.getRendererFactory().createRenderer(ua, MimeConstants.MIME_PDF);
165             atrenderer.mimicRenderer(targetRenderer);
166             atrenderer.setContentHandler(athandler);
167             ua.setRendererOverride(atrenderer);
168             fop = effFactory.newFop(ua);
169             SAXResult fores = new SAXResult(fop.getDefaultHandler());
170             transformer.transform(src, fores);
171         } finally {
172             ElementListObserver.removeObserver(elCollector);
173         }
174         Document doc = (Document)domres.getNode();
175         if (areaTreeOutputDir != null) {
176             testAssistant.saveDOM(doc, new File(areaTreeOutputDir, testFile.getName() + ".at.xml"));
177         }
178         FormattingResults results = fop.getResults();
179         LayoutResult result = new LayoutResult(doc, elCollector, results);
180         checkAll(effFactory, testFile, result, eventsChecker);
181     }
182 
getFopFactory(TestFilesConfiguration testConfig, Document testDoc)183     private FopFactory getFopFactory(TestFilesConfiguration testConfig, Document testDoc)  throws SAXException, IOException {
184         EnvironmentProfile profile = EnvironmentalProfileFactory.createRestrictedIO(
185             testConfig.getTestDirectory().getParentFile().toURI(),
186             ResourceResolverFactory.createDefaultResourceResolver());
187         InputStream confStream =
188             new FopConfBuilder().setStrictValidation(true)
189                                 .setFontBaseURI("test/resources/fonts/ttf/")
190                                 .startRendererConfig(PDFRendererConfBuilder.class)
191                                   .startFontsConfig()
192                                     .startFont(null, "DejaVuLGCSerif.ttf")
193                                       .addTriplet("DejaVu LGC Serif", "normal", "normal")
194                                     .endFont()
195                                   .endFontConfig()
196                                 .endRendererConfig().build();
197         FopFactoryBuilder builder =
198             new FopConfParser(confStream, new File(".").toURI(), profile).getFopFactoryBuilder();
199         // builder.setStrictFOValidation(isStrictValidation(testDoc));
200         // builder.getFontManager().setBase14KerningEnabled(isBase14KerningEnabled(testDoc));
201         return builder.build();
202     }
203 
checkAll(FopFactory fopFactory, File testFile, LayoutResult result, EventsChecker eventsChecker)204     private void checkAll(FopFactory fopFactory, File testFile, LayoutResult result, EventsChecker eventsChecker) throws TransformerException {
205         Element testRoot = testAssistant.getTestRoot(testFile);
206         NodeList nodes;
207         nodes = testRoot.getElementsByTagName("at-checks");
208         if (nodes.getLength() > 0) {
209             Element atChecks = (Element)nodes.item(0);
210             doATChecks(atChecks, result);
211         }
212         nodes = testRoot.getElementsByTagName("if-checks");
213         if (nodes.getLength() > 0) {
214             Element ifChecks = (Element)nodes.item(0);
215             Document ifDocument = createIF(fopFactory, testFile, result.getAreaTree());
216             ifTester.doIFChecks(testFile.getName(), ifChecks, ifDocument);
217         }
218         nodes = testRoot.getElementsByTagName("event-checks");
219         if (nodes.getLength() > 0) {
220             Element eventChecks = (Element) nodes.item(0);
221             doEventChecks(eventChecks, eventsChecker);
222         }
223         eventsChecker.emitUncheckedEvents();
224     }
225 
createIF(FopFactory fopFactory, File testFile, Document areaTreeXML)226     private Document createIF(FopFactory fopFactory, File testFile, Document areaTreeXML) throws TransformerException {
227         try {
228             FOUserAgent ua = fopFactory.newFOUserAgent();
229             ua.getEventBroadcaster().addEventListener(new ConsoleEventListenerForTests(testFile.getName(), EventSeverity.WARN));
230             IFRenderer ifRenderer = new IFRenderer(ua);
231             IFSerializer serializer = new IFSerializer(new IFContext(ua));
232             DOMResult result = new DOMResult();
233             serializer.setResult(result);
234             ifRenderer.setDocumentHandler(serializer);
235             ua.setRendererOverride(ifRenderer);
236             FontInfo fontInfo = new FontInfo();
237             //Construct the AreaTreeModel that will received the individual pages
238             final AreaTreeModel treeModel = new RenderPagesModel(ua, null, fontInfo, null);
239             //Iterate over all intermediate files
240             AreaTreeParser parser = new AreaTreeParser();
241             ContentHandler handler = parser.getContentHandler(treeModel, ua);
242             DelegatingContentHandler proxy = new DelegatingContentHandler() {
243                 public void endDocument() throws SAXException {
244                     super.endDocument();
245                     treeModel.endDocument();
246                 }
247             };
248             proxy.setDelegateContentHandler(handler);
249             Transformer transformer = tfactory.newTransformer();
250             transformer.transform(new DOMSource(areaTreeXML), new SAXResult(proxy));
251             return (Document)result.getNode();
252         } catch (Exception e) {
253             throw new TransformerException("Error while generating intermediate format file: " + e.getMessage(), e);
254         }
255     }
256 
doATChecks(Element checksRoot, LayoutResult result)257     private void doATChecks(Element checksRoot, LayoutResult result) {
258         List<LayoutEngineCheck> checks = layoutEngineChecksFactory.createCheckList(checksRoot);
259         if (checks.size() == 0) {
260             throw new RuntimeException("No available area tree check");
261         }
262         for (LayoutEngineCheck check : checks) {
263             check.check(result);
264         }
265     }
266 
doEventChecks(Element eventChecks, EventsChecker eventsChecker)267     private void doEventChecks(Element eventChecks, EventsChecker eventsChecker) {
268         NodeList events = eventChecks.getElementsByTagName("event");
269         for (int i = 0; i < events.getLength(); i++) {
270             Element event = (Element) events.item(i);
271             NamedNodeMap attributes = event.getAttributes();
272             Map<String, String> params = new HashMap<String, String>();
273             String key = null;
274             for (int j = 0; j < attributes.getLength(); j++) {
275                 Node attribute = attributes.item(j);
276                 String name = attribute.getNodeName();
277                 String value = attribute.getNodeValue();
278                 if ("key".equals(name)) {
279                     key = value;
280                 } else {
281                     params.put(name, value);
282                 }
283             }
284             if (key == null) {
285                 throw new RuntimeException("An event element must have a \"key\" attribute");
286             }
287             eventsChecker.checkEvent(key, params);
288         }
289     }
290 
getTestFiles(TestFilesConfiguration testConfig)291     private static Collection<Object[]> getTestFiles(TestFilesConfiguration testConfig) {
292         File mainDir = testConfig.getTestDirectory();
293         IOFileFilter filter;
294         String single = testConfig.getSingleTest();
295         String startsWith = testConfig.getStartsWith();
296         if (single != null) {
297             filter = new NameFileFilter(single);
298         } else if (startsWith != null) {
299             filter = new PrefixFileFilter(startsWith);
300             filter = new AndFileFilter(filter, new SuffixFileFilter(testConfig.getFileSuffix()));
301         } else {
302             filter = new SuffixFileFilter(testConfig.getFileSuffix());
303         }
304         String testset = testConfig.getTestSet();
305         Collection<File> files = FileUtils.listFiles(new File(mainDir, testset), filter, TrueFileFilter.INSTANCE);
306         if (testConfig.hasPrivateTests()) {
307             Collection<File> privateFiles =
308                 FileUtils.listFiles(new File(mainDir, "private-testcases"), filter, TrueFileFilter.INSTANCE);
309             files.addAll(privateFiles);
310         }
311         Collection<Object[]> parametersForJUnit4 = new ArrayList<Object[]>();
312         int index = 0;
313         for (File f : files) {
314             parametersForJUnit4.add(new Object[] { testConfig, f });
315             if (DEBUG) {
316                 System.out.println(String.format("%3d %s", index++, f));
317             }
318         }
319         return parametersForJUnit4;
320     }
321 
getTestFiles()322     private static Collection<Object[]> getTestFiles() {
323         String testSet = System.getProperty("fop.complexscripts.testset");
324         testSet = (testSet != null ? testSet : "standard") + "-testcases";
325         return getTestFiles(testSet);
326     }
327 
getTestFiles(String testSetName)328     private static Collection<Object[]> getTestFiles(String testSetName) {
329         TestFilesConfiguration.Builder builder = new TestFilesConfiguration.Builder();
330         builder.testDir("test/resources/complexscripts/layout")
331                .singleProperty("fop.complexscripts.single")
332                .startsWithProperty("fop.complexscripts.starts-with")
333                .suffix(".xml")
334                .testSet(testSetName)
335                .privateTestsProperty("fop.complexscripts.private");
336         return getTestFiles(builder.build());
337     }
338 
339     private static class EventsChecker implements EventListener {
340 
341         private final List<Event> events = new ArrayList<Event>();
342         private final EventListener defaultListener;
343 
EventsChecker(EventListener fallbackListener)344         public EventsChecker(EventListener fallbackListener) {
345             this.defaultListener = fallbackListener;
346         }
347 
processEvent(Event event)348         public void processEvent(Event event) {
349             events.add(event);
350         }
351 
checkEvent(String expectedKey, Map<String, String> expectedParams)352         public void checkEvent(String expectedKey, Map<String, String> expectedParams) {
353             boolean eventFound = false;
354             for (Iterator<Event> iter = events.iterator(); !eventFound && iter.hasNext();) {
355                 Event event = iter.next();
356                 if (event.getEventKey().equals(expectedKey)) {
357                     eventFound = true;
358                     iter.remove();
359                     checkParameters(event, expectedParams);
360                 }
361             }
362             if (!eventFound) {
363                 fail("Event did not occur but was expected to: " + expectedKey + expectedParams);
364             }
365         }
366 
checkParameters(Event event, Map<String, String> expectedParams)367         private void checkParameters(Event event, Map<String, String> expectedParams) {
368             Map<String, Object> actualParams = event.getParams();
369             for (Map.Entry<String, String> expectedParam : expectedParams.entrySet()) {
370                 assertTrue("Event \"" + event.getEventKey()
371                         + "\" is missing parameter \"" + expectedParam.getKey() + '"',
372                         actualParams.containsKey(expectedParam.getKey()));
373                 assertEquals("Event \"" + event.getEventKey()
374                         + "\" has wrong value for parameter \"" + expectedParam.getKey() + "\";",
375                         actualParams.get(expectedParam.getKey()).toString(),
376                         expectedParam.getValue());
377             }
378         }
379 
emitUncheckedEvents()380         public void emitUncheckedEvents() {
381             for (Event event : events) {
382                 defaultListener.processEvent(event);
383             }
384         }
385     }
386 
387 }
388