org.jenkinsci.plugins.workflow.structs.DescribableHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.jenkinsci.plugins.workflow.structs.DescribableHelper.java

Source

/*
 * The MIT License
 *
 * Copyright 2014 Jesse Glick.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.jenkinsci.plugins.workflow.structs;

import hudson.Extension;
import com.google.common.primitives.Primitives;
import hudson.Util;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.ParameterDefinition;
import hudson.model.ParameterValue;
import hudson.model.ParametersDefinitionProperty;
import java.beans.Introspector;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import net.java.sezpoz.Index;
import net.java.sezpoz.IndexItem;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ObjectUtils;
import org.codehaus.groovy.reflection.ReflectionCache;
import org.kohsuke.stapler.ClassDescriptor;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.NoStaplerConstructorException;
import org.kohsuke.stapler.lang.Klass;

/**
 * Utility for converting between {@link Describable}s (and some other objects) and map-like representations.
 * Ultimately should live in Jenkins core (or Stapler).
 */
public class DescribableHelper {

    private static final Logger LOG = Logger.getLogger(DescribableHelper.class.getName());

    /**
     * Creates an instance of a class via {@link DataBoundConstructor} and {@link DataBoundSetter}.
     * <p>The arguments may be primitives (as wrappers) or {@link String}s if that is their declared type.
     * {@link Character}s, {@link Enum}s, and {@link URL}s may be represented by {@link String}s.
     * Other object types may be passed in raw? as well, but JSON-like structures are encouraged instead.
     * Specifically a {@link List} may be used to represent any list- or array-valued argument.
     * A {@link Map} with {@link String} keys may be used to represent any class which is itself data-bound.
     * In that case the special key {@link #CLAZZ} is used to specify the {@link Class#getName};
     * or it may be omitted if the argument is declared to take a concrete type;
     * or {@link Class#getSimpleName} may be used in case the argument type is {@link Describable}
     * and only one subtype is registered (as a {@link Descriptor}) with that simple name.
     */
    public static <T> T instantiate(Class<? extends T> clazz, Map<String, ?> arguments) throws Exception {
        String[] names = loadConstructorParamNames(clazz);
        Constructor<T> c = findConstructor(clazz, names.length);
        Object[] args = buildArguments(clazz, arguments, c.getGenericParameterTypes(), names, true);
        T o = c.newInstance(args);
        injectSetters(o, arguments);
        return o;
    }

    /**
     * Computes arguments suitable to pass to {@link #instantiate} to reconstruct this object.
     * @param o a data-bound object
     * @return constructor and/or setter parameters
     * @throws UnsupportedOperationException if the class does not follow the expected structure
     */
    public static Map<String, Object> uninstantiate(Object o) throws UnsupportedOperationException {
        Class<?> clazz = o.getClass();
        Map<String, Object> r = new TreeMap<String, Object>();
        String[] names;
        try {
            names = loadConstructorParamNames(clazz);
        } catch (NoStaplerConstructorException x) {
            throw new UnsupportedOperationException(x);
        }
        for (String name : names) {
            inspect(r, o, clazz, name);
        }
        r.values().removeAll(Collections.singleton(null));
        Map<String, Object> constructorOnlyDataBoundProps = new TreeMap<String, Object>(r);
        List<String> dataBoundSetters = new ArrayList<String>();
        for (Class<?> c = clazz; c != null; c = c.getSuperclass()) {
            for (Field f : c.getDeclaredFields()) {
                if (f.isAnnotationPresent(DataBoundSetter.class)) {
                    String field = f.getName();
                    dataBoundSetters.add(field);
                    inspect(r, o, clazz, field);
                }
            }
            for (Method m : c.getDeclaredMethods()) {
                if (m.isAnnotationPresent(DataBoundSetter.class) && m.getName().startsWith("set")) {
                    String field = Introspector.decapitalize(m.getName().substring(3));
                    dataBoundSetters.add(field);
                    inspect(r, o, clazz, field);
                }
            }
        }
        clearDefaultSetters(clazz, r, constructorOnlyDataBoundProps, dataBoundSetters);
        return r;
    }

