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