org.eclipse.jetty.util.ClassMatcher Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jetty-util Show documentation
Show all versions of jetty-util Show documentation
Utility classes for Jetty
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.util;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
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.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
* A matcher for classes based on package and/or location and/or module/
*
* Performs pattern matching of a class against a set of pattern entries.
* A class pattern is a string of one of the forms:
* - 'org.package.SomeClass' will match a specific class
*
- 'org.package.' will match a specific package hierarchy
*
- '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)
*
- 'file:///some/location/' - A file system directory from which
* the class was loaded
*
- 'file:///some/location.jar' - The URI of a jar file from which
* the class was loaded
*
- 'jrt:/modulename' - A Java9 module name
* - Any of the above patterns preceded by '-' will exclude rather than include the match.
*
* When class is initialized from a classpath pattern string, entries
* in this string should be separated by ':' (semicolon) or ',' (comma).
*/
public class ClassMatcher extends AbstractSet
{
public static class Entry
{
private final String _pattern;
private final String _name;
private final boolean _inclusive;
protected Entry(String name, boolean inclusive)
{
_name = name;
_inclusive = inclusive;
_pattern = inclusive ? _name : ("-" + _name);
}
public String getPattern()
{
return _pattern;
}
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;
}
}
private static class PackageEntry extends Entry
{
protected PackageEntry(String name, boolean inclusive)
{
super(name, inclusive);
}
}
private static class ClassEntry extends Entry
{
protected ClassEntry(String name, boolean inclusive)
{
super(name, inclusive);
}
}
private static class LocationEntry extends Entry
{
private final Path _path;
protected LocationEntry(String name, boolean inclusive)
{
super(name, inclusive);
URI uri = URI.create(name);
if (!uri.isAbsolute() && !"file".equalsIgnoreCase(uri.getScheme()))
throw new IllegalArgumentException("Not a valid file URI: " + name);
_path = Paths.get(uri);
}
public Path getPath()
{
return _path;
}
}
private static class ModuleEntry extends Entry
{
private final String _module;
protected ModuleEntry(String name, boolean inclusive)
{
super(name, inclusive);
if (!getName().startsWith("jrt:"))
throw new IllegalArgumentException(name);
_module = getName().split("/")[1];
}
public String getModule()
{
return _module;
}
}
public static class ByPackage extends AbstractSet implements Predicate
{
private final Index.Mutable _entries = new Index.Builder()
.caseSensitive(true)
.mutable()
.build();
@Override
public boolean test(String name)
{
return _entries.getBest(name) != null;
}
@Override
public Iterator 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 instanceof ClassEntry)
name += "$";
else if (!(entry instanceof PackageEntry))
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();
}
}
public static class ByClass extends HashSet implements Predicate
{
private final Map _entries = new HashMap<>();
@Override
public boolean test(String name)
{
return _entries.containsKey(name);
}
@Override
public Iterator iterator()
{
return _entries.values().iterator();
}
@Override
public int size()
{
return _entries.size();
}
@Override
public boolean add(Entry entry)
{
if (!(entry instanceof ClassEntry))
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 implements Predicate
{
private final ByClass _byClass = new ByClass();
private final ByPackage _byPackage = new ByPackage();
@Override
public boolean test(String name)
{
return _byPackage.test(name) || _byClass.test(name);
}
@Override
public Iterator 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 entry)
{
if (entry instanceof PackageEntry)
return _byPackage.add(entry);
if (entry instanceof ClassEntry)
{
// Add class name to packages also as classes act
// as packages for nested classes.
boolean added = _byPackage.add(entry);
added = _byClass.add(entry) || added;
return added;
}
throw new IllegalArgumentException();
}
@Override
public boolean remove(Object o)
{
if (!(o instanceof Entry))
return false;
boolean removedPackage = _byPackage.remove(o);
boolean removedClass = _byClass.remove(o);
return removedPackage || removedClass;
}
@Override
public void clear()
{
_byPackage.clear();
_byClass.clear();
}
}
public static class ByLocation extends HashSet implements Predicate
{
@Override
public boolean test(URI uri)
{
if ((uri == null) || (!uri.isAbsolute()))
return false;
if (!uri.getScheme().equals("file"))
return false;
Path path = Paths.get(uri);
for (Entry entry : this)
{
if (!(entry instanceof LocationEntry))
throw new IllegalStateException();
Path entryPath = ((LocationEntry)entry).getPath();
if (Files.isDirectory(entryPath))
{
if (path.startsWith(entryPath))
{
return true;
}
}
else
{
try
{
if (Files.isSameFile(path, entryPath))
{
return true;
}
}
catch (IOException ignore)
{
// this means there is a FileSystem issue preventing comparison.
// Use old technique
if (path.equals(entryPath))
{
return true;
}
}
}
}
return false;
}
}
public static class ByModule extends HashSet implements Predicate
{
private final Index.Mutable _entries = new Index.Builder()
.caseSensitive(true)
.mutable()
.build();
@Override
public boolean test(URI uri)
{
if ((uri == null) || (!uri.isAbsolute()))
return false;
if (!uri.getScheme().equalsIgnoreCase("jrt"))
return false;
String module = uri.getPath();
int end = module.indexOf('/', 1);
if (end < 1)
end = module.length();
return _entries.get(module, 1, end - 1) != null;
}
@Override
public Iterator iterator()
{
return _entries.keySet().stream().map(_entries::get).iterator();
}
@Override
public int size()
{
return _entries.size();
}
@Override
public boolean add(Entry entry)
{
if (!(entry instanceof ModuleEntry))
throw new IllegalArgumentException(entry.toString());
String module = ((ModuleEntry)entry).getModule();
if (_entries.get(module) != null)
return false;
_entries.put(module, entry);
return true;
}
@Override
public boolean remove(Object entry)
{
if (!(entry instanceof Entry))
return false;
return _entries.remove(((Entry)entry).getName()) != null;
}
}
public static class ByLocationOrModule extends AbstractSet implements Predicate
{
private final ByLocation _byLocation = new ByLocation();
private final ByModule _byModule = new ByModule();
@Override
public boolean test(URI name)
{
if ((name == null) || (!name.isAbsolute()))
return false;
return _byLocation.test(name) || _byModule.test(name);
}
@Override
public Iterator iterator()
{
Set entries = new HashSet<>();
entries.addAll(_byLocation);
entries.addAll(_byModule);
return entries.iterator();
}
@Override
public int size()
{
return _byLocation.size() + _byModule.size();
}
@Override
public boolean add(Entry entry)
{
if (entry instanceof LocationEntry)
return _byLocation.add(entry);
if (entry instanceof ModuleEntry)
return _byModule.add(entry);
throw new IllegalArgumentException(entry.toString());
}
@Override
public boolean remove(Object o)
{
if (o instanceof LocationEntry)
return _byLocation.remove(o);
if (o instanceof ModuleEntry)
return _byModule.remove(o);
return false;
}
@Override
public void clear()
{
_byLocation.clear();
_byModule.clear();
}
}
protected final Map _entries;
protected final IncludeExcludeSet _patterns;
protected final IncludeExcludeSet _locations;
protected ClassMatcher(Map entries, IncludeExcludeSet patterns, IncludeExcludeSet locations)
{
_entries = entries;
_patterns = patterns == null ? new IncludeExcludeSet<>(ByPackageOrName.class) : patterns;
_locations = locations == null ? new IncludeExcludeSet<>(ByLocationOrModule.class) : locations;
}
private ClassMatcher(Map entries)
{
this(entries, null, null);
}
public ClassMatcher()
{
this(new HashMap<>());
}
public ClassMatcher(ClassMatcher patterns)
{
this(new HashMap<>());
if (patterns != null)
setAll(patterns.getPatterns());
}
public ClassMatcher(String... patterns)
{
this(new HashMap<>());
if (patterns != null && patterns.length > 0)
setAll(patterns);
}
public ClassMatcher(String pattern)
{
this(new HashMap<>());
add(pattern);
}
@Deprecated
protected interface Constructor
{
T construct(Map entries, IncludeExcludeSet patterns, IncludeExcludeSet locations);
}
/**
* Wrap an instance of a {@link ClassMatcher} using a constructor of an extended {@code ClassMatcher}
* that needs access to the internal fields of the passed matcher.
* @param matcher The matcher to wrap
* @param constructor The constructor to build the API specific wrapper
* @param The type of the API specific wrapper
* @return A wrapper of the {@code matcher}, sharing internal state.
* @deprecated use {@link ClassMatcher} directly.
*/
@Deprecated
protected static T wrap(ClassMatcher matcher, Constructor constructor)
{
return constructor.construct(matcher._entries, matcher._patterns, matcher._locations);
}
public ClassMatcher asImmutable()
{
return new ClassMatcher(Map.copyOf(_entries),
_patterns.asImmutable(),
_locations.asImmutable());
}
public boolean include(String name)
{
if (name == null)
return false;
return add(newEntry(name, true));
}
public boolean include(String... name)
{
boolean added = false;
for (String n : name)
{
if (n != null)
added = add(newEntry(n, true)) || added;
}
return added;
}
public boolean exclude(String name)
{
if (name == null)
return false;
return add(newEntry(name, false));
}
public boolean exclude(String... name)
{
boolean added = false;
for (String n : name)
{
if (n != null)
added = add(newEntry(n, false)) || added;
}
return added;
}
@Override
public boolean add(String pattern)
{
if (pattern == null)
return false;
return add(newEntry(pattern));
}
public boolean add(String... pattern)
{
boolean added = false;
for (String p : pattern)
{
if (p != null)
added = add(newEntry(p)) || added;
}
return added;
}
protected boolean add(Entry entry)
{
if (_entries.containsKey(entry.getPattern()))
return false;
_entries.put(entry.getPattern(), entry);
if (entry instanceof LocationEntry || entry instanceof ModuleEntry)
{
if (entry.isInclusive())
_locations.include(entry);
else
_locations.exclude(entry);
}
else
{
if (entry.isInclusive())
_patterns.include(entry);
else
_patterns.exclude(entry);
}
return true;
}
protected Entry newEntry(String pattern)
{
if (pattern.startsWith("-"))
return newEntry(pattern.substring(1), false);
return newEntry(pattern, true);
}
protected Entry newEntry(String name, boolean inclusive)
{
if (name.startsWith("-"))
throw new IllegalStateException(name);
if (name.startsWith("file:"))
return new LocationEntry(name, inclusive);
if (name.startsWith("jrt:"))
return new ModuleEntry(name, inclusive);
if (name.endsWith("."))
return new PackageEntry(name, inclusive);
return new ClassEntry(name, inclusive);
}
@Override
public boolean remove(Object o)
{
if (!(o instanceof String pattern))
return false;
Entry entry = _entries.remove(pattern);
if (entry == null)
return false;
List 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 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);
}
/**
* Add array of classpath patterns.
* @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[0]);
}
/**
* @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
{
return combine(_patterns, clazz.getName(), _locations, () -> TypeUtil.getLocationOfClass(clazz));
}
catch (Exception ignored)
{
}
return false;
}
public boolean match(String name, URL url)
{
if (url == null)
return false;
// 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 = StringUtil.replace(name, '/', '.');
return combine(_patterns, name, _locations, () ->
{
try
{
return URIUtil.unwrapContainer(url.toURI());
}
catch (URISyntaxException ignored)
{
return null;
}
});
}
/**
* Match a class against inclusions and exclusions by name and location.
* Name based checks are performed before location checks. For a class to match,
* it must not be excluded by either name or location, and must either be explicitly
* included, or for there to be no inclusions. In the case where the location
* of the class is null, it will match if it is included by name, or
* if there are no location exclusions.
*
* @param names configured inclusions and exclusions by name
* @param name the name to check
* @param locations configured inclusions and exclusions by location
* @param location the location of the class (can be null)
* @return true if the class is not excluded but is included, or there are
* no inclusions. False otherwise.
*/
static boolean combine(IncludeExcludeSet names, String name, IncludeExcludeSet locations, Supplier location)
{
// check the name set
Boolean byName = names.isIncludedAndNotExcluded(name);
// If we excluded by name, then no match
if (Boolean.FALSE == byName)
return false;
// check the location set
URI uri = location.get();
Boolean byLocation = uri == null ? null : locations.isIncludedAndNotExcluded(uri);
// If we excluded by location or couldn't check location exclusion, then no match
if (Boolean.FALSE == byLocation || (locations.hasExcludes() && uri == null))
return false;
// If there are includes, then we must be included to match.
if (names.hasIncludes() || locations.hasIncludes())
return byName == Boolean.TRUE || byLocation == Boolean.TRUE;
// Otherwise there are no includes and it was not excluded, so match
return true;
}
}