    /**
     * Loads a definition of the structure of a class: what kind of data you might get back from {@link #uninstantiate} on an instance,
     * or might want to pass to {@link #instantiate}.
     */
    public static Schema schemaFor(Class<?> clazz) {
        return new Schema(clazz);
    }

    /**
     * Definition of how a particular class may be configured.
     */
    public static final class Schema {

        private final Class<?> type;
        private final Map<String, ParameterType> parameters;
        private final List<String> mandatoryParameters;

        Schema(Class<?> clazz) {
            this.type = clazz;
            mandatoryParameters = new ArrayList<String>();
            parameters = new TreeMap<String, ParameterType>();
            String[] names = loadConstructorParamNames(clazz);
            Type[] types = findConstructor(clazz, names.length).getGenericParameterTypes();
            for (int i = 0; i < names.length; i++) {
                mandatoryParameters.add(names[i]);
                parameters.put(names[i], ParameterType.of(types[i]));
            }
            for (Class<?> c = clazz; c != null; c = c.getSuperclass()) {
                for (Field f : c.getDeclaredFields()) {
                    if (f.isAnnotationPresent(DataBoundSetter.class)) {
                        f.setAccessible(true);
                        parameters.put(f.getName(), ParameterType.of(f.getGenericType()));
                    }
                }
                for (Method m : c.getDeclaredMethods()) {
                    if (m.isAnnotationPresent(DataBoundSetter.class)) {
                        Type[] parameterTypes = m.getGenericParameterTypes();
                        if (!m.getName().startsWith("set") || parameterTypes.length != 1) {
                            throw new IllegalStateException(m + " cannot be a @DataBoundSetter");
                        }
                        m.setAccessible(true);
                        parameters.put(Introspector.decapitalize(m.getName().substring(3)),
                                ParameterType.of(m.getGenericParameterTypes()[0]));
                    }
                }
            }
        }

        /**
         * A concrete class, usually {@link Describable}.
         */
        public Class<?> getType() {
            return type;
        }

        /**
         * A map from parameter names to types.
         * A parameter name is either the name of an argument to a {@link DataBoundConstructor},
         * or the JavaBeans property name corresponding to a {@link DataBoundSetter}.
         */
        public Map<String, ParameterType> parameters() {
            return parameters;
        }

        /**
         * Mandatory (constructor) parameters, in order.
         * Parameters at the end of the list may be omitted, in which case they are assumed to be null or some other default value
         * (in these cases it would be better to use {@link DataBoundSetter} on the type definition).
         * Will be keys in {@link #parameters}.
         */
        public List<String> mandatoryParameters() {
            return mandatoryParameters;
        }

        /**
         * Corresponds to {@link Descriptor#getDisplayName} where available.
         */
        public String getDisplayName() {
            for (Descriptor<?> d : getDescriptorList()) {
                if (d.clazz == type) {
                    return d.getDisplayName();
                }
            }
            return type.getSimpleName();
        }

        /**
         * Loads help defined for this object as a whole or one of its parameters.
         * Note that you may need to use {@link Util#replaceMacro(String, Map)}
         * to replace {@code ${rootURL}} with some other value.
         * @param parameter if specified, one of {@link #parameters}; else for the whole object
         * @return some HTML (in English locale), if available, else null
         * @see Descriptor#doHelp
         */
        public @CheckForNull String getHelp(@CheckForNull String parameter) throws IOException {
            for (Klass<?> c = Klass.java(type); c != null; c = c.getSuperClass()) {
                URL u = c.getResource(parameter == null ? "help.html" : "help-" + parameter + ".html");
                if (u != null) {
                    return IOUtils.toString(u, "UTF-8");
                }
            }
            return null;
        }

        @Override
        public String toString() {
            StringBuilder b = new StringBuilder("(");
            boolean first = true;
            Map<String, ParameterType> params = new TreeMap<String, ParameterType>(parameters());
            for (String param : mandatoryParameters()) {
                if (first) {
                    first = false;
                } else {
                    b.append(", ");
                }
                b.append(param).append(": ").append(params.remove(param));
            }
            for (Map.Entry<String, ParameterType> entry : params.entrySet()) {
                if (first) {
                    first = false;
                } else {
                    b.append(", ");
                }
                b.append('[').append(entry.getKey()).append(": ").append(entry.getValue()).append(']');
            }
            return b.append(')').toString();
        }

    }

