// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.webapp; import java.io.File; import java.net.URI; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.function.Predicate; import org.eclipse.jetty.util.ArrayTernaryTrie; import org.eclipse.jetty.util.IncludeExcludeSet; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; /** * Classpath classes list performs sequential pattern matching of a class name * against an internal array of classpath pattern entries. * A class pattern is a string of one of the forms:<ul> * <li>'org.package.SomeClass' will match a specific class * <li>'org.package.' will match a specific package hierarchy * <li>'org.package.SomeClass$NestedClass ' will match a nested class exactly otherwise. * Nested classes are matched by their containing class. (eg. org.example.MyClass * matches org.example.MyClass$AnyNestedClass) * <li>'file:///some/location/' - A file system directory from which * the class was loaded * <li>'file:///some/location.jar' - The URI of a jar file from which * the class was loaded * <li>Any of the above patterns preceeded by '-' will exclude rather than include the match. * </ul> * When class is initialized from a classpath pattern string, entries * in this string should be separated by ':' (semicolon) or ',' (comma). */ public class ClasspathPattern extends AbstractSet<String> { private static final Logger LOG = Log.getLogger(ClasspathPattern.class); enum Type { PACKAGE, CLASSNAME, LOCATION } private static class Entry { private final String _pattern; private final String _name; private final boolean _inclusive; private final Type _type; Entry(String pattern) { _pattern=pattern; _inclusive = !pattern.startsWith("-"); _name = _inclusive ? pattern : pattern.substring(1).trim(); _type = _name.startsWith("file:")?Type.LOCATION:(_name.endsWith(".")?Type.PACKAGE:Type.CLASSNAME); } Entry(String name, boolean include) { _pattern=include?name:("-"+name); _inclusive = include; _name = name; _type = _name.startsWith("file:")?Type.LOCATION:(_name.endsWith(".")?Type.PACKAGE:Type.CLASSNAME); } public String getPattern() { return _pattern; } public boolean isPackage() { return _type==Type.PACKAGE; } public boolean isClassName() { return _type==Type.CLASSNAME; } public boolean isLocation() { return _type==Type.LOCATION; } public String getName() { return _name; } @Override public String toString() { return _pattern; } @Override public int hashCode() { return _pattern.hashCode(); } @Override public boolean equals(Object o) { return (o instanceof Entry) && _pattern.equals(((Entry)o)._pattern); } public boolean isInclusive() { return _inclusive; } } public static class ByPackage extends AbstractSet<Entry> implements Predicate<String> { private final ArrayTernaryTrie.Growing<Entry> _entries = new ArrayTernaryTrie.Growing<>(false,512,512); @Override public boolean test(String name) { return _entries.getBest(name)!=null; } @Override public Iterator<Entry> iterator() { return _entries.keySet().stream().map(_entries::get).iterator(); } @Override public int size() { return _entries.size(); } @Override public boolean isEmpty() { return _entries.isEmpty(); } @Override public boolean add(Entry entry) { String name = entry.getName(); if (entry.isClassName()) name+="$"; else if (entry.isLocation()) throw new IllegalArgumentException(entry.toString()); else if (".".equals(name)) name=""; if (_entries.get(name)!=null) return false; return _entries.put(name,entry); } @Override public boolean remove(Object entry) { if (!(entry instanceof Entry)) return false; return _entries.remove(((Entry)entry).getName())!=null; } @Override public void clear() { _entries.clear(); } } @SuppressWarnings("serial") public static class ByName extends HashSet<Entry> implements Predicate<String> { private final Map<String,Entry> _entries = new HashMap<>(); @Override public boolean test(String name) { return _entries.containsKey(name); } @Override public Iterator<Entry> iterator() { return _entries.values().iterator(); } @Override public int size() { return _entries.size(); } @Override public boolean add(Entry entry) { if (!entry.isClassName()) throw new IllegalArgumentException(entry.toString()); return _entries.put(entry.getName(),entry)==null; } @Override public boolean remove(Object entry) { if (!(entry instanceof Entry)) return false; return _entries.remove(((Entry)entry).getName())!=null; } } public static class ByPackageOrName extends AbstractSet<Entry> implements Predicate<String> { private final ByName _byName = new ByName(); private final ByPackage _byPackage = new ByPackage(); @Override public boolean test(String name) { return _byPackage.test(name) || _byName.test(name) ; } @Override public Iterator<Entry> iterator() { // by package contains all entries (classes are also $ packages). return _byPackage.iterator(); } @Override public int size() { return _byPackage.size(); } @Override public boolean add(Entry e) { if (e.isLocation()) throw new IllegalArgumentException(); if (e.isPackage()) return _byPackage.add(e); // Add class name to packages also as classes act // as packages for nested classes. boolean added = _byPackage.add(e); added = _byName.add(e) || added; return added; } @Override public boolean remove(Object o) { if (!(o instanceof Entry)) return false; boolean removed = _byPackage.remove(o); if (!((Entry)o).isPackage()) removed = _byName.remove(o) || removed; return removed; } @Override public void clear() { _byPackage.clear(); _byName.clear(); } } @SuppressWarnings("serial") public static class ByLocation extends HashSet<File> implements Predicate<Path> { @Override public boolean test(Path path) { for (File file: this) { if (file.isDirectory()) { if (path.startsWith(file.toPath())) { return true; } } else { if (path.equals(file.toPath())) { return true; } } } return false; } } Map<String,Entry> _entries = new HashMap<>(); IncludeExcludeSet<Entry,String> _patterns = new IncludeExcludeSet<>(ByPackageOrName.class); IncludeExcludeSet<File,Path> _locations = new IncludeExcludeSet<>(ByLocation.class); public ClasspathPattern() { } public ClasspathPattern(ClasspathPattern patterns) { if (patterns!=null) setAll(patterns.getPatterns()); } public ClasspathPattern(String... patterns) { if (patterns!=null && patterns.length>0) setAll(patterns); } public ClasspathPattern(String pattern) { add(pattern); } public boolean include(String name) { if (name==null) return false; return add(new Entry(name,true)); } public boolean include(String... name) { boolean added = false; for (String n:name) if (n!=null) added = add(new Entry(n,true)) || added; return added; } public boolean exclude(String name) { if (name==null) return false; return add(new Entry(name,false)); } public boolean exclude(String... name) { boolean added = false; for (String n:name) if (n!=null) added = add(new Entry(n,false)) || added; return added; } @Override public boolean add(String pattern) { if (pattern==null) return false; return add(new Entry(pattern)); } public boolean add(String... pattern) { boolean added = false; for (String p:pattern) if (p!=null) added = add(new Entry(p)) || added; return added; } protected boolean add(Entry entry) { if (_entries.containsKey(entry.getPattern())) return false; _entries.put(entry.getPattern(),entry); if (entry.isLocation()) { try { File file = Resource.newResource(entry.getName()).getFile().getAbsoluteFile().getCanonicalFile(); if (entry.isInclusive()) _locations.include(file); else _locations.exclude(file); } catch (Exception e) { throw new IllegalArgumentException(e); } } else { if (entry.isInclusive()) _patterns.include(entry); else _patterns.exclude(entry); } return true; } @Override public boolean remove(Object o) { if (!(o instanceof String)) return false; String pattern = (String)o; Entry entry = _entries.remove(pattern); if (entry==null) return false; List<Entry> saved = new ArrayList<>(_entries.values()); clear(); for (Entry e:saved) add(e); return true; } @Override public void clear() { _entries.clear(); _patterns.clear(); _locations.clear(); } @Override public Iterator<String> iterator() { return _entries.keySet().iterator(); } @Override public int size() { return _entries.size(); } /** * Initialize the matcher by parsing each classpath pattern in an array * * @param classes array of classpath patterns */ private void setAll(String[] classes) { _entries.clear(); addAll(classes); } /** * @param classes array of classpath patterns */ private void addAll(String[] classes) { if (classes!=null) addAll(Arrays.asList(classes)); } /** * @return array of classpath patterns */ public String[] getPatterns() { return toArray(new String[_entries.size()]); } /** * @return array of inclusive classpath patterns */ public String[] getInclusions() { return _entries.values().stream().filter(Entry::isInclusive).map(Entry::getName).toArray(String[]::new); } /** * @return array of excluded classpath patterns (without '-' prefix) */ public String[] getExclusions() { return _entries.values().stream().filter(e->!e.isInclusive()).map(Entry::getName).toArray(String[]::new); } /** * Match the class name against the pattern * * @param name name of the class to match * @return true if class matches the pattern */ public boolean match(String name) { return _patterns.test(name); } /** * Match the class name against the pattern * * @param clazz A class to try to match * @return true if class matches the pattern */ public boolean match(Class<?> clazz) { try { Boolean byName = _patterns.isIncludedAndNotExcluded(clazz.getName()); if (Boolean.FALSE.equals(byName)) return byName; // Already excluded so no need to check location. URI location = TypeUtil.getLocationOfClass(clazz); Boolean byLocation = location == null ? null : _locations.isIncludedAndNotExcluded(Paths.get(location)); if (LOG.isDebugEnabled()) LOG.debug("match {} from {} byName={} byLocation={} in {}",clazz,location,byName,byLocation,this); // Combine the tri-state match of both IncludeExclude Sets boolean included = byName==Boolean.TRUE || byLocation==Boolean.TRUE || (byName==null && !_patterns.hasIncludes() && byLocation==null && !_locations.hasIncludes()); boolean excluded = byName==Boolean.FALSE || byLocation==Boolean.FALSE; return included && !excluded; } catch (Exception e) { LOG.warn(e); } return false; } public boolean match(String name, URL url) { // Strip class suffix for name matching if (name.endsWith(".class")) name=name.substring(0,name.length()-6); // Treat path elements as packages for name matching name=name.replace("/","."); Boolean byName = _patterns.isIncludedAndNotExcluded(name); if (Boolean.FALSE.equals(byName)) return byName; // Already excluded so no need to check location. // Try to find a file path for location matching Boolean byLocation = null; try { URI jarUri = URIUtil.getJarSource(url.toURI()); if ("file".equalsIgnoreCase(jarUri.getScheme())) { File file = new File(jarUri); byLocation = _locations.isIncludedAndNotExcluded(file.toPath()); } } catch(Exception e) { LOG.ignore(e); } // Combine the tri-state match of both IncludeExclude Sets boolean included = byName==Boolean.TRUE || byLocation==Boolean.TRUE || (byName==null && !_patterns.hasIncludes() && byLocation==null && !_locations.hasIncludes()); boolean excluded = byName==Boolean.FALSE || byLocation==Boolean.FALSE; return included && !excluded; } }