1/*
2 * Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23package jaxp.library;
24
25import static org.testng.Assert.fail;
26
27import java.io.ByteArrayInputStream;
28import java.io.File;
29import java.io.IOException;
30import java.io.InputStream;
31import java.io.StringWriter;
32import java.nio.ByteBuffer;
33import java.nio.ByteOrder;
34import java.nio.charset.Charset;
35import java.nio.charset.StandardCharsets;
36import java.nio.charset.UnsupportedCharsetException;
37import java.nio.file.Files;
38import java.nio.file.Paths;
39import java.security.Permission;
40import java.util.ArrayList;
41import java.util.HashMap;
42import java.util.List;
43import java.util.Map;
44import java.util.Optional;
45import java.util.concurrent.Callable;
46import java.util.concurrent.ConcurrentHashMap;
47import java.util.function.Supplier;
48import java.util.regex.Pattern;
49import java.util.stream.Collectors;
50
51import javax.xml.parsers.DocumentBuilder;
52import javax.xml.parsers.DocumentBuilderFactory;
53import javax.xml.parsers.ParserConfigurationException;
54import javax.xml.transform.Transformer;
55import javax.xml.transform.TransformerException;
56import javax.xml.transform.TransformerFactory;
57import javax.xml.transform.dom.DOMSource;
58import javax.xml.transform.stream.StreamResult;
59
60import org.w3c.dom.Document;
61import org.w3c.dom.Node;
62import org.xml.sax.SAXException;
63
64/**
65 * This is an interface provide basic support for JAXP functional test.
66 */
67public class JAXPTestUtilities {
68    /**
69     * Prefix for error message.
70     */
71    public static final String ERROR_MSG_HEADER = "Unexcepted exception thrown:";
72
73    /**
74     * Prefix for error message on clean up block.
75     */
76    public static final String ERROR_MSG_CLEANUP = "Clean up failed on %s";
77
78    /**
79     * Force using slash as File separator as we always use cygwin to test in
80     * Windows platform.
81     */
82    public static final String FILE_SEP = "/";
83
84    /**
85     * A map storing every test's current test file pointer. File number should
86     * be incremental and it's a thread-safe reading on this file number.
87     */
88    private static final ConcurrentHashMap<Class<?>, Integer> currentFileNumber
89                = new ConcurrentHashMap<>();
90
91    /**
92     * BOM table for storing BOM header.
93     */
94    private final static Map<String, byte[]> bom = new HashMap<>();
95
96    /**
97     * Initialize all BOM headers.
98     */
99    static {
100        bom.put("UTF-8", new byte[]{(byte)0xEF, (byte) 0xBB, (byte) 0xBF});
101        bom.put("UTF-16BE", new byte[]{(byte)0xFE, (byte)0xFF});
102        bom.put("UTF-16LE", new byte[]{(byte)0xFF, (byte)0xFE});
103        bom.put("UTF-32BE", new byte[]{(byte)0x00, (byte)0x00, (byte)0xFE, (byte)0xFF});
104        bom.put("UTF-32LE", new byte[]{(byte)0xFF, (byte)0xFE, (byte)0x00, (byte)0x00});
105    }
106
107    /**
108     * Compare contents of golden file with test output file line by line.
109     * return true if they're identical.
110     * @param goldfile Golden output file name
111     * @param outputfile Test output file name
112     * @return true if two files are identical.
113     *         false if two files are not identical.
114     * @throws IOException if an I/O error occurs reading from the file or a
115     *         malformed or unmappable byte sequence is read.
116     */
117    public static boolean compareWithGold(String goldfile, String outputfile)
118            throws IOException {
119        return compareWithGold(goldfile, outputfile, StandardCharsets.UTF_8);
120    }
121
122    /**
123     * Compare contents of golden file with test output file line by line.
124     * return true if they're identical.
125     * @param goldfile Golden output file name.
126     * @param outputfile Test output file name.
127     * @param cs the charset to use for decoding.
128     * @return true if two files are identical.
129     *         false if two files are not identical.
130     * @throws IOException if an I/O error occurs reading from the file or a
131     *         malformed or unmappable byte sequence is read.
132     */
133    public static boolean compareWithGold(String goldfile, String outputfile,
134             Charset cs) throws IOException {
135        boolean isSame = Files.readAllLines(Paths.get(goldfile)).
136                equals(Files.readAllLines(Paths.get(outputfile), cs));
137        if (!isSame) {
138            System.err.println("Golden file " + goldfile + " :");
139            Files.readAllLines(Paths.get(goldfile)).forEach(System.err::println);
140            System.err.println("Output file " + outputfile + " :");
141            Files.readAllLines(Paths.get(outputfile), cs).forEach(System.err::println);
142        }
143        return isSame;
144    }
145
146    /**
147     * Compare contents of golden file with test output list line by line.
148     * return true if they're identical.
149     * @param goldfile Golden output file name.
150     * @param lines test output list.
151     * @return true if file's content is identical to given list.
152     *         false if file's content is not identical to given list.
153     * @throws IOException if an I/O error occurs reading from the file or a
154     *         malformed or unmappable byte sequence is read
155     */
156    public static boolean compareLinesWithGold(String goldfile, List<String> lines)
157            throws IOException {
158        return Files.readAllLines(Paths.get(goldfile)).equals(lines);
159    }
160
161    /**
162     * Compare contents of golden file with a test output string.
163     * return true if they're identical.
164     * @param goldfile Golden output file name.
165     * @param string test string.
166     * @return true if file's content is identical to given string.
167     *         false if file's content is not identical to given string.
168     * @throws IOException if an I/O error occurs reading from the file or a
169     *         malformed or unmappable byte sequence is read
170     */
171    public static boolean compareStringWithGold(String goldfile, String string)
172            throws IOException {
173        return Files.readAllLines(Paths.get(goldfile)).stream().collect(
174                Collectors.joining(System.getProperty("line.separator")))
175                .equals(string);
176    }
177
178    /**
179     * Compare contents of golden file with test output file by their document
180     * representation.
181     * Here we ignore the white space and comments. return true if they're
182     * lexical identical.
183     * @param goldfile Golden output file name.
184     * @param resultFile Test output file name.
185     * @return true if two file's document representation are identical.
186     *         false if two file's document representation are not identical.
187     * @throws javax.xml.parsers.ParserConfigurationException if the
188     *         implementation is not available or cannot be instantiated.
189     * @throws SAXException If any parse errors occur.
190     * @throws IOException if an I/O error occurs reading from the file or a
191     *         malformed or unmappable byte sequence is read .
192     */
193    public static boolean compareDocumentWithGold(String goldfile, String resultFile)
194            throws ParserConfigurationException, SAXException, IOException {
195        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
196        factory.setNamespaceAware(true);
197        factory.setCoalescing(true);
198        factory.setIgnoringElementContentWhitespace(true);
199        factory.setIgnoringComments(true);
200        DocumentBuilder db = factory.newDocumentBuilder();
201
202        Document goldD = db.parse(Paths.get(goldfile).toFile());
203        goldD.normalizeDocument();
204        Document resultD = db.parse(Paths.get(resultFile).toFile());
205        resultD.normalizeDocument();
206        return goldD.isEqualNode(resultD);
207    }
208
209    /**
210     * Compare contents of golden file with the serialization represent by given
211     * DOM node.
212     * Here we ignore the white space and comments. return true if they're
213     * lexical identical.
214     * @param goldfile Golden output file name.
215     * @param node A DOM node instance.
216     * @return true if file's content is identical to given node's serialization
217     *         represent.
218     *         false if file's content is not identical to given node's
219     *         serialization represent.
220     * @throws TransformerException If an unrecoverable error occurs during the
221     *         course of the transformation..
222     * @throws IOException if an I/O error occurs reading from the file or a
223     *         malformed or unmappable byte sequence is read .
224     */
225    public static boolean compareSerializeDOMWithGold(String goldfile, Node node)
226            throws TransformerException, IOException {
227        TransformerFactory factory = TransformerFactory.newInstance();
228        // Use identity transformer to serialize
229        Transformer identityTransformer = factory.newTransformer();
230        StringWriter sw = new StringWriter();
231        StreamResult streamResult = new StreamResult(sw);
232        DOMSource nodeSource = new DOMSource(node);
233        identityTransformer.transform(nodeSource, streamResult);
234        return compareStringWithGold(goldfile, sw.toString());
235    }
236
237    /**
238     * Convert stream to ByteArrayInputStream by given character set.
239     * @param charset target character set.
240     * @param file a file that contains no BOM head content.
241     * @return a ByteArrayInputStream contains BOM heads and bytes in original
242     *         stream
243     * @throws IOException I/O operation failed or unsupported character set.
244     */
245    public static InputStream bomStream(String charset, String file)
246            throws IOException {
247        String localCharset = charset;
248        if (charset.equals("UTF-16") || charset.equals("UTF-32")) {
249            localCharset
250                += ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN ? "BE" : "LE";
251        }
252        if (!bom.containsKey(localCharset))
253            throw new UnsupportedCharsetException("Charset:" + localCharset);
254
255        byte[] content = Files.readAllLines(Paths.get(file)).stream().
256                collect(Collectors.joining()).getBytes(localCharset);
257        byte[] head = bom.get(localCharset);
258        ByteBuffer bb = ByteBuffer.allocate(content.length + head.length);
259        bb.put(head);
260        bb.put(content);
261        return new ByteArrayInputStream(bb.array());
262    }
263
264   /**
265     * Worker method to detect common absolute URLs.
266     *
267     * @param s String path\filename or URL (or any, really)
268     * @return true if s starts with a common URI scheme (namely
269     * the ones found in the examples of RFC2396); false otherwise
270     */
271    protected static boolean isCommonURL(String s) {
272        if (null == s)
273            return false;
274        return Pattern.compile("^(file:|http:|ftp:|gopher:|mailto:|news:|telnet:)")
275                .matcher(s).matches();
276    }
277
278    /**
279     * Utility method to translate a String filename to URL.
280     *
281     * If the name starts with a common URI scheme (namely the ones
282     * found in the examples of RFC2396), then simply return the
283     * name as-is (the assumption is that it's already a URL).
284     * Otherwise we attempt (cheaply) to convert to a file:/ URL.
285     *
286     * @param filename local path/filename of a file.
287     * @return a file:/ URL if filename represent a file, the same string if
288     *         it appears to already be a URL.
289     */
290    public static String filenameToURL(String filename) {
291        return Paths.get(filename).toUri().toASCIIString();
292    }
293
294    /**
295     * Prints error message if an exception is thrown
296     * @param ex The exception is thrown by test.
297     */
298    public static void failUnexpected(Throwable ex) {
299        fail(ERROR_MSG_HEADER, ex);
300    }
301
302    /**
303     * Prints error message if an exception is thrown when clean up a file.
304     * @param ex The exception is thrown in cleaning up a file.
305     * @param name Cleaning up file name.
306     */
307    public static void failCleanup(IOException ex, String name) {
308        fail(String.format(ERROR_MSG_CLEANUP, name), ex);
309    }
310
311    /**
312     * Retrieve next test output file name. This method is a thread-safe method.
313     * @param clazz test class.
314     * @return next test output file name.
315     */
316    public static String getNextFile(Class<?> clazz) {
317        int nextNumber = currentFileNumber.contains(clazz)
318                ? currentFileNumber.get(clazz) + 1 : 1;
319        Integer i = currentFileNumber.putIfAbsent(clazz, nextNumber);
320        if (i != null) {
321            do {
322                nextNumber = currentFileNumber.get(clazz) + 1;
323            } while (!currentFileNumber.replace(clazz, nextNumber - 1, nextNumber));
324        }
325        return USER_DIR + clazz.getName() + nextNumber + ".out";
326    }
327
328    /**
329     * Acquire a full path string by given class name and relative path string.
330     * @param clazz Class name for the test.
331     * @param relativeDir relative path between java source file and expected
332     *        path.
333     * @return a string represents the full path of accessing path.
334     */
335    public static String getPathByClassName(Class<?> clazz, String relativeDir) {
336        String javaSourcePath = System.getProperty("test.src").replaceAll("\\" + File.separator, FILE_SEP);
337        String normalizedPath = Paths.get(javaSourcePath, relativeDir).normalize().
338                toAbsolutePath().toString();
339        return normalizedPath.replace("\\", FILE_SEP) + FILE_SEP;
340    }
341
342
343    /**
344     * Run the supplier with all permissions. This won't impact global policy.
345     *
346     * @param s
347     *            Supplier to run
348     */
349    public static <T> T runWithAllPerm(Supplier<T> s) {
350        Optional<JAXPPolicyManager> policyManager = Optional.ofNullable(JAXPPolicyManager
351                .getJAXPPolicyManager(false));
352        policyManager.ifPresent(manager -> manager.setAllowAll(true));
353        try {
354            return s.get();
355        } finally {
356            policyManager.ifPresent(manager -> manager.setAllowAll(false));
357        }
358    }
359
360    /**
361     * Run the supplier with all permissions. This won't impact global policy.
362     *
363     * @param s
364     *            Supplier to run
365     */
366    public static <T> T tryRunWithAllPerm(Callable<T> c) throws Exception {
367        Optional<JAXPPolicyManager> policyManager = Optional.ofNullable(JAXPPolicyManager
368                .getJAXPPolicyManager(false));
369        policyManager.ifPresent(manager -> manager.setAllowAll(true));
370        try {
371            return c.call();
372        } finally {
373            policyManager.ifPresent(manager -> manager.setAllowAll(false));
374        }
375    }
376
377    /**
378     * Run the Runnable with all permissions. This won't impact global policy.
379     *
380     * @param s
381     *            Supplier to run
382     */
383    public static void runWithAllPerm(Runnable r) {
384        Optional<JAXPPolicyManager> policyManager = Optional.ofNullable(JAXPPolicyManager
385                .getJAXPPolicyManager(false));
386        policyManager.ifPresent(manager -> manager.setAllowAll(true));
387        try {
388            r.run();
389        } finally {
390            policyManager.ifPresent(manager -> manager.setAllowAll(false));
391        }
392    }
393
394    /**
395     * Acquire a system property.
396     *
397     * @param name
398     *            System property name to be acquired.
399     * @return property value
400     */
401    public static String getSystemProperty(String name) {
402        return runWithAllPerm(() -> System.getProperty(name));
403    }
404
405    /**
406     * Set a system property by given system value.
407     *
408     * @param name
409     *            System property name to be set.
410     * @param value
411     *            System property value to be set.
412     */
413    public static void setSystemProperty(String name, String value) {
414        runWithAllPerm(() -> System.setProperty(name, value));
415    }
416
417    /**
418     * Clear a system property.
419     *
420     * @param name
421     *            System property name to be cleared.
422     */
423    public static void clearSystemProperty(String name) {
424        runWithAllPerm(() -> System.clearProperty(name));
425    }
426
427    /**
428     * Run the runnable with assigning temporary permissions. This won't impact
429     * global policy.
430     *
431     * @param r
432     *            Runnable to run
433     * @param ps
434     *            assigning permissions to add.
435     */
436    public static void runWithTmpPermission(Runnable r, Permission... ps) {
437        JAXPPolicyManager policyManager = JAXPPolicyManager.getJAXPPolicyManager(false);
438        List<Integer> tmpPermissionIndexes = new ArrayList<>();
439        if (policyManager != null) {
440            for (Permission p : ps)
441                tmpPermissionIndexes.add(policyManager.addTmpPermission(p));
442        }
443        try {
444            r.run();
445        } finally {
446            for (int index: tmpPermissionIndexes)
447                policyManager.removeTmpPermission(index);
448        }
449    }
450
451    /**
452     * Run the supplier with assigning temporary permissions. This won't impact
453     * global policy.
454     *
455     * @param s
456     *            Supplier to run
457     * @param ps
458     *            assigning permissions to add.
459     */
460    public static <T> T runWithTmpPermission(Supplier<T> s, Permission... ps) {
461        JAXPPolicyManager policyManager = JAXPPolicyManager.getJAXPPolicyManager(false);
462        List<Integer> tmpPermissionIndexes = new ArrayList<>();
463        if (policyManager != null) {
464            for (Permission p : ps)
465                tmpPermissionIndexes.add(policyManager.addTmpPermission(p));
466        }
467        try {
468            return s.get();
469        } finally {
470            for (int index: tmpPermissionIndexes)
471                policyManager.removeTmpPermission(index);
472        }
473    }
474
475    /**
476     * Run the RunnableWithException with assigning temporary permissions. This
477     * won't impact global policy.
478     *
479     * @param r
480     *            RunnableWithException to execute
481     * @param ps
482     *            assigning permissions to add.
483     */
484    public static void tryRunWithTmpPermission(RunnableWithException r, Permission... ps) throws Exception {
485        JAXPPolicyManager policyManager = JAXPPolicyManager.getJAXPPolicyManager(false);
486        List<Integer> tmpPermissionIndexes = new ArrayList<>();
487        if (policyManager != null) {
488            for (Permission p : ps)
489                tmpPermissionIndexes.add(policyManager.addTmpPermission(p));
490        }
491        try {
492            r.run();
493        } finally {
494            for (int index: tmpPermissionIndexes)
495                policyManager.removeTmpPermission(index);
496        }
497    }
498
499    @FunctionalInterface
500    public interface RunnableWithException {
501        void run() throws Exception;
502    }
503
504    /**
505     * Current test directory.
506     */
507    public static final String USER_DIR = getSystemProperty("user.dir") + FILE_SEP;;
508
509}
510