com.github.anba.es6draft.util.Resources.java Source code

Java tutorial

Introduction

Here is the source code for com.github.anba.es6draft.util.Resources.java

Source

/**
 * Copyright (c) 2012-2016 Andr Bargull
 * Alle Rechte vorbehalten / All Rights Reserved.  Use is subject to license terms.
 *
 * <https://github.com/anba/es6draft>
 */
package com.github.anba.es6draft.util;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.configuration.CompositeConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.SystemConfiguration;
import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
import org.apache.commons.io.input.BOMInputStream;
import org.apache.commons.lang.text.StrLookup;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Resource and configuration loading utility
 */
public final class Resources {
    private static final boolean REMOVE_DISABLED_TESTS = false;
    private static final boolean DISABLE_ALL_TESTS = false;
    private static final boolean RUN_DISABLED_TESTS = false;

    private Resources() {
    }

    /**
     * {@link ConfigurationInterpolator} which reports an error for missing variables.
     */
    private static final ConfigurationInterpolator MISSING_VAR = new ConfigurationInterpolator() {
        private final StrLookup errorLookup = new StrLookup() {
            @Override
            public String lookup(String key) {
                String msg = String.format("Variable '%s' is not set", key);
                throw new NoSuchElementException(msg);
            }
        };

        @Override
        public String lookup(String var) {
            return errorLookup.lookup(var);
        }

        @Override
        protected StrLookup fetchLookupForPrefix(String prefix) {
            return errorLookup;
        }

        @Override
        protected StrLookup fetchNoPrefixLookup() {
            return errorLookup;
        }
    };

    /**
     * Loads the configuration file.
     */
    public static Configuration loadConfiguration(Class<?> clazz) {
        TestConfiguration config = clazz.getAnnotation(TestConfiguration.class);
        String file = config.file();
        String name = config.name();
        try {
            PropertiesConfiguration properties = new PropertiesConfiguration();
            // entries are mandatory unless an explicit default value was given
            properties.setThrowExceptionOnMissing(true);
            properties.getInterpolator().setParentInterpolator(MISSING_VAR);
            properties.load(resource(file), "UTF-8");

            Configuration configuration = new CompositeConfiguration(
                    Arrays.asList(new SystemConfiguration(), properties));
            return configuration.subset(name);
        } catch (ConfigurationException | IOException e) {
            throw new RuntimeException(e);
        } catch (NoSuchElementException e) {
            throw e;
        }
    }

    /**
     * Loads the named resource through {@link Class#getResourceAsStream(String)} if the uri starts with "resource:",
     * otherwise loads the resource with {@link Files#newInputStream(Path, java.nio.file.OpenOption...)}.
     */
    public static InputStream resource(String uri) throws IOException {
        return resource(uri, Paths.get(""));
    }

    /**
     * Loads the named resource through {@link Class#getResourceAsStream(String)} if the uri starts with "resource:",
     * otherwise loads the resource with {@link Files#newInputStream(Path, java.nio.file.OpenOption...)}.
     */
    public static InputStream resource(String uri, Path basedir) throws IOException {
        final String RESOURCE = "resource:";
        if (uri.startsWith(RESOURCE)) {
            String name = uri.substring(RESOURCE.length());
            InputStream res = Resources.class.getResourceAsStream(name);
            if (res == null) {
                throw new IOException("resource not found: " + name);
            }
            return res;
        } else {
            return Files.newInputStream(basedir.resolve(Paths.get(uri)));
        }
    }

    /**
     * Returns the resource path if available.
     */
    public static Path resourcePath(String uri, Path basedir) {
        final String RESOURCE = "resource:";
        if (uri.startsWith(RESOURCE)) {
            return null;
        } else {
            return basedir.resolve(Paths.get(uri)).toAbsolutePath();
        }
    }

    /**
     * Returns {@code true} if the test suite is enabled.
     */
    public static boolean isEnabled(Configuration configuration) {
        return !configuration.getBoolean("skip", false);
    }

    /**
     * Returns the test suite's base path.
     */
    public static Path getTestSuitePath(Configuration configuration) {
        try {
            String testSuite = configuration.getString("");
            return Paths.get(testSuite).toAbsolutePath();
        } catch (InvalidPathException | NoSuchElementException e) {
            System.err.println(e.getMessage());
            return null;
        }
    }

    /**
     * Load the test files based on the supplied {@link Configuration}.
     */
    public static List<TestInfo> loadTests(Configuration config) throws IOException {
        return loadTests(config, TestInfo::new);
    }

