diff --git a/core/pom.xml b/core/pom.xml index 2f47ec98e..1f4639145 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -23,11 +23,6 @@ 20110809 true - - commons-beanutils - commons-beanutils - 1.11.0 - commons-codec commons-codec @@ -105,6 +100,17 @@ json-lib 2.4-jenkins-15 + + org.springframework + spring-beans + 6.2.10 + + + org.springframework + spring-jcl + + + jakarta.servlet jakarta.servlet-api diff --git a/core/src/main/java/org/kohsuke/stapler/AcceptHeader.java b/core/src/main/java/org/kohsuke/stapler/AcceptHeader.java index 63825adb8..4c0a8e97a 100644 --- a/core/src/main/java/org/kohsuke/stapler/AcceptHeader.java +++ b/core/src/main/java/org/kohsuke/stapler/AcceptHeader.java @@ -30,7 +30,7 @@ of this software and associated documentation files (the "Software"), to deal import java.util.List; import java.util.Map; import java.util.stream.Stream; -import org.apache.commons.beanutils.Converter; +import org.springframework.core.convert.converter.Converter; /** * Represents the {@code Accept} HTTP header and help server choose the right media type to serve. @@ -234,10 +234,10 @@ public String toString() { } // this performs databinding for @Header parameter injection - public static class StaplerConverterImpl implements Converter { + public static class StaplerConverterImpl implements Converter { @Override - public Object convert(Class type, Object value) { - return new AcceptHeader(value.toString()); + public Object convert(String source) { + return new AcceptHeader(source); } } } diff --git a/core/src/main/java/org/kohsuke/stapler/AnnotationHandler.java b/core/src/main/java/org/kohsuke/stapler/AnnotationHandler.java index feb3abaa6..0f807ff16 100644 --- a/core/src/main/java/org/kohsuke/stapler/AnnotationHandler.java +++ b/core/src/main/java/org/kohsuke/stapler/AnnotationHandler.java @@ -27,7 +27,7 @@ import jakarta.servlet.ServletException; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; -import org.apache.commons.beanutils.Converter; +import org.springframework.core.convert.converter.Converter; /** * Handles stapler parameter annotations by determining what values to inject for a method call. @@ -100,12 +100,12 @@ public Object parse(StaplerRequest request, T a, Class type, String parameterNam * from String. */ protected final Object convert(Class targetType, String value) { - Converter converter = Stapler.lookupConverter(targetType); + Converter converter = Stapler.lookupConverter(targetType); if (converter == null) { throw new IllegalArgumentException("Unable to convert to " + targetType); } - return converter.convert(targetType, value); + return converter.convert(value); } static Object handle(StaplerRequest2 request, Annotation[] annotations, String parameterName, Class targetType) diff --git a/core/src/main/java/org/kohsuke/stapler/RequestImpl.java b/core/src/main/java/org/kohsuke/stapler/RequestImpl.java index 51c288b61..95a465deb 100644 --- a/core/src/main/java/org/kohsuke/stapler/RequestImpl.java +++ b/core/src/main/java/org/kohsuke/stapler/RequestImpl.java @@ -65,10 +65,6 @@ import net.sf.json.JSONException; import net.sf.json.JSONNull; import net.sf.json.JSONObject; -import org.apache.commons.beanutils.BeanUtils; -import org.apache.commons.beanutils.ConvertUtils; -import org.apache.commons.beanutils.Converter; -import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.fileupload2.core.DiskFileItem; import org.apache.commons.fileupload2.core.DiskFileItemFactory; import org.apache.commons.fileupload2.core.FileItem; @@ -84,6 +80,11 @@ import org.kohsuke.stapler.lang.Klass; import org.kohsuke.stapler.lang.MethodRef; import org.kohsuke.stapler.util.IllegalReflectiveAccessLogHandler; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.BeansException; +import org.springframework.beans.InvalidPropertyException; +import org.springframework.core.convert.converter.Converter; /** * {@link StaplerRequest2} implementation. @@ -617,7 +618,7 @@ public T bindParameters(Class type, String prefix, int index) { throw new IllegalArgumentException("Unable to convert to " + types[i]); } - args[i] = converter.convert(types[i], param); + args[i] = converter.convert(param); } return invokeConstructor(c, args); @@ -734,7 +735,8 @@ private static void fill(Object bean, String key, Object value) { if (last) { copyProperty(bean, token, value); } else { - bean = BeanUtils.getProperty(bean, token); + BeanWrapper wrapper = new BeanWrapperImpl(bean); + bean = wrapper.getPropertyValue(token); } } catch (IllegalAccessException x) { throw new IllegalAccessError(x.getMessage()); @@ -747,8 +749,8 @@ private static void fill(Object bean, String key, Object value) { throw (Error) e; } throw new RuntimeException(x); - } catch (NoSuchMethodException e) { - // ignore if there's no such property + } catch (Exception e) { + // ignore if there's no such property or other spring bean exceptions } } } @@ -936,7 +938,7 @@ public Object convertJSON(Object o) { throw new IllegalArgumentException("Unable to convert to " + type); } - return converter.convert(type, o); + return converter.convert(o.toString()); } else { // single value in a collection Converter converter = Stapler.lookupConverter(l.itemType); if (converter == null) { @@ -946,7 +948,7 @@ public Object convertJSON(Object o) { throw new IllegalArgumentException("Unable to convert to " + l.itemType); } } else { - l.add(converter.convert(l.itemType, o)); + l.add(converter.convert(o.toString())); } return l.toCollection(); } @@ -1146,14 +1148,15 @@ private void invokePostConstruct(SingleLinkedList methods, Object r) private TypePair getPropertyType(Object bean, String name) throws IllegalAccessException, InvocationTargetException { try { - PropertyDescriptor propDescriptor = PropertyUtils.getPropertyDescriptor(bean, name); + BeanWrapper wrapper = new BeanWrapperImpl(bean); + PropertyDescriptor propDescriptor = wrapper.getPropertyDescriptor(name); if (propDescriptor != null) { Method m = propDescriptor.getWriteMethod(); if (m != null) { return new TypePair(m.getGenericParameterTypes()[0], m.getParameterTypes()[0]); } } - } catch (NoSuchMethodException e) { + } catch (Exception e) { // no such property } @@ -1173,9 +1176,10 @@ private TypePair getPropertyType(Object bean, String name) private static void copyProperty(Object bean, String name, Object value) throws IllegalAccessException, InvocationTargetException { PropertyDescriptor propDescriptor; + BeanWrapper wrapper = new BeanWrapperImpl(bean); try { - propDescriptor = PropertyUtils.getPropertyDescriptor(bean, name); - } catch (NoSuchMethodException e) { + propDescriptor = wrapper.getPropertyDescriptor(name); + } catch (InvalidPropertyException e) { propDescriptor = null; } if (propDescriptor != null && propDescriptor.getWriteMethod() == null) { @@ -1184,11 +1188,11 @@ private static void copyProperty(Object bean, String name, Object value) if (propDescriptor != null) { Converter converter = Stapler.lookupConverter(propDescriptor.getPropertyType()); if (converter != null) { - value = converter.convert(propDescriptor.getPropertyType(), value); + value = converter.convert(value.toString()); } try { - PropertyUtils.setSimpleProperty(bean, name, value); - } catch (NoSuchMethodException e) { + wrapper.setPropertyValue(name, value); + } catch (BeansException e) { throw new NoSuchMethodError(e.getMessage()); } return; @@ -1197,12 +1201,12 @@ private static void copyProperty(Object bean, String name, Object value) // try a field try { Field field = bean.getClass().getField(name); - Converter converter = ConvertUtils.lookup(field.getType()); + Converter converter = Stapler.lookupConverter(field.getType()); if (converter != null) { - value = converter.convert(field.getType(), value); + value = converter.convert(value.toString()); } field.set(bean, value); - } catch (NoSuchFieldException e) { + } catch (Exception e) { // no such field } } diff --git a/core/src/main/java/org/kohsuke/stapler/Stapler.java b/core/src/main/java/org/kohsuke/stapler/Stapler.java index 234f382c9..57cc5fa61 100644 --- a/core/src/main/java/org/kohsuke/stapler/Stapler.java +++ b/core/src/main/java/org/kohsuke/stapler/Stapler.java @@ -57,6 +57,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -64,21 +65,17 @@ import java.util.Set; import java.util.Stack; import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sf.json.JSONObject; -import org.apache.commons.beanutils.ConversionException; -import org.apache.commons.beanutils.ConvertUtils; -import org.apache.commons.beanutils.ConvertUtilsBean; -import org.apache.commons.beanutils.Converter; -import org.apache.commons.beanutils.converters.DoubleConverter; -import org.apache.commons.beanutils.converters.FloatConverter; -import org.apache.commons.beanutils.converters.IntegerConverter; import org.apache.commons.fileupload2.core.FileItem; import org.apache.commons.io.IOUtils; import org.kohsuke.stapler.bind.BoundObjectTable; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.DefaultConversionService; /** * Maps an HTTP request to a method call / JSP invocation against a model object @@ -1151,21 +1148,31 @@ static String canonicalPath(String path) { } /** - * This is the {@link Converter} registry that Stapler uses, primarily + * This is the converter registry that Stapler uses, primarily * for form-to-JSON binding in {@link StaplerRequest2#bindJSON(Class, JSONObject)} * and its family of methods. */ - public static final ConvertUtilsBean CONVERT_UTILS = new ConvertUtilsBean(); + public static final Map, Converter> CONVERT_UTILS = new ConcurrentHashMap<>(); - public static Converter lookupConverter(Class type) { - Converter c = CONVERT_UTILS.lookup(type); + /** + * Spring's default conversion service for type conversion + */ + static final DefaultConversionService CONVERSION_SERVICE = new DefaultConversionService(); + + public static Converter lookupConverter(Class type) { + Converter c = CONVERT_UTILS.get(type); if (c != null) { return c; } - // fall back to compatibility behavior - c = ConvertUtils.lookup(type); - if (c != null) { - return c; + + // EnumSet conversion is handled by JSON binding, don't convert from string + if (EnumSet.class.isAssignableFrom(type)) { + return null; + } + + // check if Spring's conversion service can handle it + if (CONVERSION_SERVICE.canConvert(String.class, type)) { + return source -> CONVERSION_SERVICE.convert(source, type); } // look for the associated converter @@ -1175,7 +1182,7 @@ public static Converter lookupConverter(Class type) { } Class cl = type.getClassLoader().loadClass(type.getName() + "$StaplerConverterImpl"); c = (Converter) cl.getDeclaredConstructor().newInstance(); - CONVERT_UTILS.register(c, type); + CONVERT_UTILS.put(type, c); return c; } catch (ClassNotFoundException e) { // fall through @@ -1206,77 +1213,35 @@ public static Converter lookupConverter(Class type) { } } - // bean utils doesn't check the super type, so converters that apply to multiple types - // need to be handled outside its semantics - if (Enum.class.isAssignableFrom(type)) { // enum - return ENUM_CONVERTER; - } - return null; } static { - CONVERT_UTILS.register( - new Converter() { - @Override - public Object convert(Class type, Object value) { - if (value == null) { - return null; - } - try { - return new URL(value.toString()); - } catch (MalformedURLException e) { - throw new ConversionException(e); - } - } - }, - URL.class); - - CONVERT_UTILS.register( - new Converter() { - @Override - public FileItem convert(Class type, Object value) { - if (value == null) { - return null; - } - try { - return Stapler.getCurrentRequest2().getFileItem2(value.toString()); - } catch (ServletException | IOException e) { - throw new ConversionException(e); - } - } - }, - FileItem.class); - - CONVERT_UTILS.register( - new Converter() { - @Override - public org.apache.commons.fileupload.FileItem convert(Class type, Object value) { - if (value == null) { - return null; - } - try { - return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem( - Stapler.getCurrentRequest2().getFileItem2(value.toString())); - } catch (ServletException | IOException e) { - throw new ConversionException(e); - } - } - }, - org.apache.commons.fileupload.FileItem.class); + CONVERT_UTILS.put(URL.class, source -> { + try { + return new URL(source); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }); - // mapping for boxed types should map null to null, instead of null to zero. - CONVERT_UTILS.register(new IntegerConverter(null), Integer.class); - CONVERT_UTILS.register(new FloatConverter(null), Float.class); - CONVERT_UTILS.register(new DoubleConverter(null), Double.class); - } + CONVERT_UTILS.put(FileItem.class, source -> { + try { + return Stapler.getCurrentRequest2().getFileItem2(source); + } catch (ServletException | IOException e) { + throw new RuntimeException(e); + } + }); - private static final Converter ENUM_CONVERTER = new Converter() { - @Override - public Object convert(Class type, Object value) { - return Enum.valueOf(type, value.toString()); - } - }; + CONVERT_UTILS.put(org.apache.commons.fileupload.FileItem.class, source -> { + try { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem( + Stapler.getCurrentRequest2().getFileItem2(source)); + } catch (ServletException | IOException e) { + throw new RuntimeException(e); + } + }); + } /** * Escapes HTML/XML unsafe characters for the PCDATA section. diff --git a/core/src/main/java/org/kohsuke/stapler/StaplerRequest.java b/core/src/main/java/org/kohsuke/stapler/StaplerRequest.java index 0c0ea358b..86e7cb636 100644 --- a/core/src/main/java/org/kohsuke/stapler/StaplerRequest.java +++ b/core/src/main/java/org/kohsuke/stapler/StaplerRequest.java @@ -72,12 +72,11 @@ import javax.servlet.http.PushBuilder; import net.sf.json.JSONArray; import net.sf.json.JSONObject; -import org.apache.commons.beanutils.BeanUtils; -import org.apache.commons.beanutils.ConvertUtils; import org.apache.commons.fileupload2.core.FileItem; import org.kohsuke.stapler.bind.BoundObjectTable; import org.kohsuke.stapler.json.SubmittedForm; import org.kohsuke.stapler.lang.Klass; +import org.springframework.beans.BeanUtils; /** * Defines additional parameters/operations made available by Stapler. @@ -307,9 +306,9 @@ public interface StaplerRequest extends HttpServletRequest { * be simply ignored. * *

- * Values are converted into the right type. See {@link ConvertUtils#convert(String, Class)}. + * Values are converted into the right type using Spring's conversion service. * - * @see BeanUtils#setProperty(Object, String, Object) + * @see BeanUtils#copyProperties(Object, Object) * * @param bean * The object which will be filled out. diff --git a/core/src/main/java/org/kohsuke/stapler/StaplerRequest2.java b/core/src/main/java/org/kohsuke/stapler/StaplerRequest2.java index 91b4eb996..442948fbc 100644 --- a/core/src/main/java/org/kohsuke/stapler/StaplerRequest2.java +++ b/core/src/main/java/org/kohsuke/stapler/StaplerRequest2.java @@ -36,12 +36,11 @@ import java.util.Set; import net.sf.json.JSONArray; import net.sf.json.JSONObject; -import org.apache.commons.beanutils.BeanUtils; -import org.apache.commons.beanutils.ConvertUtils; import org.apache.commons.fileupload2.core.FileItem; import org.kohsuke.stapler.bind.BoundObjectTable; import org.kohsuke.stapler.json.SubmittedForm; import org.kohsuke.stapler.lang.Klass; +import org.springframework.beans.BeanUtils; /** * Defines additional parameters/operations made available by Stapler. @@ -269,9 +268,9 @@ public interface StaplerRequest2 extends HttpServletRequest { * be simply ignored. * *

- * Values are converted into the right type. See {@link ConvertUtils#convert(String, Class)}. + * Values are converted into the right type using Spring's conversion service. * - * @see BeanUtils#setProperty(Object, String, Object) + * @see BeanUtils#copyProperties(Object, Object) * * @param bean * The object which will be filled out. diff --git a/core/src/main/java/org/kohsuke/stapler/config/ConfigurationLoader.java b/core/src/main/java/org/kohsuke/stapler/config/ConfigurationLoader.java index 4c1a181c1..d570f0c6f 100644 --- a/core/src/main/java/org/kohsuke/stapler/config/ConfigurationLoader.java +++ b/core/src/main/java/org/kohsuke/stapler/config/ConfigurationLoader.java @@ -13,7 +13,7 @@ import java.util.Properties; import java.util.TreeMap; import java.util.function.Function; -import org.apache.commons.beanutils.ConvertUtils; +import org.springframework.core.convert.support.DefaultConversionService; /** * Provides a type-safe access to the configuration of the application. @@ -48,6 +48,7 @@ */ public class ConfigurationLoader { private final Function source; + private static final DefaultConversionService CONVERSION_SERVICE = new DefaultConversionService(); /** * The caller should use one of the fromXyz methods. @@ -127,7 +128,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return null; // TODO: check how the primitive types are handled here } - return ConvertUtils.convert(v, r); + return CONVERSION_SERVICE.convert(v, r); } private String getKey(Method method, Configuration c) { diff --git a/core/src/test/java/org/kohsuke/stapler/ObjectWithCustomConverter.java b/core/src/test/java/org/kohsuke/stapler/ObjectWithCustomConverter.java index 43bd09ff8..c4ba374c2 100644 --- a/core/src/test/java/org/kohsuke/stapler/ObjectWithCustomConverter.java +++ b/core/src/test/java/org/kohsuke/stapler/ObjectWithCustomConverter.java @@ -1,6 +1,6 @@ package org.kohsuke.stapler; -import org.apache.commons.beanutils.Converter; +import org.springframework.core.convert.converter.Converter; /** * @author Kohsuke Kawaguchi @@ -13,10 +13,10 @@ public ObjectWithCustomConverter(int x, int y) { this.y = y; } - public static class StaplerConverterImpl implements Converter { + public static class StaplerConverterImpl implements Converter { @Override - public Object convert(Class type, Object value) { - String[] tokens = value.toString().split(","); + public Object convert(String source) { + String[] tokens = source.split(","); return new ObjectWithCustomConverter(Integer.parseInt(tokens[0]), Integer.parseInt(tokens[1])); } }