#233 - PoC for DataBoundConstructor compatibility (#234)

* Enable testing against 2.107.1 in order to reveal potential JEP-200 regressions

* #233 - DataBoundConfigurator now supports legacy constructors

* Fix the merge conflict glitch

* [Issue 233] - Introduce a LegacyDataBoundConstructorProvider to support some JCasC-incompatible API modification cases

* Another merge conflict

* Issue 233 - FindBugs
This commit is contained in:
Oleg Nenashev 2018-06-28 08:26:08 +02:00 committed by Ewelina Wilkosz
parent bdfbf5b1cd
commit 570a901a75
4 changed files with 233 additions and 50 deletions

2
Jenkinsfile vendored
View File

@ -1 +1 @@
buildPlugin()
buildPlugin(jenkinsVersions: [null, "2.107.1"])

View File

@ -1,6 +1,7 @@
package org.jenkinsci.plugins.casc;
import hudson.model.Descriptor;
import javafx.collections.transformation.SortedList;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.casc.model.CNode;
import org.jenkinsci.plugins.casc.model.Mapping;
@ -11,9 +12,12 @@ import javax.annotation.Nonnull;
import javax.annotation.PostConstruct;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import static com.google.common.base.Defaults.defaultValue;
@ -28,7 +32,7 @@ public class DataBoundConfigurator<T> extends BaseConfigurator<T> {
private final static Logger logger = Logger.getLogger(DataBoundConfigurator.class.getName());
private final Class target;
private final Class<T> target;
public DataBoundConfigurator(Class<T> clazz) {
this.target = clazz;
@ -45,59 +49,38 @@ public class DataBoundConfigurator<T> extends BaseConfigurator<T> {
// c can be null for component with no-arg constructor and no extra property to be set
Mapping config = (c != null ? c.asMapping() : Mapping.EMPTY);
final Constructor constructor = getDataBoundConstructor();
final Parameter[] parameters = constructor.getParameters();
final String[] names = ClassDescriptor.loadParameterNames(constructor);
Object[] args = new Object[names.length];
if (parameters.length > 0) {
// Many jenkins components haven't been migrated to @DataBoundSetter vs @NotNull constructor parameters
// as a result it might be valid to reference a describable without parameters
for (int i = 0; i < names.length; i++) {
final CNode value = config.remove(names[i]);
if (value == null && parameters[i].getAnnotation(Nonnull.class) != null) {
throw new ConfiguratorException(names[i] + " is required to configure " + target);
// TODO: The fallback resolution may end up resolving empty constructors
// Ideally we need an annotation or whatever other logic that old constructors are acceptable
final Constructor dataBoundConstructor = getDataBoundConstructor();
T object = null;
try {
logger.log(Level.INFO, "Trying @DataBoundConstructor for target {0}: {1}",
new Object[] {target, dataBoundConstructor} );
object = tryConstructor((Constructor<T>) dataBoundConstructor, config);
} catch (ConfiguratorException ex) {
logger.log(Level.INFO, "Default databound constructor cannot be applied, " +
"will consult with Legacy DataBoundConstructor providers", ex);
for (Constructor constructor : LegacyDataBoundConstructorProvider.getLegacyDataBoundConstructors(target)) {
if (constructor == dataBoundConstructor) {
continue; // Already tried it
}
final Class t = parameters[i].getType();
if (value != null) {
if (Collection.class.isAssignableFrom(t)) {
final Type pt = parameters[i].getParameterizedType();
final Configurator lookup = Configurator.lookup(pt);
logger.log(Level.INFO, "Trying legacy constructor {0} for target {1}",
new Object[] {constructor, target});
final ArrayList<Object> list = new ArrayList<>();
for (CNode o : value.asSequence()) {
list.add(lookup.configure(o));
}
args[i] = list;
try {
object = tryConstructor((Constructor<T>) constructor, config);
} catch (ConfiguratorException ex2) {
logger.log(Level.INFO, "Constructor {0} didn't work for target {1}",
new Object[] {constructor, target});
}
} else {
final Type pt = parameters[i].getParameterizedType();
final Type k = pt != null ? pt : t;
final Configurator configurator = Configurator.lookup(k);
if (configurator == null) throw new IllegalStateException("No configurator implementation to manage "+k);
args[i] = configurator.configure(value);
}
logger.info("Setting " + target + "." + names[i] + " = " + (value.isSensitiveData() ? "****" : value));
} else if (t.isPrimitive()) {
args[i] = defaultValue(t);
if (object != null) {
break;
}
}
}
final Object object;
try {
object = constructor.newInstance(args);
} catch (IllegalArgumentException | InstantiationException | InvocationTargetException | IllegalAccessException ex) {
List<String> argumentTypes = new ArrayList<>(args.length);
for (Object arg : args) {
argumentTypes.add(arg != null ? arg.getClass().getName() : "null");
}
throw new ConfiguratorException(this,
"Failed to construct instance of " + target +
".\n Constructor: " + constructor.toString() +
".\n Arguments: " + argumentTypes, ex);
if (object == null) {
throw new ConfiguratorException("Failed to find a compatible constructor for target " + target);
}
final Set<Attribute> attributes = describe();
@ -136,7 +119,64 @@ public class DataBoundConfigurator<T> extends BaseConfigurator<T> {
}
}
return (T) object;
return object;
}
private T tryConstructor(Constructor<T> constructor, Mapping config) throws ConfiguratorException {
final Parameter[] parameters = constructor.getParameters();
final String[] names = ClassDescriptor.loadParameterNames(constructor);
Object[] args = new Object[names.length];
if (parameters.length > 0) {
// Many jenkins components haven't been migrated to @DataBoundSetter vs @NotNull constructor parameters
// as a result it might be valid to reference a describable without parameters
for (int i = 0; i < names.length; i++) {
final CNode value = config.remove(names[i]);
if (value == null && parameters[i].getAnnotation(Nonnull.class) != null) {
throw new ConfiguratorException(names[i] + " is required to configure " + target);
}
final Class t = parameters[i].getType();
if (value != null) {
if (Collection.class.isAssignableFrom(t)) {
final Type pt = parameters[i].getParameterizedType();
final Configurator lookup = Configurator.lookup(pt);
final ArrayList<Object> list = new ArrayList<>();
for (CNode o : value.asSequence()) {
list.add(lookup.configure(o));
}
args[i] = list;
} else {
final Type pt = parameters[i].getParameterizedType();
final Type k = pt != null ? pt : t;
final Configurator configurator = Configurator.lookup(k);
if (configurator == null) throw new ConfiguratorException("No configurator implementation to manage "+k);
args[i] = configurator.configure(value);
}
logger.info("Setting " + target + "." + names[i] + " = " + (value.isSensitiveData() ? "****" : value));
} else if (t.isPrimitive()) {
args[i] = defaultValue(t);
}
}
}
final T object;
try {
object = constructor.newInstance(args);
} catch (IllegalArgumentException | InstantiationException | InvocationTargetException | IllegalAccessException ex) {
List<String> argumentTypes = new ArrayList<>(args.length);
for (Object arg : args) {
argumentTypes.add(arg != null ? arg.getClass().getName() : "null");
}
throw new ConfiguratorException(this,
"Failed to construct instance of " + target +
".\n Constructor: " + constructor.toString() +
".\n Arguments: " + argumentTypes, ex);
}
return object;
}
public String getName() {

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) 2018 CloudBees, Inc.
*
* 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.casc;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
import javax.annotation.Nonnull;
import java.lang.reflect.Constructor;
import java.util.HashSet;
import java.util.Set;
/**
* Provides resolution logic for Legacy {@link org.kohsuke.stapler.DataBoundConstructor}s.
* This extension point exists to support explicit specifications of compatible constructors if
* the annotated one does not work.
*
* This extension point can be used as a workaround for some cases when there
* is a need to modify {@link org.kohsuke.stapler.DataBoundConstructor} in
* a way not compatible with JCasC (or to support existing modifications).
* It <b>should not</b> be used to defined for defining custom initialization logic,
* new {@link Configurator}s should be created instead.
*
* @see DataBoundConfigurator
* @author Oleg Nenashev
* @since TODO
*/
@Restricted(Beta.class)
public abstract class LegacyDataBoundConstructorProvider<TTarget> implements ExtensionPoint {
@Nonnull
public abstract Set<Constructor<TTarget>> getConstructorsFor(@Nonnull Class<?> clazz);
/**
* Locates legacy {@link org.kohsuke.stapler.DataBoundConstructor}s
* @param targetClazz Target class which should be produced by constructors
* @param <T> Target class which should be produced by constructors
* @return List of Legacy constructors defined by extension points.
* The list is ordered depending on Extension point ordinals.
*/
@Nonnull
public static <T> Set<Constructor<T>> getLegacyDataBoundConstructors(@Nonnull Class<T> targetClazz) {
HashSet<Constructor<T>> constructors = new HashSet<>();
for (LegacyDataBoundConstructorProvider<?> provider : all()) {
final Set<? extends Constructor<?>> provided = provider.getConstructorsFor(targetClazz);
for (Constructor<?> pr : provided) {
if (targetClazz.isAssignableFrom(pr.getDeclaringClass())) {
constructors.add((Constructor<T>) pr);
} else {
throw new IllegalStateException("Extension " + provider + " provided a wrong constructor type for " + targetClazz);
}
}
}
return constructors;
}
@Nonnull
public static ExtensionList<LegacyDataBoundConstructorProvider> all() {
return ExtensionList.lookup(LegacyDataBoundConstructorProvider.class);
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2018 CloudBees, Inc.
*
* 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.casc.core;
import hudson.Extension;
import hudson.slaves.JNLPLauncher;
import org.jenkinsci.plugins.casc.LegacyDataBoundConstructorProvider;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import javax.annotation.Nonnull;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.Set;
/**
* Provides information about Legacy constructors for {@link JNLPLauncher}.
* @see org.jenkinsci.plugins.casc.DataBoundConfigurator
* @author Oleg Nenashev
* @since TODO
*/
@Extension(optional = true)
@Restricted(NoExternalUse.class)
public class JNLPLauncherLegacyConstructorProvider
extends LegacyDataBoundConstructorProvider<JNLPLauncher> {
@Nonnull
@Override
public Set<Constructor<JNLPLauncher>> getConstructorsFor(@Nonnull Class<?> clazz) {
if (!clazz.equals(JNLPLauncher.class)) {
return Collections.emptySet();
}
try {
return Collections.singleton(JNLPLauncher.class.getConstructor(
String.class, String.class));
} catch (NoSuchMethodException e) {
// Deprecated Method has been removed or so, we do not care here
return Collections.emptySet();
}
}
}