    /**
     * Load the test files based on the supplied {@link Configuration}.
     */
    public static <TEST extends TestInfo> List<TEST> loadTests(Configuration config,
            BiFunction<Path, Path, TEST> fn) throws IOException {
        if (!isEnabled(config)) {
            return emptyList();
        }
        Path basedir = getTestSuitePath(config);
        if (basedir == null) {
            return emptyList();
        }
        return loadTests(config, mapper(fn, basedir), basedir);
    }

    /**
     * Load the test files based on the supplied {@link Configuration}.
     */
    public static <TEST extends TestInfo> List<TEST> loadTests(Configuration config,
            Function<Path, BiFunction<Path, Iterator<String>, TEST>> fn) throws IOException {
        if (!isEnabled(config)) {
            return emptyList();
        }
        Path basedir = getTestSuitePath(config);
        if (basedir == null) {
            return emptyList();
        }
        return loadTests(config, mapper(fn.apply(basedir)), basedir);
    }

    /**
     * Recursively searches for js-file test cases in {@code basedir} and its sub-directories.
     */
    private static <TEST extends TestInfo> List<TEST> loadTests(Configuration config, Function<Path, TEST> mapper,
            Path basedir) throws IOException {
        FilterFileVisitor<Path> ffv = new FilterFileVisitor<Path>(basedir, new FileMatcher(config));
        CollectorFileVisitor<Path, TEST> cfv = new CollectorFileVisitor<>(ffv, mapper);
        Files.walkFileTree(basedir, cfv);
        List<TEST> tests = cfv.getResult();
        filterTests(tests, basedir, config);
        if (REMOVE_DISABLED_TESTS) {
            tests = removeDisabled(tests);
        }
        return tests;
    }

    private static <TEST extends TestInfo> List<TEST> removeDisabled(List<TEST> tests) {
        ArrayList<TEST> actual = new ArrayList<>();
        for (TEST test : tests) {
            if (test.isEnabled()) {
                actual.add(test);
            }
        }
        return actual;
    }

    /**
     * Filter the initially collected test cases.
     */
    private static void filterTests(List<? extends TestInfo> tests, Path basedir, Configuration config)
            throws IOException {
        if (DISABLE_ALL_TESTS) {
            for (TestInfo test : tests) {
                test.setEnabled(false);
            }
        }
        if (config.containsKey("exclude.list")) {
            InputStream exclusionList = Resources.resource(config.getString("exclude.list"), basedir);
            filterTests(tests, exclusionList, config);
        }
        if (config.containsKey("exclude.xml")) {
            Set<String> excludes = readExcludeXMLs(config.getList("exclude.xml", emptyList()), basedir);
            filterTests(tests, excludes);
        }
    }