    /**
     * A type of a parameter to a class.
     */
    public static abstract class ParameterType {
        @Nonnull
        private final Type actualType;

        public Type getActualType() {
            return actualType;
        }

        ParameterType(Type actualType) {
            this.actualType = actualType;
        }

        static ParameterType of(Type type) {
            try {
                if (type instanceof Class) {
                    Class<?> c = (Class<?>) type;
                    if (c == String.class || Primitives.unwrap(c).isPrimitive()) {
                        return new AtomicType(c);
                    }
                    if (Enum.class.isAssignableFrom(c)) {
                        List<String> constants = new ArrayList<String>();
                        for (Enum<?> value : c.asSubclass(Enum.class).getEnumConstants()) {
                            constants.add(value.name());
                        }
                        return new EnumType(c, constants.toArray(new String[constants.size()]));
                    }
                    if (c == URL.class) {
                        return new AtomicType(String.class);
                    }
                    if (c.isArray()) {
                        return new ArrayType(c);
                    }
                    // Assume it is a nested object of some sort.
                    Set<Class<?>> subtypes = findSubtypes(c);
                    if ((subtypes.isEmpty() && !Modifier.isAbstract(c.getModifiers()))
                            || subtypes.equals(Collections.singleton(c))) {
                        // Probably homogeneous. (Might be concrete but subclassable.)
                        return new HomogeneousObjectType(c);
                    } else {
                        // Definitely heterogeneous.
                        Map<String, List<Class<?>>> subtypesBySimpleName = new HashMap<String, List<Class<?>>>();
                        for (Class<?> subtype : subtypes) {
                            String simpleName = subtype.getSimpleName();
                            List<Class<?>> bySimpleName = subtypesBySimpleName.get(simpleName);
                            if (bySimpleName == null) {
                                subtypesBySimpleName.put(simpleName, bySimpleName = new ArrayList<Class<?>>());
                            }
                            bySimpleName.add(subtype);
                        }
                        Map<String, Schema> types = new TreeMap<String, Schema>();
                        for (Map.Entry<String, List<Class<?>>> entry : subtypesBySimpleName.entrySet()) {
                            if (entry.getValue().size() == 1) { // normal case: unambiguous via simple name
                                try {
                                    types.put(entry.getKey(), schemaFor(entry.getValue().get(0)));
                                } catch (Exception x) {
                                    LOG.log(Level.FINE, "skipping subtype", x);
                                }
                            } else { // have to diambiguate via FQN
                                for (Class<?> subtype : entry.getValue()) {
                                    try {
                                        types.put(subtype.getName(), schemaFor(subtype));
                                    } catch (Exception x) {
                                        LOG.log(Level.FINE, "skipping subtype", x);
                                    }
                                }
                            }
                        }
                        return new HeterogeneousObjectType(c, types);
                    }
                }
                if (acceptsList(type)) {
                    return new ArrayType(type, of(((ParameterizedType) type).getActualTypeArguments()[0]));
                }
                throw new UnsupportedOperationException("do not know how to categorize attributes of type " + type);
            } catch (Exception x) {
                return new ErrorType(x, type);
            }
        }
    }

    public static final class AtomicType extends ParameterType {
        AtomicType(Class<?> clazz) {
            super(clazz);
        }

        public Class<?> getType() {
            return (Class) getActualType();
        }

        @Override
        public String toString() {
            return Primitives.unwrap((Class) getActualType()).getSimpleName();
        }
    }

    public static final class EnumType extends ParameterType {
        private final String[] values;

        EnumType(Class<?> clazz, String[] values) {
            super(clazz);
            this.values = values;
        }

        public Class<?> getType() {
            return (Class) getActualType();
        }

        /**
         * A list of enumeration values.
         */
        public String[] getValues() {
            return values.clone();
        }

        @Override
        public String toString() {
            return ((Class) getActualType()).getSimpleName() + Arrays.toString(values);
        }
    }

