From fd4493580a61d98c7c19d46261827388a2615647 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 22 Sep 2023 08:02:25 +0200 Subject: [PATCH] [MNG-7820] Get rid of plexus-utils introspection classes (#1251) --- .../org/apache/maven/plugin/ClassMap.java | 388 ++++++++++++ .../maven/plugin/IntrospectionException.java | 35 ++ .../org/apache/maven/plugin/MethodMap.java | 389 ++++++++++++ .../PluginParameterExpressionEvaluator.java | 1 - .../PluginParameterExpressionEvaluatorV4.java | 1 - .../plugin/ReflectionValueExtractor.java | 298 +++++++++ ...luginParameterExpressionEvaluatorTest.java | 3 +- ...ginParameterExpressionEvaluatorV4Test.java | 3 +- .../plugin/ReflectionValueExtractorTest.java | 574 ++++++++++++++++++ 9 files changed, 1686 insertions(+), 6 deletions(-) create mode 100644 maven-core/src/main/java/org/apache/maven/plugin/ClassMap.java create mode 100644 maven-core/src/main/java/org/apache/maven/plugin/IntrospectionException.java create mode 100644 maven-core/src/main/java/org/apache/maven/plugin/MethodMap.java create mode 100644 maven-core/src/main/java/org/apache/maven/plugin/ReflectionValueExtractor.java create mode 100644 maven-core/src/test/java/org/apache/maven/plugin/ReflectionValueExtractorTest.java diff --git a/maven-core/src/main/java/org/apache/maven/plugin/ClassMap.java b/maven-core/src/main/java/org/apache/maven/plugin/ClassMap.java new file mode 100644 index 0000000000..f71e6f5003 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/plugin/ClassMap.java @@ -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 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; + } + + /** + *

Find a Method using the methodKey provided.

+ *

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.

+ *

If nothing is found, then we must actually go + * and introspect the method from the MethodMap.

+ * @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; + } + } +} diff --git a/maven-core/src/main/java/org/apache/maven/plugin/IntrospectionException.java b/maven-core/src/main/java/org/apache/maven/plugin/IntrospectionException.java new file mode 100644 index 0000000000..38e2d83441 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/plugin/IntrospectionException.java @@ -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); + } +} diff --git a/maven-core/src/main/java/org/apache/maven/plugin/MethodMap.java b/maven-core/src/main/java/org/apache/maven/plugin/MethodMap.java new file mode 100644 index 0000000000..d86e0800d4 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/plugin/MethodMap.java @@ -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> 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 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 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. + *

+ * 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 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 methods, Class... classes) throws AmbiguousException { + LinkedList 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 maximals = new LinkedList<>(); + + for (Method app : applicables) { + Class[] appArgs = app.getParameterTypes(); + boolean lessSpecific = false; + + for (Iterator 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 getApplicables(List methods, Class... classes) { + LinkedList 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; + } +} diff --git a/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluator.java b/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluator.java index 2cd4b291a5..47bf555345 100644 --- a/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluator.java +++ b/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluator.java @@ -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 ${ and } is evaluated. diff --git a/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4.java b/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4.java index e0aaac34f6..56532bc051 100644 --- a/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4.java +++ b/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4.java @@ -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 ${ and } is evaluated. diff --git a/maven-core/src/main/java/org/apache/maven/plugin/ReflectionValueExtractor.java b/maven-core/src/main/java/org/apache/maven/plugin/ReflectionValueExtractor.java new file mode 100644 index 0000000000..6590ca6099 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/plugin/ReflectionValueExtractor.java @@ -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, 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) : ""; + } + } + + private ReflectionValueExtractor() {} + + /** + *

The implementation supports indexed, nested and mapped properties.

+ *
    + *
  • nested properties should be defined by a dot, i.e. "user.address.street"
  • + *
  • indexed properties (java.util.List or array instance) should be contains (\\w+)\\[(\\d+)\\] + * pattern, i.e. "user.addresses[1].street"
  • + *
  • mapped properties should be contains (\\w+)\\((.+)\\) pattern, + * i.e. "user.addresses(myAddress).street"
  • + *
+ * + * @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); + } + + /** + *

+ * The implementation supports indexed, nested and mapped properties. + *

+ *
    + *
  • nested properties should be defined by a dot, i.e. "user.address.street"
  • + *
  • indexed properties (java.util.List or array instance) should be contains (\\w+)\\[(\\d+)\\] + * pattern, i.e. "user.addresses[1].street"
  • + *
  • mapped properties should be contains (\\w+)\\((.+)\\) pattern, i.e. + * "user.addresses(myAddress).street"
  • + *
+ * + * @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; + } +} diff --git a/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java index 4090be80bf..8bd464057f 100644 --- a/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java +++ b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java @@ -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()); } diff --git a/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java index bcb247dade..8cb9964016 100644 --- a/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java +++ b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java @@ -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()); } diff --git a/maven-core/src/test/java/org/apache/maven/plugin/ReflectionValueExtractorTest.java b/maven-core/src/test/java/org/apache/maven/plugin/ReflectionValueExtractorTest.java new file mode 100644 index 0000000000..d39745a7e4 --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/plugin/ReflectionValueExtractorTest.java @@ -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; + + /** + *

setUp.

+ */ + @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")); + } + + /** + *