    /**
     * Filter the initially collected test cases.
     */
    private static void filterTests(List<? extends TestInfo> tests, InputStream resource, Configuration config)
            throws IOException {
        // list->map
        Map<Path, TestInfo> map = new LinkedHashMap<>();
        for (TestInfo test : tests) {
            map.put(test.getScript(), test);
        }
        // disable tests
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource, StandardCharsets.UTF_8))) {
            FileMatcher fileMatcher = null;
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (line.startsWith("#") || line.isEmpty()) {
                    continue;
                }
                TestInfo test = map.get(Paths.get(line));
                if (test == null) {
                    if (fileMatcher == null) {
                        fileMatcher = new FileMatcher(config);
                    }
                    if (matchesIncludesOrInvalidEntry(fileMatcher, line)) {
                        System.err.printf("detected stale entry '%s'\n", line);
                    }
                    continue;
                }
                test.setEnabled(RUN_DISABLED_TESTS);
            }
        }
    }

    private static boolean matchesIncludesOrInvalidEntry(FileMatcher fileMatcher, String entry) {
        Path file;
        try {
            file = Paths.get(entry);
        } catch (InvalidPathException e) {
            return true;
        }
        return fileMatcher.matches(file);
    }

    /**
     * Filter the initially collected test cases.
     */
    private static void filterTests(List<? extends TestInfo> tests, Set<String> excludes) {
        Pattern pattern = Pattern.compile("(.+?)(?:\\.([^.]*)$|$)");
        for (TestInfo test : tests) {
            String filename = test.getScript().getFileName().toString();
            Matcher matcher = pattern.matcher(filename);
            if (!matcher.matches()) {
                assert false : "regexp failure";
                continue;
            }
            String testname = matcher.group(1);
            if (excludes.contains(testname)) {
                test.setEnabled(RUN_DISABLED_TESTS);
                continue;
            }
        }
    }

    /**
     * Reads all exlusion xml-files from the configuration.
     */
    private static Set<String> readExcludeXMLs(List<?> values, Path basedir) throws IOException {
        Set<String> exclude = new HashSet<>();
        for (String s : nonEmpty(values)) {
            try (InputStream res = Resources.resource(s, basedir)) {
                exclude.addAll(readExcludeXML(res));
            }
        }
        return exclude;
    }

    private static Iterable<String> nonEmpty(List<?> c) {
        return () -> c.stream().filter(x -> (x != null && !x.toString().isEmpty())).map(Object::toString)
                .iterator();
    }

    /**
     * Load the exclusion xml-list for invalid test cases from {@link InputStream}
     */
    private static Set<String> readExcludeXML(InputStream is) throws IOException {
        Set<String> exclude = new HashSet<>();
        Reader reader = new InputStreamReader(new BOMInputStream(is), StandardCharsets.UTF_8);
        NodeList ns = xml(reader).getDocumentElement().getElementsByTagName("test");
        for (int i = 0, len = ns.getLength(); i < len; ++i) {
            exclude.add(((Element) ns.item(i)).getAttribute("id"));
        }
        return exclude;
    }

    /**
     * Reads the xml-structure from {@link Reader} and returns the corresponding {@link Document}.
     */
    public static Document xml(Reader xml) throws IOException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

        // turn off any validation or namespace features
        factory.setNamespaceAware(false);
        factory.setValidating(false);
        List<String> features = Arrays.asList("http://xml.org/sax/features/namespaces",
                "http://xml.org/sax/features/validation",
                "http://apache.org/xml/features/nonvalidating/load-dtd-grammar",
                "http://apache.org/xml/features/nonvalidating/load-external-dtd");
        for (String feature : features) {
            try {
                factory.setFeature(feature, false);
            } catch (ParserConfigurationException e) {
                // ignore invalid feature names
            }
        }

        try {
            return factory.newDocumentBuilder().parse(new InputSource(xml));
        } catch (ParserConfigurationException | SAXException e) {
            throw new IOException(e);
        }
    }

    private static <T extends TestInfo> Function<Path, T> mapper(BiFunction<Path, Path, T> fn, Path basedir) {
        return file -> fn.apply(basedir, file);
    }

    private static <T extends TestInfo> Function<Path, T> mapper(BiFunction<Path, Iterator<String>, T> fn) {
        return file -> {
            try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
                return fn.apply(file, new LineIterator(reader));
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        };
    }

    private static final class LineIterator implements Iterator<String> {
        private final BufferedReader reader;
        private String line = null;

        LineIterator(BufferedReader reader) {
            this.reader = reader;
        }

        @Override
        public boolean hasNext() {
            if (line == null) {
                try {
                    line = reader.readLine();
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            }
            return line != null;
        }

        @Override
        public String next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            String line = this.line;
            this.line = null;
            return line;
        }
    }

    private static final class CollectorFileVisitor<PATH extends Path, T> extends SimpleFileVisitor<PATH> {
        private final FileVisitor<PATH> visitor;
        private Function<PATH, T> mapper;
        private ArrayList<T> result = new ArrayList<>();

        CollectorFileVisitor(FileVisitor<PATH> visitor, Function<PATH, T> mapper) {
            this.visitor = visitor;
            this.mapper = mapper;
        }

        public ArrayList<T> getResult() {
            return result;
        }

        @Override
        public FileVisitResult preVisitDirectory(PATH dir, BasicFileAttributes attrs) throws IOException {
            return visitor.preVisitDirectory(dir, attrs);
        }

        @Override
        public FileVisitResult visitFile(PATH file, BasicFileAttributes attrs) throws IOException {
            if (visitor.visitFile(file, attrs) == FileVisitResult.CONTINUE) {
                try {
                    result.add(mapper.apply(file));
                } catch (UncheckedIOException e) {
                    throw e.getCause();
                }
            }
            return FileVisitResult.CONTINUE;
        }
    }

    private static final class FilterFileVisitor<PATH extends Path> extends SimpleFileVisitor<PATH> {
        private final Path basedir;
        private final FileMatcher fileMatcher;

        FilterFileVisitor(Path basedir, FileMatcher fileMatcher) {
            this.basedir = basedir;
            this.fileMatcher = fileMatcher;
        }

        @Override
        public FileVisitResult preVisitDirectory(PATH path, BasicFileAttributes attrs) throws IOException {
            Path dir = basedir.relativize(path);
            if (fileMatcher.matchesDirectory(dir)) {
                return FileVisitResult.CONTINUE;
            }
            return FileVisitResult.SKIP_SUBTREE;
        }

        @Override
        public FileVisitResult visitFile(PATH path, BasicFileAttributes attrs) throws IOException {
            Path file = basedir.relativize(path);
            if (attrs.isRegularFile() && attrs.size() != 0L && fileMatcher.matches(file)) {
                return FileVisitResult.CONTINUE;
            }
            return FileVisitResult.TERMINATE;
        }
    }

    private static final class FileMatcher {
        private final List<PathMatcher> includeMatchers;
        private final Set<String> includeDirs;
        private final Set<String> includeFiles;
        private final List<PathMatcher> excludeMatchers;
        private final Set<String> excludeDirs;
        private final Set<String> excludeFiles;
        private final List<String> includePrefixList;
        private final List<String> excludePrefixList;

        FileMatcher(Configuration config) {
            List<Object> include = config.getList("include", asList("**/*.js", "*.js"));
            List<Object> includeDirs = config.getList("include.dirs", emptyList());
            List<Object> includeFiles = config.getList("include.files", emptyList());
            List<Object> exclude = config.getList("exclude", emptyList());
            List<Object> excludeDirs = config.getList("exclude.dirs", emptyList());
            List<Object> excludeFiles = config.getList("exclude.files", emptyList());

            this.includeMatchers = matchers(toStrings(include));
            this.includeDirs = toStrings(includeDirs).collect(Collectors.toSet());
            this.includeFiles = toStrings(includeFiles).collect(Collectors.toSet());
            this.excludeMatchers = matchers(toStrings(exclude));
            this.excludeDirs = toStrings(excludeDirs).collect(Collectors.toSet());
            this.excludeFiles = toStrings(excludeFiles).collect(Collectors.toSet());
            this.includePrefixList = toPrefixList(toStrings(include));
            this.excludePrefixList = toPrefixList(toStrings(exclude));
        }

        private static final Stream<String> toStrings(List<?> values) {
            return values.stream().filter(v -> (v != null && !v.toString().isEmpty())).map(Object::toString);
        }

        private static List<String> toPrefixList(Stream<String> patterns) {
            Pattern dirPattern = Pattern.compile("^(?:glob:)?((?:[^/*?,{}\\[\\]\\\\]+/)+).*$");
            List<String> list = patterns.map(dirPattern::matcher).map(m -> m.matches() ? m.group(1) : null)
                    .collect(Collectors.toList());
            return list.stream().allMatch(Objects::nonNull) ? list : Collections.emptyList();
        }

        public boolean matchesDirectory(Path directory) {
            if (!matches(excludeDirs, directory.getFileName())
                    && !prefixMatches(excludePrefixList, directory.toString())
                    && matches(includePrefixList, directory.toString())) {
                return true;
            }
            return false;
        }

        public boolean matches(Path file) {
            if (!matches(excludeMatchers, file) && matches(includeMatchers, file)) {
                if (!matches(excludeFiles, file.getFileName())
                        && (includeDirs.isEmpty() || matches(includeDirs, file.getParent()))
                        && (includeFiles.isEmpty() || matches(includeFiles, file.getFileName()))) {
                    return true;
                }
            }
            return false;
        }

        private static List<PathMatcher> matchers(Stream<String> patterns) {
            return patterns.map(pattern -> {
                if (!(pattern.startsWith("glob:") || pattern.startsWith("regex:"))) {
                    pattern = "glob:" + pattern;
                }
                return pattern;
            }).map(pattern -> FileSystems.getDefault().getPathMatcher(pattern)).collect(Collectors.toList());
        }

        private static boolean matches(Set<String> names, Path path) {
            for (Path p : path) {
                if (names.contains(p.getFileName().toString())) {
                    return true;
                }
            }
            return false;
        }

        private static boolean matches(List<PathMatcher> matchers, Path path) {
            for (PathMatcher matcher : matchers) {
                if (matcher.matches(path)) {
                    return true;
                }
            }
            return false;
        }

        private static boolean matches(List<String> list, String string) {
            if (string.isEmpty() || list.isEmpty()) {
                return true;
            }
            String search = string.replace(File.separatorChar, '/') + "/";
            for (String item : list) {
                if (item.regionMatches(0, search, 0, Math.min(item.length(), search.length()))) {
                    return true;
                }
            }
            return false;
        }

        private static boolean prefixMatches(List<String> prefixList, String string) {
            String search = string.replace(File.separatorChar, '/') + "/";
            for (String prefix : prefixList) {
                if (search.startsWith(prefix)) {
                    return true;
                }
            }
            return false;
        }
    }
}