    public static final class ArrayType extends ParameterType {
        private final ParameterType elementType;

        ArrayType(Class<?> actualClass) {
            this(actualClass, of(actualClass.getComponentType()));
        }

        ArrayType(Type actualClass, ParameterType elementType) {
            super(actualClass);
            this.elementType = elementType;
        }

        /**
         * The element type of the array or list.
         */
        public ParameterType getElementType() {
            return elementType;
        }

        @Override
        public String toString() {
            return elementType + "[]";
        }
    }

    public static final class HomogeneousObjectType extends ParameterType {
        private final Schema type;

        HomogeneousObjectType(Class<?> actualClass) {
            super(actualClass);
            this.type = schemaFor(actualClass);
        }

        public Class<?> getType() {
            return (Class) getActualType();
        }

        /**
         * The schema representing a type of nested object.
         */
        public Schema getSchemaType() {
            return type;
        }

        /**
         * The actual class underlying the type.
         */
        @Override
        public String toString() {
            return type.getType().getSimpleName() + type;
        }
    }

    /**
     * A parameter (or array element) which could take any of the indicated concrete object types.
     */
    public static final class HeterogeneousObjectType extends ParameterType {
        private final Map<String, Schema> types;

        HeterogeneousObjectType(Class<?> supertype, Map<String, Schema> types) {
            super(supertype);
            this.types = types;
        }

        public Class<?> getType() {
            return (Class) getActualType();
        }

        /**
         * A map from names which could be passed to {@link #CLAZZ} to types of allowable nested objects.
         */
        public Map<String, Schema> getTypes() {
            return types;
        }

        @Override
        public String toString() {
            return getType().getSimpleName() + types;
        }
    }

    public static final class ErrorType extends ParameterType {
        private final Exception error;

        ErrorType(Exception error, Type type) {
            super(type);
            LOG.log(Level.FINE, null, error);
            this.error = error;
        }

        public Exception getError() {
            return error;
        }

        @Override
        public String toString() {
            return error.toString();
        }
    }

    /**
     * Removes configuration of any properties based on {@link DataBoundSetter} which appear unmodified from the default.
     * @param clazz the class of {@code o}
     * @param allDataBoundProps all its properties, including those from its {@link DataBoundConstructor} as well as any {@link DataBoundSetter}s; some of the latter might be deleted
     * @param constructorOnlyDataBoundProps properties from {@link DataBoundConstructor} only
     * @param dataBoundSetters a list of property names marked with {@link DataBoundSetter}
     */
    private static void clearDefaultSetters(Class<?> clazz, Map<String, Object> allDataBoundProps,
            Map<String, Object> constructorOnlyDataBoundProps, Collection<String> dataBoundSetters) {
        if (dataBoundSetters.isEmpty()) {
            return;
        }
        Object control;
        try {
            control = instantiate(clazz, constructorOnlyDataBoundProps);
        } catch (Exception x) {
            LOG.log(Level.WARNING,
                    "Cannot create control version of " + clazz + " using " + constructorOnlyDataBoundProps, x);
            return;
        }
        Map<String, Object> fromControl = new HashMap<String, Object>(constructorOnlyDataBoundProps);
        Iterator<String> fields = dataBoundSetters.iterator();
        while (fields.hasNext()) {
            String field = fields.next();
            try {
                inspect(fromControl, control, clazz, field);
            } catch (RuntimeException x) {
                LOG.log(Level.WARNING, "Failed to check property " + field + " of " + clazz + " on " + control, x);
                fields.remove();
            }
        }
        for (String field : dataBoundSetters) {
            if (ObjectUtils.equals(allDataBoundProps.get(field), fromControl.get(field))) {
                allDataBoundProps.remove(field);
            }
        }
    }

    public static final String CLAZZ = "$class";