testValueExtraction.

+ * + * @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); + } + + /** + *

testValueExtractorWithAInvalidExpression.

+ * + * @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)); + } + + /** + *

testMappedDottedKey.

+ * + * @throws Exception if any. + */ + @Test + public void testMappedDottedKey() throws Exception { + Map map = new HashMap(); + map.put("a.b", "a.b-value"); + + assertEquals("a.b-value", ReflectionValueExtractor.evaluate("h.value(a.b)", new ValueHolder(map))); + } + + /** + *

testIndexedMapped.

+ * + * @throws Exception if any. + */ + @Test + public void testIndexedMapped() throws Exception { + Map map = new HashMap(); + map.put("a", "a-value"); + List list = new ArrayList(); + list.add(map); + + assertEquals("a-value", ReflectionValueExtractor.evaluate("h.value[0](a)", new ValueHolder(list))); + } + + /** + *

testMappedIndexed.

+ * + * @throws Exception if any. + */ + @Test + public void testMappedIndexed() throws Exception { + List list = new ArrayList(); + list.add("a-value"); + Map map = new HashMap(); + map.put("a", list); + assertEquals("a-value", ReflectionValueExtractor.evaluate("h.value(a)[0]", new ValueHolder(map))); + } + + /** + *

testMappedMissingDot.

+ * + * @throws Exception if any. + */ + @Test + public void testMappedMissingDot() throws Exception { + Map map = new HashMap(); + map.put("a", new ValueHolder("a-value")); + assertNull(ReflectionValueExtractor.evaluate("h.value(a)value", new ValueHolder(map))); + } + + /** + *

testIndexedMissingDot.

+ * + * @throws Exception if any. + */ + @Test + public void testIndexedMissingDot() throws Exception { + List list = new ArrayList(); + list.add(new ValueHolder("a-value")); + assertNull(ReflectionValueExtractor.evaluate("h.value[0]value", new ValueHolder(list))); + } + + /** + *

testDotDot.

+ * + * @throws Exception if any. + */ + @Test + public void testDotDot() throws Exception { + assertNull(ReflectionValueExtractor.evaluate("h..value", new ValueHolder("value"))); + } + + /** + *

testBadIndexedSyntax.

+ * + * @throws Exception if any. + */ + @Test + public void testBadIndexedSyntax() throws Exception { + List list = new ArrayList(); + 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)); + } + + /** + *

testBadMappedSyntax.

+ * + * @throws Exception if any. + */ + @Test + public void testBadMappedSyntax() throws Exception { + Map map = new HashMap(); + 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)); + } + + /** + *

testIllegalIndexedType.

+ * + * @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 + } + } + + /** + *

testIllegalMappedType.

+ * + * @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 + } + } + + /** + *

testTrimRootToken.

+ * + * @throws Exception if any. + */ + @Test + public void testTrimRootToken() throws Exception { + assertNull(ReflectionValueExtractor.evaluate("project", project, true)); + } + + /** + *

testArtifactMap.

+ * + * @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 dependencies = new ArrayList<>(); + + private Build build; + + private String artifactId; + + private String name; + + private String version; + + private Map 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 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 getDependenciesAsMap() { + Map 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 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; + } + } + + /** + *

testRootPropertyRegression.

+ * + * @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); + } +}