[MNG-7820] Get rid of plexus-utils introspection classes (#1251)

This commit is contained in:
Guillaume Nodet 2023-09-22 08:02:25 +02:00 committed by GitHub
parent e91cee9a2d
commit fd4493580a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1686 additions and 6 deletions

View File

@ -0,0 +1,388 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.plugin;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Hashtable;
import java.util.Map;
/**
* A cache of introspection information for a specific class instance.
* Keys {@link Method} objects by a concatenation of the
* method name and the names of classes that make up the parameters.
*/
class ClassMap {
private static final class CacheMiss {}
private static final CacheMiss CACHE_MISS = new CacheMiss();
private static final Object OBJECT = new Object();
/**
* Class passed into the constructor used to as
* the basis for the Method map.
*/
private final Class<?> clazz;
/**
* Cache of Methods, or CACHE_MISS, keyed by method
* name and actual arguments used to find it.
*/
private final Map<String, Object> methodCache = new Hashtable<>();
private MethodMap methodMap = new MethodMap();
/**
* Standard constructor
* @param clazz The class.
*/
ClassMap(Class<?> clazz) {
this.clazz = clazz;
populateMethodCache();
}
/**
* @return the class object whose methods are cached by this map.
*/
Class<?> getCachedClass() {
return clazz;
}
/**
* <p>Find a Method using the methodKey provided.</p>
* <p>Look in the methodMap for an entry. If found,
* it'll either be a CACHE_MISS, in which case we
* simply give up, or it'll be a Method, in which
* case, we return it.</p>
* <p>If nothing is found, then we must actually go
* and introspect the method from the MethodMap.</p>
* @param name Method name.
* @param params Method parameters.
* @return The found method.
* @throws MethodMap.AmbiguousException in case of duplicate methods.
*/
public Method findMethod(String name, Object... params) throws MethodMap.AmbiguousException {
String methodKey = makeMethodKey(name, params);
Object cacheEntry = methodCache.get(methodKey);
if (cacheEntry == CACHE_MISS) {
return null;
}
if (cacheEntry == null) {
try {
cacheEntry = methodMap.find(name, params);
} catch (MethodMap.AmbiguousException ae) {
// that's a miss :)
methodCache.put(methodKey, CACHE_MISS);
throw ae;
}
if (cacheEntry == null) {
methodCache.put(methodKey, CACHE_MISS);
} else {
methodCache.put(methodKey, cacheEntry);
}
}
// Yes, this might just be null.
return (Method) cacheEntry;
}
/**
* Populate the Map of direct hits. These
* are taken from all the public methods
* that our class provides.
*/
private void populateMethodCache() {
// get all publicly accessible methods
Method[] methods = getAccessibleMethods(clazz);
// map and cache them
for (Method method : methods) {
// now get the 'public method', the method declared by a
// public interface or class (because the actual implementing
// class may be a facade...)
Method publicMethod = getPublicMethod(method);
// it is entirely possible that there is no public method for
// the methods of this class (i.e. in the facade, a method
// that isn't on any of the interfaces or superclass
// in which case, ignore it. Otherwise, map and cache
if (publicMethod != null) {
methodMap.add(publicMethod);
methodCache.put(makeMethodKey(publicMethod), publicMethod);
}
}
}
/**
* Make a methodKey for the given method using
* the concatenation of the name and the
* types of the method parameters.
*/
private String makeMethodKey(Method method) {
Class<?>[] parameterTypes = method.getParameterTypes();
StringBuilder methodKey = new StringBuilder(method.getName());
for (Class<?> parameterType : parameterTypes) {
// If the argument type is primitive then we want
// to convert our primitive type signature to the
// corresponding Object type so introspection for
// methods with primitive types will work correctly.
if (parameterType.isPrimitive()) {
if (parameterType.equals(Boolean.TYPE)) {
methodKey.append("java.lang.Boolean");
} else if (parameterType.equals(Byte.TYPE)) {
methodKey.append("java.lang.Byte");
} else if (parameterType.equals(Character.TYPE)) {
methodKey.append("java.lang.Character");
} else if (parameterType.equals(Double.TYPE)) {
methodKey.append("java.lang.Double");
} else if (parameterType.equals(Float.TYPE)) {
methodKey.append("java.lang.Float");
} else if (parameterType.equals(Integer.TYPE)) {
methodKey.append("java.lang.Integer");
} else if (parameterType.equals(Long.TYPE)) {
methodKey.append("java.lang.Long");
} else if (parameterType.equals(Short.TYPE)) {
methodKey.append("java.lang.Short");
}
} else {
methodKey.append(parameterType.getName());
}
}
return methodKey.toString();
}
private static String makeMethodKey(String method, Object... params) {
StringBuilder methodKey = new StringBuilder().append(method);
for (Object param : params) {
Object arg = param;
if (arg == null) {
arg = OBJECT;
}
methodKey.append(arg.getClass().getName());
}
return methodKey.toString();
}
/**
* Retrieves public methods for a class. In case the class is not
* public, retrieves methods with same signature as its public methods
* from public superclasses and interfaces (if they exist). Basically
* upcasts every method to the nearest acccessible method.
*/
private static Method[] getAccessibleMethods(Class<?> clazz) {
Method[] methods = clazz.getMethods();
// Short circuit for the (hopefully) majority of cases where the
// clazz is public
if (Modifier.isPublic(clazz.getModifiers())) {
return methods;
}
// No luck - the class is not public, so we're going the longer way.
MethodInfo[] methodInfos = new MethodInfo[methods.length];
for (int i = methods.length; i-- > 0; ) {
methodInfos[i] = new MethodInfo(methods[i]);
}
int upcastCount = getAccessibleMethods(clazz, methodInfos, 0);
// Reallocate array in case some method had no accessible counterpart.
if (upcastCount < methods.length) {
methods = new Method[upcastCount];
}
int j = 0;
for (MethodInfo methodInfo : methodInfos) {
if (methodInfo.upcast) {
methods[j++] = methodInfo.method;
}
}
return methods;
}
/**
* Recursively finds a match for each method, starting with the class, and then
* searching the superclass and interfaces.
*
* @param clazz Class to check
* @param methodInfos array of methods we are searching to match
* @param upcastCount current number of methods we have matched
* @return count of matched methods
*/
private static int getAccessibleMethods(Class<?> clazz, MethodInfo[] methodInfos, int upcastCount) {
int l = methodInfos.length;
// if this class is public, then check each of the currently
// 'non-upcasted' methods to see if we have a match
if (Modifier.isPublic(clazz.getModifiers())) {
for (int i = 0; i < l && upcastCount < l; ++i) {
try {
MethodInfo methodInfo = methodInfos[i];
if (!methodInfo.upcast) {
methodInfo.tryUpcasting(clazz);
upcastCount++;
}
} catch (NoSuchMethodException e) {
// Intentionally ignored - it means it wasn't found in the current class
}
}
/*
* Short circuit if all methods were upcast
*/
if (upcastCount == l) {
return upcastCount;
}
}
// Examine superclass
Class<?> superclazz = clazz.getSuperclass();
if (superclazz != null) {
upcastCount = getAccessibleMethods(superclazz, methodInfos, upcastCount);
// Short circuit if all methods were upcast
if (upcastCount == l) {
return upcastCount;
}
}
// Examine interfaces. Note we do it even if superclazz == null.
// This is redundant as currently java.lang.Object does not implement
// any interfaces, however nothing guarantees it will not in the future.
Class<?>[] interfaces = clazz.getInterfaces();
for (int i = interfaces.length; i-- > 0; ) {
upcastCount = getAccessibleMethods(interfaces[i], methodInfos, upcastCount);
// Short circuit if all methods were upcast
if (upcastCount == l) {
return upcastCount;
}
}
return upcastCount;
}
/**
* For a given method, retrieves its publicly accessible counterpart.
* This method will look for a method with same name
* and signature declared in a public superclass or implemented interface of this
* method's declaring class. This counterpart method is publicly callable.
*
* @param method a method whose publicly callable counterpart is requested.
* @return the publicly callable counterpart method. Note that if the parameter
* method is itself declared by a public class, this method is an identity
* function.
*/
private static Method getPublicMethod(Method method) {
Class<?> clazz = method.getDeclaringClass();
// Short circuit for (hopefully the majority of) cases where the declaring
// class is public.
if ((clazz.getModifiers() & Modifier.PUBLIC) != 0) {
return method;
}
return getPublicMethod(clazz, method.getName(), method.getParameterTypes());
}
/**
* Looks up the method with specified name and signature in the first public
* superclass or implemented interface of the class.
*
* @param clazz the class whose method is sought
* @param name the name of the method
* @param paramTypes the classes of method parameters
*/
private static Method getPublicMethod(Class<?> clazz, String name, Class<?>... paramTypes) {
// if this class is public, then try to get it
if ((clazz.getModifiers() & Modifier.PUBLIC) != 0) {
try {
return clazz.getMethod(name, paramTypes);
} catch (NoSuchMethodException e) {
// If the class does not have the method, then neither its superclass
// nor any of its interfaces has it so quickly return null.
return null;
}
}
// try the superclass
Class<?> superclazz = clazz.getSuperclass();
if (superclazz != null) {
Method superclazzMethod = getPublicMethod(superclazz, name, paramTypes);
if (superclazzMethod != null) {
return superclazzMethod;
}
}
// and interfaces
Class<?>[] interfaces = clazz.getInterfaces();
for (Class<?> anInterface : interfaces) {
Method interfaceMethod = getPublicMethod(anInterface, name, paramTypes);
if (interfaceMethod != null) {
return interfaceMethod;
}
}
return null;
}
/**
* Used for the iterative discovery process for public methods.
*/
private static final class MethodInfo {
Method method;
String name;
Class<?>[] parameterTypes;
boolean upcast;
MethodInfo(Method method) {
this.method = null;
name = method.getName();
parameterTypes = method.getParameterTypes();
upcast = false;
}
void tryUpcasting(Class<?> clazz) throws NoSuchMethodException {
method = clazz.getMethod(name, parameterTypes);
name = null;
parameterTypes = null;
upcast = true;
}
}
}

View File

@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.plugin;
class IntrospectionException extends Exception {
/**
*
*/
private static final long serialVersionUID = -6090771282553728784L;
IntrospectionException(String message) {
super(message);
}
IntrospectionException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,389 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.plugin;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
class MethodMap {
private static final int MORE_SPECIFIC = 0;
private static final int LESS_SPECIFIC = 1;
private static final int INCOMPARABLE = 2;
/**
* Keep track of all methods with the same name.
*/
private final Map<String, List<Method>> methodByNameMap = new Hashtable<>();
/**
* Add a method to a list of methods by name.
* For a particular class we are keeping track
* of all the methods with the same name.
*
* @param method The method
*/
void add(Method method) {
String methodName = method.getName();
List<Method> l = get(methodName);
if (l == null) {
l = new ArrayList<>();
methodByNameMap.put(methodName, l);
}
l.add(method);
}
/**
* Return a list of methods with the same name.
*
* @param key The name of the method.
* @return List list of methods
*/
List<Method> get(String key) {
return methodByNameMap.get(key);
}
/**
* Find a method. Attempts to find the
* most specific applicable method using the
* algorithm described in the JLS section
* 15.12.2 (with the exception that it can't
* distinguish a primitive type argument from
* an object type argument, since in reflection
* primitive type arguments are represented by
* their object counterparts, so for an argument of
* type (say) java.lang.Integer, it will not be able
* to decide between a method that takes int and a
* method that takes java.lang.Integer as a parameter.
* <p>
* This turns out to be a relatively rare case
* where this is needed - however, functionality
* like this is needed.
*
* @param methodName name of method
* @param args the actual arguments with which the method is called
* @return the most specific applicable method, or null if no
* method is applicable.
* @throws AmbiguousException if there is more than one maximally
* specific applicable method
*/
Method find(String methodName, Object... args) throws AmbiguousException {
List<Method> methodList = get(methodName);
if (methodList == null) {
return null;
}
int l = args.length;
Class<?>[] classes = new Class[l];
for (int i = 0; i < l; ++i) {
Object arg = args[i];
// if we are careful down below, a null argument goes in there
// so we can know that the null was passed to the method
classes[i] = arg == null ? null : arg.getClass();
}
return getMostSpecific(methodList, classes);
}
/**
* simple distinguishable exception, used when
* we run across ambiguous overloading
*/
static class AmbiguousException extends Exception {
private static final long serialVersionUID = 751688436639650618L;
}
private static Method getMostSpecific(List<Method> methods, Class<?>... classes) throws AmbiguousException {
LinkedList<Method> applicables = getApplicables(methods, classes);
if (applicables.isEmpty()) {
return null;
}
if (applicables.size() == 1) {
return applicables.getFirst();
}
// This list will contain the maximally specific methods. Hopefully at
// the end of the below loop, the list will contain exactly one method,
// (the most specific method) otherwise we have ambiguity.
LinkedList<Method> maximals = new LinkedList<>();
for (Method app : applicables) {
Class<?>[] appArgs = app.getParameterTypes();
boolean lessSpecific = false;
for (Iterator<Method> maximal = maximals.iterator(); !lessSpecific && maximal.hasNext(); ) {
Method max = maximal.next();
switch (moreSpecific(appArgs, max.getParameterTypes())) {
case MORE_SPECIFIC:
// This method is more specific than the previously
// known maximally specific, so remove the old maximum.
maximal.remove();
break;
case LESS_SPECIFIC:
// This method is less specific than some of the
// currently known maximally specific methods, so we
// won't add it into the set of maximally specific
// methods
lessSpecific = true;
break;
default:
}
}
if (!lessSpecific) {
maximals.addLast(app);
}
}
if (maximals.size() > 1) {
// We have more than one maximally specific method
throw new AmbiguousException();
}
return maximals.getFirst();
}
/**
* Determines which method signature (represented by a class array) is more
* specific. This defines a partial ordering on the method signatures.
*
* @param c1 first signature to compare
* @param c2 second signature to compare
* @return MORE_SPECIFIC if c1 is more specific than c2, LESS_SPECIFIC if
* c1 is less specific than c2, INCOMPARABLE if they are incomparable.
*/
private static int moreSpecific(Class<?>[] c1, Class<?>[] c2) {
boolean c1MoreSpecific = false;
boolean c2MoreSpecific = false;
for (int i = 0; i < c1.length; ++i) {
if (c1[i] != c2[i]) {
c1MoreSpecific = c1MoreSpecific || isStrictMethodInvocationConvertible(c2[i], c1[i]);
c2MoreSpecific = c2MoreSpecific || isStrictMethodInvocationConvertible(c1[i], c2[i]);
}
}
if (c1MoreSpecific) {
if (c2MoreSpecific) {
// Incomparable due to cross-assignable arguments (i.e.
// foo(String, Object) vs. foo(Object, String))
return INCOMPARABLE;
}
return MORE_SPECIFIC;
}
if (c2MoreSpecific) {
return LESS_SPECIFIC;
}
// Incomparable due to non-related arguments (i.e.
// foo(Runnable) vs. foo(Serializable))
return INCOMPARABLE;
}
/**
* Returns all methods that are applicable to actual argument types.
*
* @param methods list of all candidate methods
* @param classes the actual types of the arguments
* @return a list that contains only applicable methods (number of
* formal and actual arguments matches, and argument types are assignable
* to formal types through a method invocation conversion).
*/
private static LinkedList<Method> getApplicables(List<Method> methods, Class<?>... classes) {
LinkedList<Method> list = new LinkedList<>();
for (Method method : methods) {
if (isApplicable(method, classes)) {
list.add(method);
}
}
return list;
}
/**
* Returns true if the supplied method is applicable to actual
* argument types.
*
* @param method The method to check for applicability
* @param classes The arguments
* @return true if the method applies to the parameter types
*/
private static boolean isApplicable(Method method, Class<?>... classes) {
Class<?>[] methodArgs = method.getParameterTypes();
if (methodArgs.length != classes.length) {
return false;
}
for (int i = 0; i < classes.length; ++i) {
if (!isMethodInvocationConvertible(methodArgs[i], classes[i])) {
return false;
}
}
return true;
}
/**
* Determines whether a type represented by a class object is
* convertible to another type represented by a class object using a
* method invocation conversion, treating object types of primitive
* types as if they were primitive types (that is, a Boolean actual
* parameter type matches boolean primitive formal type). This behavior
* is because this method is used to determine applicable methods for
* an actual parameter list, and primitive types are represented by
* their object duals in reflective method calls.
*
* @param formal the formal parameter type to which the actual
* parameter type should be convertible
* @param actual the actual parameter type.
* @return true if either formal type is assignable from actual type,
* or formal is a primitive type and actual is its corresponding object
* type or an object type of a primitive type that can be converted to
* the formal type.
*/
private static boolean isMethodInvocationConvertible(Class<?> formal, Class<?> actual) {
// if it's a null, it means the arg was null
if (actual == null && !formal.isPrimitive()) {
return true;
}
// Check for identity or widening reference conversion
if (actual != null && formal.isAssignableFrom(actual)) {
return true;
}
// Check for boxing with widening primitive conversion. Note that
// actual parameters are never primitives.
if (formal.isPrimitive()) {
if (formal == Boolean.TYPE && actual == Boolean.class) {
return true;
}
if (formal == Character.TYPE && actual == Character.class) {
return true;
}
if (formal == Byte.TYPE && actual == Byte.class) {
return true;
}
if (formal == Short.TYPE && (actual == Short.class || actual == Byte.class)) {
return true;
}
if (formal == Integer.TYPE && (actual == Integer.class || actual == Short.class || actual == Byte.class)) {
return true;
}
if (formal == Long.TYPE
&& (actual == Long.class
|| actual == Integer.class
|| actual == Short.class
|| actual == Byte.class)) {
return true;
}
if (formal == Float.TYPE
&& (actual == Float.class
|| actual == Long.class
|| actual == Integer.class
|| actual == Short.class
|| actual == Byte.class)) {
return true;
}
if (formal == Double.TYPE
&& (actual == Double.class
|| actual == Float.class
|| actual == Long.class
|| actual == Integer.class
|| actual == Short.class
|| actual == Byte.class)) {
return true;
}
}
return false;
}
/**
* Determines whether a type represented by a class object is
* convertible to another type represented by a class object using a
* method invocation conversion, without matching object and primitive
* types. This method is used to determine the more specific type when
* comparing signatures of methods.
*
* @param formal the formal parameter type to which the actual
* parameter type should be convertible
* @param actual the actual parameter type.
* @return true if either formal type is assignable from actual type,
* or formal and actual are both primitive types and actual can be
* subject to widening conversion to formal.
*/
private static boolean isStrictMethodInvocationConvertible(Class<?> formal, Class<?> actual) {
// we shouldn't get a null into, but if so
if (actual == null && !formal.isPrimitive()) {
return true;
}
// Check for identity or widening reference conversion
if (formal.isAssignableFrom(actual)) {
return true;
}
// Check for widening primitive conversion.
if (formal.isPrimitive()) {
if (formal == Short.TYPE && (actual == Byte.TYPE)) {
return true;
}
if (formal == Integer.TYPE && (actual == Short.TYPE || actual == Byte.TYPE)) {
return true;
}
if (formal == Long.TYPE && (actual == Integer.TYPE || actual == Short.TYPE || actual == Byte.TYPE)) {
return true;
}
if (formal == Float.TYPE
&& (actual == Long.TYPE || actual == Integer.TYPE || actual == Short.TYPE || actual == Byte.TYPE)) {
return true;
}
if (formal == Double.TYPE
&& (actual == Float.TYPE
|| actual == Long.TYPE
|| actual == Integer.TYPE
|| actual == Short.TYPE
|| actual == Byte.TYPE)) {
return true;
}
}
return false;
}
}

View File

@ -27,7 +27,6 @@ import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
import org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
import org.codehaus.plexus.util.introspection.ReflectionValueExtractor;
/**
* Evaluator for plugin parameters expressions. Content surrounded by <code>${</code> and <code>}</code> is evaluated.

View File

@ -32,7 +32,6 @@ import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
import org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
import org.codehaus.plexus.util.introspection.ReflectionValueExtractor;
/**
* Evaluator for plugin parameters expressions. Content surrounded by <code>${</code> and <code>}</code> is evaluated.

View File

@ -0,0 +1,298 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.plugin;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.plugin.MethodMap.AmbiguousException;
/**
* Using simple dotted expressions to extract the values from an Object instance using JSP-like expressions
* such as {@code project.build.sourceDirectory}.
*/
class ReflectionValueExtractor {
private static final Object[] OBJECT_ARGS = new Object[0];
/**
* Use a WeakHashMap here, so the keys (Class objects) can be garbage collected.
* This approach prevents permgen space overflows due to retention of discarded
* classloaders.
*/
private static final Map<Class<?>, ClassMap> CLASS_MAPS = new WeakHashMap<>();
static final int EOF = -1;
static final char PROPERTY_START = '.';
static final char INDEXED_START = '[';
static final char INDEXED_END = ']';
static final char MAPPED_START = '(';
static final char MAPPED_END = ')';
static class Tokenizer {
final String expression;
int idx;
Tokenizer(String expression) {
this.expression = expression;
}
public int peekChar() {
return idx < expression.length() ? expression.charAt(idx) : EOF;
}
public int skipChar() {
return idx < expression.length() ? expression.charAt(idx++) : EOF;
}
public String nextToken(char delimiter) {
int start = idx;
while (idx < expression.length() && delimiter != expression.charAt(idx)) {
idx++;
}
// delimiter MUST be present
if (idx <= start || idx >= expression.length()) {
return null;
}
return expression.substring(start, idx++);
}
public String nextPropertyName() {
final int start = idx;
while (idx < expression.length() && Character.isJavaIdentifierPart(expression.charAt(idx))) {
idx++;
}
// property name does not require delimiter
if (idx <= start || idx > expression.length()) {
return null;
}
return expression.substring(start, idx);
}
public int getPosition() {
return idx < expression.length() ? idx : EOF;
}
// to make tokenizer look pretty in debugger
@Override
public String toString() {
return idx < expression.length() ? expression.substring(idx) : "<EOF>";
}
}
private ReflectionValueExtractor() {}
/**
* <p>The implementation supports indexed, nested and mapped properties.</p>
* <ul>
* <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
* <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
* pattern, i.e. "user.addresses[1].street"</li>
* <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern,
* i.e. "user.addresses(myAddress).street"</li>
* </ul>
*
* @param expression not null expression
* @param root not null object
* @return the object defined by the expression
* @throws IntrospectionException if any
*/
public static Object evaluate(@Nonnull String expression, @Nullable Object root) throws IntrospectionException {
return evaluate(expression, root, true);
}
/**
* <p>
* The implementation supports indexed, nested and mapped properties.
* </p>
* <ul>
* <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
* <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
* pattern, i.e. "user.addresses[1].street"</li>
* <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e.
* "user.addresses(myAddress).street"</li>
* </ul>
*
* @param expression not null expression
* @param root not null object
* @param trimRootToken trim root token yes/no.
* @return the object defined by the expression
* @throws IntrospectionException if any
*/
public static Object evaluate(@Nonnull String expression, @Nullable Object root, boolean trimRootToken)
throws IntrospectionException {
Object value = root;
// ----------------------------------------------------------------------
// Walk the dots and retrieve the ultimate value desired from the
// MavenProject instance.
// ----------------------------------------------------------------------
if (expression == null || expression.isEmpty() || !Character.isJavaIdentifierStart(expression.charAt(0))) {
return null;
}
boolean hasDots = expression.indexOf(PROPERTY_START) >= 0;
final Tokenizer tokenizer;
if (trimRootToken && hasDots) {
tokenizer = new Tokenizer(expression);
tokenizer.nextPropertyName();
if (tokenizer.getPosition() == EOF) {
return null;
}
} else {
tokenizer = new Tokenizer("." + expression);
}
int propertyPosition = tokenizer.getPosition();
while (value != null && tokenizer.peekChar() != EOF) {
switch (tokenizer.skipChar()) {
case INDEXED_START:
value = getIndexedValue(
expression,
propertyPosition,
tokenizer.getPosition(),
value,
tokenizer.nextToken(INDEXED_END));
break;
case MAPPED_START:
value = getMappedValue(
expression,
propertyPosition,
tokenizer.getPosition(),
value,
tokenizer.nextToken(MAPPED_END));
break;
case PROPERTY_START:
propertyPosition = tokenizer.getPosition();
value = getPropertyValue(value, tokenizer.nextPropertyName());
break;
default:
// could not parse expression
return null;
}
}
return value;
}
private static Object getMappedValue(
final String expression, final int from, final int to, final Object value, final String key)
throws IntrospectionException {
if (value == null || key == null) {
return null;
}
if (value instanceof Map) {
return ((Map) value).get(key);
}
final String message = String.format(
"The token '%s' at position '%d' refers to a java.util.Map, but the value "
+ "seems is an instance of '%s'",
expression.subSequence(from, to), from, value.getClass());
throw new IntrospectionException(message);
}
private static Object getIndexedValue(
final String expression, final int from, final int to, final Object value, final String indexStr)
throws IntrospectionException {
try {
int index = Integer.parseInt(indexStr);
if (value.getClass().isArray()) {
return Array.get(value, index);
}
if (value instanceof List) {
return ((List) value).get(index);
}
} catch (NumberFormatException | IndexOutOfBoundsException e) {
return null;
}
final String message = String.format(
"The token '%s' at position '%d' refers to a java.util.List or an array, but the value "
+ "seems is an instance of '%s'",
expression.subSequence(from, to), from, value.getClass());
throw new IntrospectionException(message);
}
private static Object getPropertyValue(Object value, String property) throws IntrospectionException {
if (value == null || property == null || property.isEmpty()) {
return null;
}
ClassMap classMap = getClassMap(value.getClass());
String methodBase = Character.toTitleCase(property.charAt(0)) + property.substring(1);
String methodName = "get" + methodBase;
try {
Method method = classMap.findMethod(methodName);
if (method == null) {
// perhaps this is a boolean property??
methodName = "is" + methodBase;
method = classMap.findMethod(methodName);
}
if (method == null) {
return null;
}
return method.invoke(value, OBJECT_ARGS);
} catch (InvocationTargetException e) {
throw new IntrospectionException(e.getTargetException());
} catch (AmbiguousException | IllegalAccessException e) {
throw new IntrospectionException(e);
}
}
private static ClassMap getClassMap(Class<?> clazz) {
ClassMap classMap = CLASS_MAPS.get(clazz);
if (classMap == null) {
classMap = new ClassMap(clazz);
CLASS_MAPS.put(clazz, classMap);
}
return classMap;
}
}

View File

@ -21,7 +21,6 @@ package org.apache.maven.plugin;
import javax.inject.Inject;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@ -375,7 +374,7 @@ class PluginParameterExpressionEvaluatorTest extends AbstractCoreMavenComponentT
void testRootDirectoryWithNull() throws Exception {
ExpressionEvaluator ee = createExpressionEvaluator(createDefaultProject(), null, new Properties());
Exception e = assertThrows(Exception.class, () -> ee.evaluate("${session.rootDirectory}"));
e = assertInstanceOf(InvocationTargetException.class, e.getCause());
e = assertInstanceOf(IntrospectionException.class, e.getCause());
e = assertInstanceOf(IllegalStateException.class, e.getCause());
assertEquals(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE, e.getMessage());
}

View File

@ -21,7 +21,6 @@ package org.apache.maven.plugin;
import javax.inject.Inject;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@ -388,7 +387,7 @@ public class PluginParameterExpressionEvaluatorV4Test extends AbstractCoreMavenC
void testRootDirectoryWithNull() throws Exception {
ExpressionEvaluator ee = createExpressionEvaluator(createDefaultProject(), null, new Properties());
Exception e = assertThrows(Exception.class, () -> ee.evaluate("${session.rootDirectory}"));
e = assertInstanceOf(InvocationTargetException.class, e.getCause());
e = assertInstanceOf(IntrospectionException.class, e.getCause());
e = assertInstanceOf(IllegalStateException.class, e.getCause());
assertEquals(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE, e.getMessage());
}

View File

@ -0,0 +1,574 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.plugin;
/*
* Copyright The Codehaus Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* ReflectionValueExtractorTest class.
*/
public class ReflectionValueExtractorTest {
private Project project;
/**
* <p>setUp.</p>
*/
@BeforeEach
void setUp() {
Dependency dependency1 = new Dependency();
dependency1.setArtifactId("dep1");
Dependency dependency2 = new Dependency();
dependency2.setArtifactId("dep2");
project = new Project();
project.setModelVersion("4.0.0");
project.setGroupId("org.apache.maven");
project.setArtifactId("maven-core");
project.setName("Maven");
project.setVersion("2.0-SNAPSHOT");
project.setScm(new Scm());
project.getScm().setConnection("scm-connection");
project.addDependency(dependency1);
project.addDependency(dependency2);
project.setBuild(new Build());
// Build up an artifactMap
project.addArtifact(new Artifact("g0", "a0", "v0", "e0", "c0"));
project.addArtifact(new Artifact("g1", "a1", "v1", "e1", "c1"));
project.addArtifact(new Artifact("g2", "a2", "v2", "e2", "c2"));
}
/**
* <p>testValueExtraction.</p>
*
* @throws Exception if any.
*/
@Test
void testValueExtraction() throws Exception {
// ----------------------------------------------------------------------
// Top level values
// ----------------------------------------------------------------------
assertEquals("4.0.0", ReflectionValueExtractor.evaluate("project.modelVersion", project));
assertEquals("org.apache.maven", ReflectionValueExtractor.evaluate("project.groupId", project));
assertEquals("maven-core", ReflectionValueExtractor.evaluate("project.artifactId", project));
assertEquals("Maven", ReflectionValueExtractor.evaluate("project.name", project));
assertEquals("2.0-SNAPSHOT", ReflectionValueExtractor.evaluate("project.version", project));
// ----------------------------------------------------------------------
// SCM
// ----------------------------------------------------------------------
assertEquals("scm-connection", ReflectionValueExtractor.evaluate("project.scm.connection", project));
// ----------------------------------------------------------------------
// Dependencies
// ----------------------------------------------------------------------
List<?> dependencies = (List) ReflectionValueExtractor.evaluate("project.dependencies", project);
assertNotNull(dependencies);
assertEquals(2, dependencies.size());
// ----------------------------------------------------------------------
// Dependencies - using index notation
// ----------------------------------------------------------------------
// List
Dependency dependency = (Dependency) ReflectionValueExtractor.evaluate("project.dependencies[0]", project);
assertNotNull(dependency);
assertEquals("dep1", dependency.getArtifactId());
String artifactId = (String) ReflectionValueExtractor.evaluate("project.dependencies[1].artifactId", project);
assertEquals("dep2", artifactId);
// Array
dependency = (Dependency) ReflectionValueExtractor.evaluate("project.dependenciesAsArray[0]", project);
assertNotNull(dependency);
assertEquals("dep1", dependency.getArtifactId());
artifactId = (String) ReflectionValueExtractor.evaluate("project.dependenciesAsArray[1].artifactId", project);
assertEquals("dep2", artifactId);
// Map
dependency = (Dependency) ReflectionValueExtractor.evaluate("project.dependenciesAsMap(dep1)", project);
assertNotNull(dependency);
assertEquals("dep1", dependency.getArtifactId());
artifactId = (String) ReflectionValueExtractor.evaluate("project.dependenciesAsMap(dep2).artifactId", project);
assertEquals("dep2", artifactId);
// ----------------------------------------------------------------------
// Build
// ----------------------------------------------------------------------
Build build = (Build) ReflectionValueExtractor.evaluate("project.build", project);
assertNotNull(build);
}
/**
* <p>testValueExtractorWithAInvalidExpression.</p>
*
* @throws Exception if any.
*/
@Test
public void testValueExtractorWithAInvalidExpression() throws Exception {
assertNull(ReflectionValueExtractor.evaluate("project.foo", project));
assertNull(ReflectionValueExtractor.evaluate("project.dependencies[10]", project));
assertNull(ReflectionValueExtractor.evaluate("project.dependencies[0].foo", project));
}
/**
* <p>testMappedDottedKey.</p>
*
* @throws Exception if any.
*/
@Test
public void testMappedDottedKey() throws Exception {
Map<String, String> map = new HashMap<String, String>();
map.put("a.b", "a.b-value");
assertEquals("a.b-value", ReflectionValueExtractor.evaluate("h.value(a.b)", new ValueHolder(map)));
}
/**
* <p>testIndexedMapped.</p>
*
* @throws Exception if any.
*/
@Test
public void testIndexedMapped() throws Exception {
Map<Object, Object> map = new HashMap<Object, Object>();
map.put("a", "a-value");
List<Object> list = new ArrayList<Object>();
list.add(map);
assertEquals("a-value", ReflectionValueExtractor.evaluate("h.value[0](a)", new ValueHolder(list)));
}
/**
* <p>testMappedIndexed.</p>
*
* @throws Exception if any.
*/
@Test
public void testMappedIndexed() throws Exception {
List<Object> list = new ArrayList<Object>();
list.add("a-value");
Map<Object, Object> map = new HashMap<Object, Object>();
map.put("a", list);
assertEquals("a-value", ReflectionValueExtractor.evaluate("h.value(a)[0]", new ValueHolder(map)));
}
/**
* <p>testMappedMissingDot.</p>
*
* @throws Exception if any.
*/
@Test
public void testMappedMissingDot() throws Exception {
Map<Object, Object> map = new HashMap<Object, Object>();
map.put("a", new ValueHolder("a-value"));
assertNull(ReflectionValueExtractor.evaluate("h.value(a)value", new ValueHolder(map)));
}
/**
* <p>testIndexedMissingDot.</p>
*
* @throws Exception if any.
*/
@Test
public void testIndexedMissingDot() throws Exception {
List<Object> list = new ArrayList<Object>();
list.add(new ValueHolder("a-value"));
assertNull(ReflectionValueExtractor.evaluate("h.value[0]value", new ValueHolder(list)));
}
/**
* <p>testDotDot.</p>
*
* @throws Exception if any.
*/
@Test
public void testDotDot() throws Exception {
assertNull(ReflectionValueExtractor.evaluate("h..value", new ValueHolder("value")));
}
/**
* <p>testBadIndexedSyntax.</p>
*
* @throws Exception if any.
*/
@Test
public void testBadIndexedSyntax() throws Exception {
List<Object> list = new ArrayList<Object>();
list.add("a-value");
Object value = new ValueHolder(list);
assertNull(ReflectionValueExtractor.evaluate("h.value[", value));
assertNull(ReflectionValueExtractor.evaluate("h.value[]", value));
assertNull(ReflectionValueExtractor.evaluate("h.value[a]", value));
assertNull(ReflectionValueExtractor.evaluate("h.value[0", value));
assertNull(ReflectionValueExtractor.evaluate("h.value[0)", value));
assertNull(ReflectionValueExtractor.evaluate("h.value[-1]", value));
}
/**
* <p>testBadMappedSyntax.</p>
*
* @throws Exception if any.
*/
@Test
public void testBadMappedSyntax() throws Exception {
Map<Object, Object> map = new HashMap<Object, Object>();
map.put("a", "a-value");
Object value = new ValueHolder(map);
assertNull(ReflectionValueExtractor.evaluate("h.value(", value));
assertNull(ReflectionValueExtractor.evaluate("h.value()", value));
assertNull(ReflectionValueExtractor.evaluate("h.value(a", value));
assertNull(ReflectionValueExtractor.evaluate("h.value(a]", value));
}
/**
* <p>testIllegalIndexedType.</p>
*
* @throws Exception if any.
*/
@Test
public void testIllegalIndexedType() throws Exception {
try {
ReflectionValueExtractor.evaluate("h.value[1]", new ValueHolder("string"));
} catch (Exception e) {
// TODO assert exception message
}
}
/**
* <p>testIllegalMappedType.</p>
*
* @throws Exception if any.
*/
@Test
public void testIllegalMappedType() throws Exception {
try {
ReflectionValueExtractor.evaluate("h.value(key)", new ValueHolder("string"));
} catch (Exception e) {
// TODO assert exception message
}
}
/**
* <p>testTrimRootToken.</p>
*
* @throws Exception if any.
*/
@Test
public void testTrimRootToken() throws Exception {
assertNull(ReflectionValueExtractor.evaluate("project", project, true));
}
/**
* <p>testArtifactMap.</p>
*
* @throws Exception if any.
*/
@Test
public void testArtifactMap() throws Exception {
assertEquals(
"g0",
((Artifact) ReflectionValueExtractor.evaluate("project.artifactMap(g0:a0:c0)", project)).getGroupId());
assertEquals(
"a1",
((Artifact) ReflectionValueExtractor.evaluate("project.artifactMap(g1:a1:c1)", project))
.getArtifactId());
assertEquals(
"c2",
((Artifact) ReflectionValueExtractor.evaluate("project.artifactMap(g2:a2:c2)", project))
.getClassifier());
}
public static class Artifact {
private String groupId;
private String artifactId;
private String version;
private String extension;
private String classifier;
public Artifact(String groupId, String artifactId, String version, String extension, String classifier) {
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
this.extension = extension;
this.classifier = classifier;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getArtifactId() {
return artifactId;
}
public void setArtifactId(String artifactId) {
this.artifactId = artifactId;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getExtension() {
return extension;
}
public void setExtension(String extension) {
this.extension = extension;
}
public String getClassifier() {
return classifier;
}
public void setClassifier(String classifier) {
this.classifier = classifier;
}
}
public static class Project {
private String modelVersion;
private String groupId;
private Scm scm;
private List<Dependency> dependencies = new ArrayList<>();
private Build build;
private String artifactId;
private String name;
private String version;
private Map<String, Artifact> artifactMap = new HashMap<>();
private String description;
public void setModelVersion(String modelVersion) {
this.modelVersion = modelVersion;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public void setScm(Scm scm) {
this.scm = scm;
}
public void addDependency(Dependency dependency) {
this.dependencies.add(dependency);
}
public void setBuild(Build build) {
this.build = build;
}
public void setArtifactId(String artifactId) {
this.artifactId = artifactId;
}
public void setName(String name) {
this.name = name;
}
public void setVersion(String version) {
this.version = version;
}
public Scm getScm() {
return scm;
}
public String getModelVersion() {
return modelVersion;
}
public String getGroupId() {
return groupId;
}
public List<Dependency> getDependencies() {
return dependencies;
}
public Build getBuild() {
return build;
}
public String getArtifactId() {
return artifactId;
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
public Dependency[] getDependenciesAsArray() {
return getDependencies().toArray(new Dependency[0]);
}
public Map<String, Dependency> getDependenciesAsMap() {
Map<String, Dependency> ret = new HashMap<>();
for (Dependency dep : getDependencies()) {
ret.put(dep.getArtifactId(), dep);
}
return ret;
}
// ${project.artifactMap(g:a:v)}
public void addArtifact(Artifact a) {
artifactMap.put(a.getGroupId() + ":" + a.getArtifactId() + ":" + a.getClassifier(), a);
}
public Map<String, Artifact> getArtifactMap() {
return artifactMap;
}
public void setDescription(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
public static class Build {}
public static class Dependency {
private String artifactId;
public String getArtifactId() {
return artifactId;
}
public void setArtifactId(String id) {
artifactId = id;
}
}
public static class Scm {
private String connection;
public void setConnection(String connection) {
this.connection = connection;
}
public String getConnection() {
return connection;
}
}
public static class ValueHolder {
private final Object value;
public ValueHolder(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
/**
* <p>testRootPropertyRegression.</p>
*
* @throws Exception if any.
*/
@Test
public void testRootPropertyRegression() throws Exception {
Project project = new Project();
project.setDescription("c:\\\\org\\apache\\test");
Object evalued = ReflectionValueExtractor.evaluate("description", project);
assertNotNull(evalued);
}
}