    private static Object[] buildArguments(Class<?> clazz, Map<String, ?> arguments, Type[] types, String[] names,
            boolean callEvenIfNoArgs) throws Exception {
        Object[] args = new Object[names.length];
        boolean hasArg = callEvenIfNoArgs;
        for (int i = 0; i < args.length; i++) {
            String name = names[i];
            hasArg |= arguments.containsKey(name);
            Object a = arguments.get(name);
            Type type = types[i];
            if (a != null) {
                args[i] = coerce(clazz.getName() + "." + name, type, a);
            } else if (type == boolean.class) {
                args[i] = false;
            } else if (type instanceof Class && ((Class) type).isPrimitive() && callEvenIfNoArgs) {
                throw new UnsupportedOperationException("not yet handling @DataBoundConstructor default value of "
                        + type + "; pass an explicit value for " + name);
            } else {
                // TODO this might be fine (ExecutorStep.label), or not (GenericSCMStep.scm); should inspect parameter annotations for @Nonnull and throw an UOE if found
            }
        }
        return hasArg ? args : null;
    }

    @SuppressWarnings("unchecked")
    private static Object coerce(String context, Type type, @Nonnull Object o) throws Exception {
        if (type instanceof Class) {
            o = ReflectionCache.getCachedClass((Class) type).coerceArgument(o);
        }
        if (type instanceof Class && Primitives.wrap((Class) type).isInstance(o)) {
            return o;
        } else if (o instanceof Map) {
            Map<String, Object> m = new HashMap<String, Object>();
            for (Map.Entry<?, ?> entry : ((Map<?, ?>) o).entrySet()) {
                m.put((String) entry.getKey(), entry.getValue());
            }

            String clazzS = (String) m.remove(CLAZZ);
            Class<?> clazz;
            if (clazzS == null) {
                if (Modifier.isAbstract(((Class) type).getModifiers())) {
                    throw new UnsupportedOperationException(
                            "must specify " + CLAZZ + " with an implementation of " + type);
                }
                clazz = (Class) type;
            } else if (clazzS.contains(".")) {
                Jenkins j = Jenkins.getInstance();
                ClassLoader loader = j != null ? j.getPluginManager().uberClassLoader
                        : DescribableHelper.class.getClassLoader();
                clazz = loader.loadClass(clazzS);
            } else if (type instanceof Class) {
                clazz = null;
                for (Class<?> c : findSubtypes((Class<?>) type)) {
                    if (c.getSimpleName().equals(clazzS)) {
                        if (clazz != null) {
                            throw new UnsupportedOperationException(clazzS + " as a " + type + " could mean either "
                                    + clazz.getName() + " or " + c.getName());
                        }
                        clazz = c;
                    }
                }
                if (clazz == null) {
                    throw new UnsupportedOperationException(
                            "no known implementation of " + type + " is named " + clazzS);
                }
            } else {
                throw new UnsupportedOperationException("JENKINS-26535: do not know how to handle " + type);
            }
            return instantiate(clazz.asSubclass((Class<?>) type), m);
        } else if (o instanceof String && type instanceof Class && ((Class) type).isEnum()) {
            return Enum.valueOf(((Class) type).asSubclass(Enum.class), (String) o);
        } else if (o instanceof String && type == URL.class) {
            return new URL((String) o);
        } else if (o instanceof String && (type == char.class || type == Character.class)
                && ((String) o).length() == 1) {
            return ((String) o).charAt(0);
        } else if (o instanceof List && type instanceof Class && ((Class) type).isArray()) {
            Class<?> componentType = ((Class) type).getComponentType();
            List<Object> list = mapList(context, componentType, (List) o);
            return list.toArray((Object[]) Array.newInstance(componentType, list.size()));
        } else if (o instanceof List && acceptsList(type)) {
            return mapList(context, ((ParameterizedType) type).getActualTypeArguments()[0], (List) o);
        } else {
            throw new ClassCastException(context + " expects " + type + " but received " + o.getClass());
        }
    }

    /** Whether this type is generic of {@link List} or a supertype thereof (such as {@link Collection}). */
    @SuppressWarnings("unchecked")
    private static boolean acceptsList(Type type) {
        return type instanceof ParameterizedType && ((ParameterizedType) type).getRawType() instanceof Class
                && ((Class) ((ParameterizedType) type).getRawType()).isAssignableFrom(List.class);
    }

    private static List<Object> mapList(String context, Type type, List<?> list) throws Exception {
        List<Object> r = new ArrayList<Object>();
        for (Object elt : list) {
            r.add(coerce(context, type, elt));
        }
        return r;
    }

    private static String[] loadConstructorParamNames(Class<?> clazz) {
        if (clazz == ParametersDefinitionProperty.class) { // TODO pending core fix
            return new String[] { "parameterDefinitions" };
        }
        return new ClassDescriptor(clazz).loadConstructorParamNames();
    }

    // adapted from RequestImpl
    @SuppressWarnings("unchecked")
    private static <T> Constructor<T> findConstructor(Class<? extends T> clazz, int length) {
        try { // may work without this, but only if the JVM happens to return the right overload first
            if (clazz == ParametersDefinitionProperty.class && length == 1) { // TODO pending core fix
                return (Constructor<T>) ParametersDefinitionProperty.class.getConstructor(List.class);
            }
        } catch (NoSuchMethodException x) {
            throw new AssertionError(x);
        }
        Constructor<T>[] ctrs = (Constructor<T>[]) clazz.getConstructors();
        for (Constructor<T> c : ctrs) {
            if (c.getAnnotation(DataBoundConstructor.class) != null) {
                if (c.getParameterTypes().length != length) {
                    throw new IllegalArgumentException(c
                            + " has @DataBoundConstructor but it doesn't match with your .stapler file. Try clean rebuild");
                }
                return c;
            }
        }
        for (Constructor<T> c : ctrs) {
            if (c.getParameterTypes().length == length) {
                return c;
            }
        }
        throw new IllegalArgumentException(clazz + " does not have a constructor with " + length + " arguments");
    }

    /**
     * Injects via {@link DataBoundSetter}
     */
    private static void injectSetters(Object o, Map<String, ?> arguments) throws Exception {
        for (Class<?> c = o.getClass(); c != null; c = c.getSuperclass()) {
            for (Field f : c.getDeclaredFields()) {
                if (f.isAnnotationPresent(DataBoundSetter.class)) {
                    f.setAccessible(true);
                    if (arguments.containsKey(f.getName())) {
                        Object v = arguments.get(f.getName());
                        f.set(o, v != null ? coerce(c.getName() + "." + f.getName(), f.getType(), v) : null);
                    }
                }
            }
            for (Method m : c.getDeclaredMethods()) {
                if (m.isAnnotationPresent(DataBoundSetter.class)) {
                    Type[] parameterTypes = m.getGenericParameterTypes();
                    if (!m.getName().startsWith("set") || parameterTypes.length != 1) {
                        throw new IllegalStateException(m + " cannot be a @DataBoundSetter");
                    }
                    m.setAccessible(true);
                    Object[] args = buildArguments(c, arguments, parameterTypes,
                            new String[] { Introspector.decapitalize(m.getName().substring(3)) }, false);
                    if (args != null) {
                        m.invoke(o, args);
                    }
                }
            }
        }
    }

    private static void inspect(Map<String, Object> r, Object o, Class<?> clazz, String field) {
        AtomicReference<Type> type = new AtomicReference<Type>();
        Object value = inspect(o, clazz, field, type);
        try {
            String[] names = loadConstructorParamNames(clazz);
            int idx = Arrays.asList(names).indexOf(field);
            if (idx >= 0) {
                Type ctorType = findConstructor(clazz, names.length).getGenericParameterTypes()[idx];
                if (!type.get().equals(ctorType)) {
                    LOG.log(Level.WARNING,
                            "For {0}.{1}, preferring constructor type {2} to differing getter type {3}",
                            new Object[] { clazz.getName(), field, ctorType, type });
                    type.set(ctorType);
                }
            }
        } catch (IllegalArgumentException x) {
            // From loadConstructorParamNames or findConstructor; ignore
        }
        r.put(field, uncoerce(value, type.get()));
    }

    private static Object uncoerce(Object o, Type type) {
        if (type instanceof Class && ((Class) type).isEnum() && o instanceof Enum) {
            return ((Enum) o).name();
        } else if (type == URL.class && o instanceof URL) {
            return ((URL) o).toString();
        } else if ((type == Character.class || type == char.class) && o instanceof Character) {
            return ((Character) o).toString();
        } else if (o instanceof Object[]) {
            List<Object> list = new ArrayList<Object>();
            Object[] array = (Object[]) o;
            for (Object elt : array) {
                list.add(uncoerce(elt, array.getClass().getComponentType()));
            }
            return list;
        } else if (o instanceof List && acceptsList(type)) {
            List<Object> list = new ArrayList<Object>();
            for (Object elt : (List<?>) o) {
                list.add(uncoerce(elt, ((ParameterizedType) type).getActualTypeArguments()[0]));
            }
            return list;
        } else if (o != null && !o.getClass().getName().startsWith("java.")) {
            try {
                // Check to see if this can be treated as a data-bound struct.
                Map<String, Object> nested = uninstantiate(o);
                if (type != o.getClass()) {
                    nested.put(CLAZZ, o.getClass().getSimpleName());
                }
                return nested;
            } catch (UnsupportedOperationException x) {
                // then leave it raw
                if (!(x.getCause() instanceof NoStaplerConstructorException)) {
                    LOG.log(Level.WARNING, "failed to uncoerce " + o, x);
                }
            }
        }
        return o;
    }

    private static Object inspect(Object o, Class<?> clazz, String field, AtomicReference<Type> type) {
        try {
            try {
                Field f = clazz.getField(field);
                type.set(f.getGenericType());
                return f.get(o);
            } catch (NoSuchFieldException x) {
                // OK, check for getter instead
            }
            try {
                Method m = clazz.getMethod("get" + Character.toUpperCase(field.charAt(0)) + field.substring(1));
                type.set(m.getGenericReturnType());
                return m.invoke(o);
            } catch (NoSuchMethodException x) {
                // one more check
            }
            try {
                type.set(boolean.class);
                return clazz.getMethod("is" + Character.toUpperCase(field.charAt(0)) + field.substring(1))
                        .invoke(o);
            } catch (NoSuchMethodException x) {
                throw new UnsupportedOperationException(
                        "no public field " + field + " (or getter method) found in " + clazz);
            }
        } catch (UnsupportedOperationException x) {
            throw x;
        } catch (Exception x) {
            throw new UnsupportedOperationException(x);
        }
    }

    static Set<Class<?>> findSubtypes(Class<?> supertype) {
        Set<Class<?>> clazzes = new HashSet<Class<?>>();
        for (Descriptor<?> d : getDescriptorList()) {
            if (supertype.isAssignableFrom(d.clazz)) {
                clazzes.add(d.clazz);
            }
        }
        if (supertype == ParameterValue.class) { // TODO JENKINS-26093 hack, pending core change
            for (Class<?> d : findSubtypes(ParameterDefinition.class)) {
                String name = d.getName();
                if (name.endsWith("Definition")) {
                    try {
                        Class<?> c = d.getClassLoader().loadClass(name.replaceFirst("Definition$", "Value"));
                        if (supertype.isAssignableFrom(c)) {
                            clazzes.add(c);
                        }
                    } catch (ClassNotFoundException x) {
                        // ignore
                    }
                }
            }
        }
        return clazzes;
    }

    @SuppressWarnings("rawtypes")
    private static List<? extends Descriptor> getDescriptorList() {
        Jenkins j = Jenkins.getInstance();
        if (j != null) {
            // Jenkins.getDescriptorList does not work well since it is limited to descriptors declaring one supertype, and does not work at all for SimpleBuildStep.
            return j.getExtensionList(Descriptor.class);
        } else {
            // TODO should be part of ExtensionList.lookup in core, but here now for benefit of tests:
            List<Descriptor<?>> descriptors = new ArrayList<Descriptor<?>>();
            for (IndexItem<Extension, Object> item : Index.load(Extension.class, Object.class)) {
                try {
                    Object o = item.instance();
                    if (o instanceof Descriptor) {
                        descriptors.add((Descriptor) o);
                    }
                } catch (InstantiationException x) {
                    // ignore for now
                }
            }
            return descriptors;
        }
    }

    private DescribableHelper() {
    }

}