From 75a721d1019da2a2fa86e24ff439df4a224e5b19 Mon Sep 17 00:00:00 2001 From: Emmanuel Bourg Date: Wed, 3 Aug 2016 19:55:01 +0200 Subject: Imported Upstream version 4.3.2 --- .../java/org/springframework/asm/ClassReader.java | 3 +- .../java/org/springframework/asm/ClassWriter.java | 47 +- .../main/java/org/springframework/asm/Frame.java | 2 +- .../main/java/org/springframework/asm/Handle.java | 60 +- .../springframework/core/CollectionFactory.java | 6 +- .../core/ConfigurableObjectInputStream.java | 12 +- .../core/DecoratingClassLoader.java | 33 +- .../org/springframework/core/DecoratingProxy.java | 47 + .../org/springframework/core/MethodClassKey.java | 85 ++ .../springframework/core/MethodIntrospector.java | 15 +- .../org/springframework/core/MethodParameter.java | 78 ++ .../core/OverridingClassLoader.java | 37 +- .../core/SerializableTypeWrapper.java | 7 +- .../org/springframework/core/SpringProperties.java | 3 +- ...ractAliasAwareAnnotationAttributeExtractor.java | 17 +- .../core/annotation/AnnotatedElementUtils.java | 1135 +++++++++++++++----- .../annotation/AnnotationAttributeExtractor.java | 5 +- .../core/annotation/AnnotationAttributes.java | 100 +- .../annotation/AnnotationAwareOrderComparator.java | 20 +- .../core/annotation/AnnotationUtils.java | 293 +++-- .../DefaultAnnotationAttributeExtractor.java | 7 +- .../MapAnnotationAttributeExtractor.java | 36 +- .../SynthesizedAnnotationInvocationHandler.java | 31 +- .../annotation/SynthesizingMethodParameter.java | 57 +- .../core/convert/TypeDescriptor.java | 6 +- .../core/convert/converter/ConverterRegistry.java | 13 +- .../support/AbstractConditionalEnumConverter.java | 50 + .../core/convert/support/ConversionUtils.java | 15 +- .../convert/support/DefaultConversionService.java | 8 +- .../convert/support/EnumToIntegerConverter.java | 40 + .../convert/support/EnumToStringConverter.java | 21 +- .../convert/support/GenericConversionService.java | 17 +- .../support/IntegerToEnumConverterFactory.java | 52 + .../convert/support/ObjectToObjectConverter.java | 7 +- .../support/StringToEnumConverterFactory.java | 13 +- .../core/env/AbstractEnvironment.java | 3 +- .../core/env/AbstractPropertyResolver.java | 17 +- .../core/env/ConfigurablePropertyResolver.java | 20 +- .../springframework/core/env/PropertyResolver.java | 24 +- .../core/env/PropertySourcesPropertyResolver.java | 67 +- .../core/env/SystemEnvironmentPropertySource.java | 58 +- .../core/io/AbstractFileResolvingResource.java | 4 +- .../core/io/DefaultResourceLoader.java | 37 +- .../core/io/FileSystemResource.java | 5 +- .../org/springframework/core/io/PathResource.java | 6 +- .../springframework/core/io/ProtocolResolver.java | 42 + .../java/org/springframework/core/io/Resource.java | 23 +- .../org/springframework/core/io/UrlResource.java | 7 +- .../io/support/DefaultPropertySourceFactory.java | 39 + .../PathMatchingResourcePatternResolver.java | 132 ++- .../core/io/support/PropertySourceFactory.java | 41 + .../core/io/support/ResourcePatternUtils.java | 6 +- .../core/io/support/ResourceRegion.java | 77 ++ .../core/io/support/SpringFactoriesLoader.java | 10 +- .../serializer/support/SerializationDelegate.java | 78 ++ .../core/style/ToStringCreator.java | 14 +- .../core/task/SimpleAsyncTaskExecutor.java | 23 +- .../springframework/core/task/TaskDecorator.java | 45 + .../core/task/support/TaskExecutorAdapter.java | 50 +- .../AbstractRecursiveAnnotationVisitor.java | 4 +- .../AnnotationAttributesReadingVisitor.java | 51 +- .../AnnotationMetadataReadingVisitor.java | 5 +- .../AnnotationReadingVisitorUtils.java | 43 +- .../classreading/MethodMetadataReadingVisitor.java | 8 +- .../RecursiveAnnotationArrayVisitor.java | 2 +- .../RecursiveAnnotationAttributesVisitor.java | 51 +- .../java/org/springframework/lang/UsesSunMisc.java | 36 + .../org/springframework/util/AntPathMatcher.java | 65 +- .../springframework/util/AutoPopulatingList.java | 15 +- .../java/org/springframework/util/Base64Utils.java | 8 +- .../java/org/springframework/util/ClassUtils.java | 11 +- .../java/org/springframework/util/DigestUtils.java | 26 +- .../util/LinkedCaseInsensitiveMap.java | 10 +- .../java/org/springframework/util/MimeType.java | 157 +-- .../org/springframework/util/MimeTypeUtils.java | 5 +- .../java/org/springframework/util/NumberUtils.java | 48 +- .../util/ResizableByteArrayOutputStream.java | 2 +- .../org/springframework/util/ResourceUtils.java | 4 +- .../java/org/springframework/util/StopWatch.java | 4 +- .../java/org/springframework/util/StreamUtils.java | 59 +- .../util/UpdateMessageDigestInputStream.java | 4 +- .../org/springframework/util/backoff/BackOff.java | 2 +- .../util/comparator/CompoundComparator.java | 2 +- .../util/comparator/InstanceComparator.java | 2 +- .../CompletableToListenableFutureAdapter.java | 5 +- .../util/concurrent/FailureCallback.java | 10 +- .../util/concurrent/FutureAdapter.java | 9 +- .../util/concurrent/ListenableFuture.java | 23 +- .../util/concurrent/ListenableFutureAdapter.java | 8 +- .../util/concurrent/ListenableFutureCallback.java | 4 +- .../ListenableFutureCallbackRegistry.java | 38 +- .../util/concurrent/SuccessCallback.java | 8 +- .../util/xml/XmlValidationModeDetector.java | 2 +- 93 files changed, 2970 insertions(+), 967 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/core/DecoratingProxy.java create mode 100644 spring-core/src/main/java/org/springframework/core/MethodClassKey.java create mode 100644 spring-core/src/main/java/org/springframework/core/convert/support/AbstractConditionalEnumConverter.java create mode 100644 spring-core/src/main/java/org/springframework/core/convert/support/EnumToIntegerConverter.java create mode 100644 spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java create mode 100644 spring-core/src/main/java/org/springframework/core/io/ProtocolResolver.java create mode 100644 spring-core/src/main/java/org/springframework/core/io/support/DefaultPropertySourceFactory.java create mode 100644 spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java create mode 100644 spring-core/src/main/java/org/springframework/core/io/support/ResourceRegion.java create mode 100644 spring-core/src/main/java/org/springframework/core/serializer/support/SerializationDelegate.java create mode 100644 spring-core/src/main/java/org/springframework/core/task/TaskDecorator.java create mode 100644 spring-core/src/main/java/org/springframework/lang/UsesSunMisc.java (limited to 'spring-core/src/main') diff --git a/spring-core/src/main/java/org/springframework/asm/ClassReader.java b/spring-core/src/main/java/org/springframework/asm/ClassReader.java index 25ad9aeb..316c14ec 100644 --- a/spring-core/src/main/java/org/springframework/asm/ClassReader.java +++ b/spring-core/src/main/java/org/springframework/asm/ClassReader.java @@ -2502,11 +2502,12 @@ public class ClassReader { int tag = readByte(index); int[] items = this.items; int cpIndex = items[readUnsignedShort(index + 1)]; + boolean itf = b[cpIndex - 1] == ClassWriter.IMETH; String owner = readClass(cpIndex, buf); cpIndex = items[readUnsignedShort(cpIndex + 2)]; String name = readUTF8(cpIndex, buf); String desc = readUTF8(cpIndex + 2, buf); - return new Handle(tag, owner, name, desc); + return new Handle(tag, owner, name, desc, itf); } } } diff --git a/spring-core/src/main/java/org/springframework/asm/ClassWriter.java b/spring-core/src/main/java/org/springframework/asm/ClassWriter.java index 4ec8c53a..72add9d6 100644 --- a/spring-core/src/main/java/org/springframework/asm/ClassWriter.java +++ b/spring-core/src/main/java/org/springframework/asm/ClassWriter.java @@ -1052,7 +1052,7 @@ public class ClassWriter extends ClassVisitor { } } else if (cst instanceof Handle) { Handle h = (Handle) cst; - return newHandleItem(h.tag, h.owner, h.name, h.desc); + return newHandleItem(h.tag, h.owner, h.name, h.desc, h.itf); } else { throw new IllegalArgumentException("value " + cst); } @@ -1187,10 +1187,12 @@ public class ClassWriter extends ClassVisitor { * the name of the field or method. * @param desc * the descriptor of the field or method. + * @param itf + * true if the owner is an interface. * @return a new or an already existing method type reference item. */ Item newHandleItem(final int tag, final String owner, final String name, - final String desc) { + final String desc, final boolean itf) { key4.set(HANDLE_BASE + tag, owner, name, desc); Item result = get(key4); if (result == null) { @@ -1199,8 +1201,7 @@ public class ClassWriter extends ClassVisitor { } else { put112(HANDLE, tag, - newMethod(owner, name, desc, - tag == Opcodes.H_INVOKEINTERFACE)); + newMethod(owner, name, desc, itf)); } result = new Item(index++, key4); put(result); @@ -1230,10 +1231,44 @@ public class ClassWriter extends ClassVisitor { * the descriptor of the field or method. * @return the index of a new or already existing method type reference * item. + * + * @deprecated this method is superseded by + * {@link #newHandle(int, String, String, String, boolean)}. */ + @Deprecated public int newHandle(final int tag, final String owner, final String name, final String desc) { - return newHandleItem(tag, owner, name, desc).index; + return newHandle(tag, owner, name, desc, tag == Opcodes.H_INVOKEINTERFACE); + } + + /** + * Adds a handle to the constant pool of the class being build. Does nothing + * if the constant pool already contains a similar item. This method is + * intended for {@link Attribute} sub classes, and is normally not needed by + * class generators or adapters. + * + * @param tag + * the kind of this handle. Must be {@link Opcodes#H_GETFIELD}, + * {@link Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, + * {@link Opcodes#H_PUTSTATIC}, {@link Opcodes#H_INVOKEVIRTUAL}, + * {@link Opcodes#H_INVOKESTATIC}, + * {@link Opcodes#H_INVOKESPECIAL}, + * {@link Opcodes#H_NEWINVOKESPECIAL} or + * {@link Opcodes#H_INVOKEINTERFACE}. + * @param owner + * the internal name of the field or method owner class. + * @param name + * the name of the field or method. + * @param desc + * the descriptor of the field or method. + * @param itf + * true if the owner is an interface. + * @return the index of a new or already existing method type reference + * item. + */ + public int newHandle(final int tag, final String owner, final String name, + final String desc, final boolean itf) { + return newHandleItem(tag, owner, name, desc, itf).index; } /** @@ -1265,7 +1300,7 @@ public class ClassWriter extends ClassVisitor { int hashCode = bsm.hashCode(); bootstrapMethods.putShort(newHandle(bsm.tag, bsm.owner, bsm.name, - bsm.desc)); + bsm.desc, bsm.isInterface())); int argsLength = bsmArgs.length; bootstrapMethods.putShort(argsLength); diff --git a/spring-core/src/main/java/org/springframework/asm/Frame.java b/spring-core/src/main/java/org/springframework/asm/Frame.java index 29e71c5a..389e416b 100644 --- a/spring-core/src/main/java/org/springframework/asm/Frame.java +++ b/spring-core/src/main/java/org/springframework/asm/Frame.java @@ -163,7 +163,7 @@ final class Frame { private static final int LOCAL = 0x2000000; /** - * Kind of the the types that are relative to the stack of an input stack + * Kind of the types that are relative to the stack of an input stack * map frame. The value of such types is a position relatively to the top of * this stack. */ diff --git a/spring-core/src/main/java/org/springframework/asm/Handle.java b/spring-core/src/main/java/org/springframework/asm/Handle.java index adc6f097..02d4a58c 100644 --- a/spring-core/src/main/java/org/springframework/asm/Handle.java +++ b/spring-core/src/main/java/org/springframework/asm/Handle.java @@ -64,6 +64,12 @@ public final class Handle { */ final String desc; + + /** + * Indicate if the owner is an interface or not. + */ + final boolean itf; + /** * Constructs a new field or method handle. * @@ -84,12 +90,44 @@ public final class Handle { * @param desc * the descriptor of the field or method designated by this * handle. + * + * @deprecated this constructor has been superseded + * by {@link #Handle(int, String, String, String, boolean)}. */ + @Deprecated public Handle(int tag, String owner, String name, String desc) { + this(tag, owner, name, desc, tag == Opcodes.H_INVOKEINTERFACE); + } + + /** + * Constructs a new field or method handle. + * + * @param tag + * the kind of field or method designated by this Handle. Must be + * {@link Opcodes#H_GETFIELD}, {@link Opcodes#H_GETSTATIC}, + * {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, + * {@link Opcodes#H_INVOKEVIRTUAL}, + * {@link Opcodes#H_INVOKESTATIC}, + * {@link Opcodes#H_INVOKESPECIAL}, + * {@link Opcodes#H_NEWINVOKESPECIAL} or + * {@link Opcodes#H_INVOKEINTERFACE}. + * @param owner + * the internal name of the class that owns the field or method + * designated by this handle. + * @param name + * the name of the field or method designated by this handle. + * @param desc + * the descriptor of the field or method designated by this + * handle. + * @param itf + * true if the owner is an interface. + */ + public Handle(int tag, String owner, String name, String desc, boolean itf) { this.tag = tag; this.owner = owner; this.name = name; this.desc = desc; + this.itf = itf; } /** @@ -135,6 +173,17 @@ public final class Handle { return desc; } + /** + * Returns true if the owner of the field or method designated + * by this handle is an interface. + * + * @return true if the owner of the field or method designated + * by this handle is an interface. + */ + public boolean isInterface() { + return itf; + } + @Override public boolean equals(Object obj) { if (obj == this) { @@ -144,13 +193,13 @@ public final class Handle { return false; } Handle h = (Handle) obj; - return tag == h.tag && owner.equals(h.owner) && name.equals(h.name) - && desc.equals(h.desc); + return tag == h.tag && itf == h.itf && owner.equals(h.owner) + && name.equals(h.name) && desc.equals(h.desc); } @Override public int hashCode() { - return tag + owner.hashCode() * name.hashCode() * desc.hashCode(); + return tag + (itf? 64: 0) + owner.hashCode() * name.hashCode() * desc.hashCode(); } /** @@ -158,13 +207,16 @@ public final class Handle { * representation is: * *
+     * for a reference to a class:
      * owner '.' name desc ' ' '(' tag ')'
+     * for a reference to an interface:
+     * owner '.' name desc ' ' '(' tag ' ' itf ')'
      * 
* * . As this format is unambiguous, it can be parsed if necessary. */ @Override public String toString() { - return owner + '.' + name + desc + " (" + tag + ')'; + return owner + '.' + name + desc + " (" + tag + (itf? " itf": "") + ')'; } } diff --git a/spring-core/src/main/java/org/springframework/core/CollectionFactory.java b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java index e9af0617..0a83068c 100644 --- a/spring-core/src/main/java/org/springframework/core/CollectionFactory.java +++ b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -203,7 +203,7 @@ public abstract class CollectionFactory { try { return (Collection) collectionType.newInstance(); } - catch (Exception ex) { + catch (Throwable ex) { throw new IllegalArgumentException( "Could not instantiate Collection type: " + collectionType.getName(), ex); } @@ -318,7 +318,7 @@ public abstract class CollectionFactory { try { return (Map) mapType.newInstance(); } - catch (Exception ex) { + catch (Throwable ex) { throw new IllegalArgumentException("Could not instantiate Map type: " + mapType.getName(), ex); } } diff --git a/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java b/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java index 5af282f6..70a0a87c 100644 --- a/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java +++ b/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import java.io.InputStream; import java.io.NotSerializableException; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; -import java.lang.reflect.Proxy; import org.springframework.util.ClassUtils; @@ -101,7 +100,7 @@ public class ConfigurableObjectInputStream extends ObjectInputStream { } } try { - return Proxy.getProxyClass(this.classLoader, resolvedInterfaces); + return ClassUtils.createCompositeInterface(resolvedInterfaces, this.classLoader); } catch (IllegalArgumentException ex) { throw new ClassNotFoundException(null, ex); @@ -117,7 +116,7 @@ public class ConfigurableObjectInputStream extends ObjectInputStream { for (int i = 0; i < interfaces.length; i++) { resolvedInterfaces[i] = resolveFallbackIfPossible(interfaces[i], ex); } - return Proxy.getProxyClass(getFallbackClassLoader(), resolvedInterfaces); + return ClassUtils.createCompositeInterface(resolvedInterfaces, getFallbackClassLoader()); } } } @@ -139,8 +138,9 @@ public class ConfigurableObjectInputStream extends ObjectInputStream { /** * Return the fallback ClassLoader to use when no ClassLoader was specified - * and ObjectInputStream's own default ClassLoader failed. - *

The default implementation simply returns {@code null}. + * and ObjectInputStream's own default class loader failed. + *

The default implementation simply returns {@code null}, indicating + * that no specific fallback is available. */ protected ClassLoader getFallbackClassLoader() throws IOException { return null; diff --git a/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java b/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java index 350df5cf..b29ac071 100644 --- a/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java +++ b/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.core; -import java.util.HashSet; +import java.util.Collections; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.springframework.lang.UsesJava7; import org.springframework.util.Assert; @@ -49,11 +50,11 @@ public abstract class DecoratingClassLoader extends ClassLoader { } - private final Set excludedPackages = new HashSet(); + private final Set excludedPackages = + Collections.newSetFromMap(new ConcurrentHashMap(8)); - private final Set excludedClasses = new HashSet(); - - private final Object exclusionMonitor = new Object(); + private final Set excludedClasses = + Collections.newSetFromMap(new ConcurrentHashMap(8)); /** @@ -79,9 +80,7 @@ public abstract class DecoratingClassLoader extends ClassLoader { */ public void excludePackage(String packageName) { Assert.notNull(packageName, "Package name must not be null"); - synchronized (this.exclusionMonitor) { - this.excludedPackages.add(packageName); - } + this.excludedPackages.add(packageName); } /** @@ -92,9 +91,7 @@ public abstract class DecoratingClassLoader extends ClassLoader { */ public void excludeClass(String className) { Assert.notNull(className, "Class name must not be null"); - synchronized (this.exclusionMonitor) { - this.excludedClasses.add(className); - } + this.excludedClasses.add(className); } /** @@ -107,15 +104,13 @@ public abstract class DecoratingClassLoader extends ClassLoader { * @see #excludeClass */ protected boolean isExcluded(String className) { - synchronized (this.exclusionMonitor) { - if (this.excludedClasses.contains(className)) { + if (this.excludedClasses.contains(className)) { + return true; + } + for (String packageName : this.excludedPackages) { + if (className.startsWith(packageName)) { return true; } - for (String packageName : this.excludedPackages) { - if (className.startsWith(packageName)) { - return true; - } - } } return false; } diff --git a/spring-core/src/main/java/org/springframework/core/DecoratingProxy.java b/spring-core/src/main/java/org/springframework/core/DecoratingProxy.java new file mode 100644 index 00000000..b3ae9fce --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/DecoratingProxy.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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. + */ + +package org.springframework.core; + +/** + * Interface to be implemented by decorating proxies, in particular Spring AOP + * proxies but potentially also custom proxies with decorator semantics. + * + *

Note that this interface should just be implemented if the decorated class + * is not within the hierarchy of the proxy class to begin with. In particular, + * a "target-class" proxy such as a Spring AOP CGLIB proxy should not implement + * it since any lookup on the target class can simply be performed on the proxy + * class there anyway. + * + *

Defined in the core module in order to allow + * #{@link org.springframework.core.annotation.AnnotationAwareOrderComparator} + * (and potential other candidates without spring-aop dependencies) to use it + * for introspection purposes, in particular annotation lookups. + * + * @author Juergen Hoeller + * @since 4.3 + */ +public interface DecoratingProxy { + + /** + * Return the (ultimate) decorated class behind this proxy. + *

In case of an AOP proxy, this will be the ultimate target class, + * not just the immediate target (in case of multiple nested proxies). + * @return the decorated class (never {@code null}) + */ + Class getDecoratedClass(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/MethodClassKey.java b/spring-core/src/main/java/org/springframework/core/MethodClassKey.java new file mode 100644 index 00000000..b837fc64 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/MethodClassKey.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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. + */ + +package org.springframework.core; + +import java.lang.reflect.Method; + +import org.springframework.util.ObjectUtils; + +/** + * A common key class for a method against a specific target class, + * including {@link #toString()} representation and {@link Comparable} + * support (as suggested for custom {@code HashMap} keys as of Java 8). + * + * @author Juergen Hoeller + * @since 4.3 + */ +public final class MethodClassKey implements Comparable { + + private final Method method; + + private final Class targetClass; + + + /** + * Create a key object for the given method and target class. + * @param method the method to wrap (must not be {@code null}) + * @param targetClass the target class that the method will be invoked + * on (may be {@code null} if identical to the declaring class) + */ + public MethodClassKey(Method method, Class targetClass) { + this.method = method; + this.targetClass = targetClass; + } + + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MethodClassKey)) { + return false; + } + MethodClassKey otherKey = (MethodClassKey) other; + return (this.method.equals(otherKey.method) && + ObjectUtils.nullSafeEquals(this.targetClass, otherKey.targetClass)); + } + + @Override + public int hashCode() { + return this.method.hashCode() + (this.targetClass != null ? this.targetClass.hashCode() * 29 : 0); + } + + @Override + public String toString() { + return this.method + (this.targetClass != null ? " on " + this.targetClass : ""); + } + + @Override + public int compareTo(MethodClassKey other) { + int result = this.method.getName().compareTo(other.method.getName()); + if (result == 0) { + result = this.method.toString().compareTo(other.method.toString()); + if (result == 0 && this.targetClass != null) { + result = this.targetClass.getName().compareTo(other.targetClass.getName()); + } + } + return result; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java index d402c10b..804c26e7 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java +++ b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,10 +85,9 @@ public abstract class MethodIntrospector { /** * Select methods on the given target type based on a filter. - *

Callers define methods of interest through the - * {@link ReflectionUtils.MethodFilter} parameter. + *

Callers define methods of interest through the {@code MethodFilter} parameter. * @param targetType the target type to search methods on - * @param methodFilter a {@link ReflectionUtils.MethodFilter} to help + * @param methodFilter a {@code MethodFilter} to help * recognize handler methods of interest * @return the selected methods, or an empty set in case of no match */ @@ -111,22 +110,26 @@ public abstract class MethodIntrospector { * @param targetType the target type to search methods on * (typically an interface-based JDK proxy) * @return a corresponding invocable method on the target type + * @throws IllegalStateException if the given method is not invocable on the given + * target type (typically due to a proxy mismatch) */ public static Method selectInvocableMethod(Method method, Class targetType) { if (method.getDeclaringClass().isAssignableFrom(targetType)) { return method; } try { + String methodName = method.getName(); + Class[] parameterTypes = method.getParameterTypes(); for (Class ifc : targetType.getInterfaces()) { try { - return ifc.getMethod(method.getName(), method.getParameterTypes()); + return ifc.getMethod(methodName, parameterTypes); } catch (NoSuchMethodException ex) { // Alright, not on this interface then... } } // A final desperate attempt on the proxy class itself... - return targetType.getMethod(method.getName(), method.getParameterTypes()); + return targetType.getMethod(methodName, parameterTypes); } catch (NoSuchMethodException ex) { throw new IllegalStateException(String.format( diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameter.java b/spring-core/src/main/java/org/springframework/core/MethodParameter.java index e34b8d00..48344a91 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.Map; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * Helper class that encapsulates the specification of a method parameter, i.e. a {@link Method} @@ -47,6 +48,21 @@ import org.springframework.util.Assert; */ public class MethodParameter { + private static final Class javaUtilOptionalClass; + + static { + Class clazz; + try { + clazz = ClassUtils.forName("java.util.Optional", MethodParameter.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Java 8 not available - Optional references simply not supported then. + clazz = null; + } + javaUtilOptionalClass = clazz; + } + + private final Method method; private final Constructor constructor; @@ -70,6 +86,8 @@ public class MethodParameter { private volatile String parameterName; + private volatile MethodParameter nestedMethodParameter; + /** * Create a new {@code MethodParameter} for the given method, with nesting level 1. @@ -279,6 +297,44 @@ public class MethodParameter { return this.typeIndexesPerLevel; } + /** + * Return a variant of this {@code MethodParameter} which points to the + * same parameter but one nesting level deeper. This is effectively the + * same as {@link #increaseNestingLevel()}, just with an independent + * {@code MethodParameter} object (e.g. in case of the original being cached). + * @since 4.3 + */ + public MethodParameter nested() { + if (this.nestedMethodParameter != null) { + return this.nestedMethodParameter; + } + MethodParameter nestedParam = clone(); + nestedParam.nestingLevel = this.nestingLevel + 1; + this.nestedMethodParameter = nestedParam; + return nestedParam; + } + + /** + * Return whether this method parameter is declared as optional + * in the form of Java 8's {@link java.util.Optional}. + * @since 4.3 + */ + public boolean isOptional() { + return (getParameterType() == javaUtilOptionalClass); + } + + /** + * Return a variant of this {@code MethodParameter} which points to + * the same parameter but one nesting level deeper in case of a + * {@link java.util.Optional} declaration. + * @since 4.3 + * @see #isOptional() + * @see #nested() + */ + public MethodParameter nestedIfOptional() { + return (isOptional() ? nested() : this); + } + /** * Set a containing class to resolve the parameter type against. @@ -350,6 +406,7 @@ public class MethodParameter { Integer index = getTypeIndexForLevel(i); type = args[index != null ? index : args.length - 1]; } + // TODO: Object.class if unresolvable } if (type instanceof Class) { return (Class) type; @@ -406,6 +463,16 @@ public class MethodParameter { return adaptAnnotation(getAnnotatedElement().getAnnotation(annotationType)); } + /** + * Return whether the method/constructor is annotated with the given type. + * @param annotationType the annotation type to look for + * @since 4.3 + * @see #getMethodAnnotation(Class) + */ + public boolean hasMethodAnnotation(Class annotationType) { + return getAnnotatedElement().isAnnotationPresent(annotationType); + } + /** * Return the annotations associated with the specific method/constructor parameter. */ @@ -530,6 +597,17 @@ public class MethodParameter { return (getMember().hashCode() * 31 + this.parameterIndex); } + @Override + public String toString() { + return (this.method != null ? "method '" + this.method.getName() + "'" : "constructor") + + " parameter " + this.parameterIndex; + } + + @Override + public MethodParameter clone() { + return new MethodParameter(this); + } + /** * Create a new MethodParameter for the given method or constructor. diff --git a/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java b/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java index 684d8561..c0bc9f32 100644 --- a/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java +++ b/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java @@ -23,12 +23,12 @@ import org.springframework.lang.UsesJava7; import org.springframework.util.FileCopyUtils; /** - * {@code ClassLoader} that does not always delegate to the - * parent loader, as normal class loaders do. This enables, for example, - * instrumentation to be forced in the overriding ClassLoader, or a - * "throwaway" class loading behavior, where selected classes are - * temporarily loaded in the overriding ClassLoader, in order to load - * an instrumented version of the class in the parent ClassLoader later on. + * {@code ClassLoader} that does not always delegate to the parent loader + * as normal class loaders do. This enables, for example, instrumentation to be + * forced in the overriding ClassLoader, or a "throwaway" class loading behavior + * where selected application classes are temporarily loaded in the overriding + * {@code ClassLoader} for introspection purposes before eventually loading an + * instrumented version of the class in the given parent {@code ClassLoader}. * * @author Rod Johnson * @author Juergen Hoeller @@ -38,7 +38,8 @@ import org.springframework.util.FileCopyUtils; public class OverridingClassLoader extends DecoratingClassLoader { /** Packages that are excluded by default */ - public static final String[] DEFAULT_EXCLUDED_PACKAGES = new String[] {"java.", "javax.", "sun.", "oracle."}; + public static final String[] DEFAULT_EXCLUDED_PACKAGES = new String[] + {"java.", "javax.", "sun.", "oracle.", "javassist.", "org.aspectj.", "net.sf.cglib."}; private static final String CLASS_FILE_SUFFIX = ".class"; @@ -49,18 +50,40 @@ public class OverridingClassLoader extends DecoratingClassLoader { } + private final ClassLoader overrideDelegate; + + /** * Create a new OverridingClassLoader for the given ClassLoader. * @param parent the ClassLoader to build an overriding ClassLoader for */ public OverridingClassLoader(ClassLoader parent) { + this(parent, null); + } + + /** + * Create a new OverridingClassLoader for the given ClassLoader. + * @param parent the ClassLoader to build an overriding ClassLoader for + * @param overrideDelegate the ClassLoader to delegate to for overriding + * @since 4.3 + */ + public OverridingClassLoader(ClassLoader parent, ClassLoader overrideDelegate) { super(parent); + this.overrideDelegate = overrideDelegate; for (String packageName : DEFAULT_EXCLUDED_PACKAGES) { excludePackage(packageName); } } + @Override + public Class loadClass(String name) throws ClassNotFoundException { + if (this.overrideDelegate != null && isEligibleForOverriding(name)) { + return this.overrideDelegate.loadClass(name); + } + return super.loadClass(name); + } + @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { if (isEligibleForOverriding(name)) { diff --git a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java index 8f1fd9d9..eaae658a 100644 --- a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java +++ b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -372,6 +372,8 @@ abstract class SerializableTypeWrapper { private final String methodName; + private final Class declaringClass; + private final int index; private transient Method method; @@ -381,6 +383,7 @@ abstract class SerializableTypeWrapper { public MethodInvokeTypeProvider(TypeProvider provider, Method method, int index) { this.provider = provider; this.methodName = method.getName(); + this.declaringClass = method.getDeclaringClass(); this.index = index; this.method = method; } @@ -404,7 +407,7 @@ abstract class SerializableTypeWrapper { private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { inputStream.defaultReadObject(); - this.method = ReflectionUtils.findMethod(this.provider.getType().getClass(), this.methodName); + this.method = ReflectionUtils.findMethod(this.declaringClass, this.methodName); Assert.state(Type.class == this.method.getReturnType() || Type[].class == this.method.getReturnType()); } } diff --git a/spring-core/src/main/java/org/springframework/core/SpringProperties.java b/spring-core/src/main/java/org/springframework/core/SpringProperties.java index f0b25d6d..f5c34994 100644 --- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ import org.apache.commons.logging.LogFactory; * @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME * @see org.springframework.beans.CachedIntrospectionResults#IGNORE_BEANINFO_PROPERTY_NAME * @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME + * @see org.springframework.test.context.cache.ContextCache#MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME */ public abstract class SpringProperties { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java index 0febd916..32b8725c 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.List; import java.util.Map; @@ -35,13 +34,13 @@ import org.springframework.util.ObjectUtils; * @param the type of source supported by this extractor * @see Annotation * @see AliasFor - * @see AnnotationUtils#synthesizeAnnotation(Annotation, AnnotatedElement) + * @see AnnotationUtils#synthesizeAnnotation(Annotation, Object) */ abstract class AbstractAliasAwareAnnotationAttributeExtractor implements AnnotationAttributeExtractor { private final Class annotationType; - private final AnnotatedElement annotatedElement; + private final Object annotatedElement; private final S source; @@ -56,7 +55,7 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor implements Anno * @param source the underlying source of annotation attributes; never {@code null} */ AbstractAliasAwareAnnotationAttributeExtractor( - Class annotationType, AnnotatedElement annotatedElement, S source) { + Class annotationType, Object annotatedElement, S source) { Assert.notNull(annotationType, "annotationType must not be null"); Assert.notNull(source, "source must not be null"); @@ -73,7 +72,7 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor implements Anno } @Override - public final AnnotatedElement getAnnotatedElement() { + public final Object getAnnotatedElement() { return this.annotatedElement; } @@ -89,18 +88,18 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor implements Anno List aliasNames = this.attributeAliasMap.get(attributeName); if (aliasNames != null) { - Object defaultValue = AnnotationUtils.getDefaultValue(getAnnotationType(), attributeName); + Object defaultValue = AnnotationUtils.getDefaultValue(this.annotationType, attributeName); for (String aliasName : aliasNames) { Object aliasValue = getRawAttributeValue(aliasName); if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) && !ObjectUtils.nullSafeEquals(attributeValue, defaultValue) && !ObjectUtils.nullSafeEquals(aliasValue, defaultValue)) { - String elementName = (getAnnotatedElement() != null ? getAnnotatedElement().toString() : "unknown element"); + String elementName = (this.annotatedElement != null ? this.annotatedElement.toString() : "unknown element"); throw new AnnotationConfigurationException(String.format( "In annotation [%s] declared on %s and synthesized from [%s], attribute '%s' and its " + "alias '%s' are present with values of [%s] and [%s], but only one is permitted.", - getAnnotationType().getName(), elementName, getSource(), attributeName, aliasName, + this.annotationType.getName(), elementName, this.source, attributeName, aliasName, ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue))); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java index 74549cf2..6021ab16 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,8 +33,8 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** - * General utility methods for finding annotations and meta-annotations on - * {@link AnnotatedElement AnnotatedElements}. + * General utility methods for finding annotations, meta-annotations, and + * repeatable annotations on {@link AnnotatedElement AnnotatedElements}. * *

{@code AnnotatedElementUtils} defines the public API for Spring's * meta-annotation programming model with support for annotation attribute @@ -48,7 +48,9 @@ import org.springframework.util.MultiValueMap; *

Support for meta-annotations with attribute overrides in * composed annotations is provided by all variants of the * {@code getMergedAnnotationAttributes()}, {@code getMergedAnnotation()}, - * {@code findMergedAnnotationAttributes()}, and {@code findMergedAnnotation()} + * {@code getAllMergedAnnotations()}, {@code getMergedRepeatableAnnotations()}, + * {@code findMergedAnnotationAttributes()}, {@code findMergedAnnotation()}, + * {@code findAllMergedAnnotations()}, and {@code findMergedRepeatableAnnotations()} * methods. * *

Find vs. Get Semantics

@@ -94,8 +96,44 @@ import org.springframework.util.MultiValueMap; */ public class AnnotatedElementUtils { + /** + * {@code null} constant used to denote that the search algorithm should continue. + */ private static final Boolean CONTINUE = null; + private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + + private static final Processor alwaysTrueAnnotationProcessor = new AlwaysTrueBooleanAnnotationProcessor(); + + + /** + * Build an adapted {@link AnnotatedElement} for the given annotations, + * typically for use with other methods on {@link AnnotatedElementUtils}. + * @param annotations the annotations to expose through the {@code AnnotatedElement} + * @since 4.3 + */ + public static AnnotatedElement forAnnotations(final Annotation... annotations) { + return new AnnotatedElement() { + @Override + @SuppressWarnings("unchecked") + public T getAnnotation(Class annotationClass) { + for (Annotation ann : annotations) { + if (ann.annotationType() == annotationClass) { + return (T) ann; + } + } + return null; + } + @Override + public Annotation[] getAnnotations() { + return annotations; + } + @Override + public Annotation[] getDeclaredAnnotations() { + return annotations; + } + }; + } /** * Get the fully qualified class names of all meta-annotation types @@ -114,26 +152,8 @@ public class AnnotatedElementUtils { public static Set getMetaAnnotationTypes(AnnotatedElement element, Class annotationType) { Assert.notNull(element, "AnnotatedElement must not be null"); Assert.notNull(annotationType, "annotationType must not be null"); - final Set types = new LinkedHashSet(); - - try { - Annotation annotation = element.getAnnotation(annotationType); - if (annotation != null) { - searchWithGetSemantics(annotation.annotationType(), annotationType, null, new SimpleAnnotationProcessor() { - @Override - public Object process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { - types.add(annotation.annotationType().getName()); - return CONTINUE; - } - }, new HashSet(), 1); - } - } - catch (Throwable ex) { - AnnotationUtils.rethrowAnnotationConfigurationException(ex); - throw new IllegalStateException("Failed to introspect annotations on " + element, ex); - } - return (!types.isEmpty() ? types : null); + return getMetaAnnotationTypes(element, element.getAnnotation(annotationType)); } /** @@ -153,51 +173,49 @@ public class AnnotatedElementUtils { public static Set getMetaAnnotationTypes(AnnotatedElement element, String annotationName) { Assert.notNull(element, "AnnotatedElement must not be null"); Assert.hasLength(annotationName, "annotationName must not be null or empty"); - final Set types = new LinkedHashSet(); + + return getMetaAnnotationTypes(element, AnnotationUtils.getAnnotation(element, annotationName)); + } + + private static Set getMetaAnnotationTypes(AnnotatedElement element, Annotation composed) { + if (composed == null) { + return null; + } try { - Annotation annotation = AnnotationUtils.getAnnotation(element, annotationName); - if (annotation != null) { - searchWithGetSemantics(annotation.annotationType(), null, annotationName, new SimpleAnnotationProcessor() { + final Set types = new LinkedHashSet(); + searchWithGetSemantics(composed.annotationType(), null, null, null, new SimpleAnnotationProcessor(true) { @Override public Object process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { types.add(annotation.annotationType().getName()); return CONTINUE; } }, new HashSet(), 1); - } + return (!types.isEmpty() ? types : null); } catch (Throwable ex) { AnnotationUtils.rethrowAnnotationConfigurationException(ex); throw new IllegalStateException("Failed to introspect annotations on " + element, ex); } - - return (!types.isEmpty() ? types : null); } /** * Determine if the supplied {@link AnnotatedElement} is annotated with * a composed annotation that is meta-annotated with an - * annotation of the specified {@code annotationName}. + * annotation of the specified {@code annotationType}. *

This method follows get semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element - * @param annotationType the annotation type on which to find meta-annotations + * @param annotationType the meta-annotation type to find * @return {@code true} if a matching meta-annotation is present * @since 4.2.3 * @see #getMetaAnnotationTypes */ - public static boolean hasMetaAnnotationTypes(AnnotatedElement element, final Class annotationType) { + public static boolean hasMetaAnnotationTypes(AnnotatedElement element, Class annotationType) { Assert.notNull(element, "AnnotatedElement must not be null"); Assert.notNull(annotationType, "annotationType must not be null"); - return Boolean.TRUE.equals(searchWithGetSemantics(element, annotationType, null, new SimpleAnnotationProcessor() { - @Override - public Boolean process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { - boolean found = (annotation.annotationType() == annotationType); - return (found && metaDepth > 0 ? Boolean.TRUE : CONTINUE); - } - })); + return hasMetaAnnotationTypes(element, annotationType, null); } /** @@ -212,21 +230,28 @@ public class AnnotatedElementUtils { * @return {@code true} if a matching meta-annotation is present * @see #getMetaAnnotationTypes */ - public static boolean hasMetaAnnotationTypes(AnnotatedElement element, final String annotationName) { + public static boolean hasMetaAnnotationTypes(AnnotatedElement element, String annotationName) { Assert.notNull(element, "AnnotatedElement must not be null"); Assert.hasLength(annotationName, "annotationName must not be null or empty"); - return Boolean.TRUE.equals(searchWithGetSemantics(element, null, annotationName, new SimpleAnnotationProcessor() { - @Override - public Boolean process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { - boolean found = annotation.annotationType().getName().equals(annotationName); - return (found && metaDepth > 0 ? Boolean.TRUE : CONTINUE); - } - })); + return hasMetaAnnotationTypes(element, null, annotationName); + } + + private static boolean hasMetaAnnotationTypes(AnnotatedElement element, Class annotationType, + String annotationName) { + + return Boolean.TRUE.equals( + searchWithGetSemantics(element, annotationType, annotationName, new SimpleAnnotationProcessor() { + + @Override + public Boolean process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { + return (metaDepth > 0 ? Boolean.TRUE : CONTINUE); + } + })); } /** - * Determine if an annotation of the specified {@code annotationName} + * Determine if an annotation of the specified {@code annotationType} * is present on the supplied {@link AnnotatedElement} or * within the annotation hierarchy above the specified element. *

If this method returns {@code true}, then {@link #getMergedAnnotationAttributes} @@ -234,21 +259,21 @@ public class AnnotatedElementUtils { *

This method follows get semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element - * @param annotationType the annotation type on which to find meta-annotations + * @param annotationType the annotation type to find * @return {@code true} if a matching annotation is present * @since 4.2.3 + * @see #hasAnnotation(AnnotatedElement, Class) */ - public static boolean isAnnotated(AnnotatedElement element, final Class annotationType) { + public static boolean isAnnotated(AnnotatedElement element, Class annotationType) { Assert.notNull(element, "AnnotatedElement must not be null"); Assert.notNull(annotationType, "annotationType must not be null"); - return Boolean.TRUE.equals(searchWithGetSemantics(element, annotationType, null, new SimpleAnnotationProcessor() { - @Override - public Boolean process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { - boolean found = annotation.annotationType() == annotationType; - return (found ? Boolean.TRUE : CONTINUE); - } - })); + // Shortcut: directly present on the element, with no processing needed? + if (element.isAnnotationPresent(annotationType)) { + return true; + } + + return Boolean.TRUE.equals(searchWithGetSemantics(element, annotationType, null, alwaysTrueAnnotationProcessor)); } /** @@ -263,40 +288,29 @@ public class AnnotatedElementUtils { * @param annotationName the fully qualified class name of the annotation type to find * @return {@code true} if a matching annotation is present */ - public static boolean isAnnotated(AnnotatedElement element, final String annotationName) { + public static boolean isAnnotated(AnnotatedElement element, String annotationName) { Assert.notNull(element, "AnnotatedElement must not be null"); Assert.hasLength(annotationName, "annotationName must not be null or empty"); - return Boolean.TRUE.equals(searchWithGetSemantics(element, null, annotationName, new SimpleAnnotationProcessor() { - @Override - public Boolean process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { - boolean found = annotation.annotationType().getName().equals(annotationName); - return (found ? Boolean.TRUE : CONTINUE); - } - })); + return Boolean.TRUE.equals(searchWithGetSemantics(element, null, annotationName, alwaysTrueAnnotationProcessor)); } /** - * Get the first annotation of the specified {@code annotationType} within - * the annotation hierarchy above the supplied {@code element}, - * merge that annotation's attributes with matching attributes from - * annotations in lower levels of the annotation hierarchy, and synthesize - * the result back into an annotation of the specified {@code annotationType}. - *

{@link AliasFor @AliasFor} semantics are fully supported, both - * within a single annotation and within the annotation hierarchy. - *

This method delegates to {@link #getMergedAnnotationAttributes(AnnotatedElement, Class)} - * and {@link AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement)}. - * @param element the annotated element - * @param annotationType the annotation type to find - * @return the merged, synthesized {@code Annotation}, or {@code null} if not found - * @since 4.2 - * @see #getMergedAnnotationAttributes(AnnotatedElement, Class) - * @see #findMergedAnnotation(AnnotatedElement, Class) - * @see AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement) + * @deprecated As of Spring Framework 4.2, use {@link #getMergedAnnotationAttributes(AnnotatedElement, String)} instead. */ - public static A getMergedAnnotation(AnnotatedElement element, Class annotationType) { - AnnotationAttributes attributes = getMergedAnnotationAttributes(element, annotationType); - return AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element); + @Deprecated + public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, String annotationName) { + return getMergedAnnotationAttributes(element, annotationName); + } + + /** + * @deprecated As of Spring Framework 4.2, use {@link #getMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean)} instead. + */ + @Deprecated + public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, String annotationName, + boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + + return getMergedAnnotationAttributes(element, annotationName, classValuesAsString, nestedAnnotationsAsMap); } /** @@ -321,7 +335,7 @@ public class AnnotatedElementUtils { Assert.notNull(annotationType, "annotationType must not be null"); AnnotationAttributes attributes = searchWithGetSemantics(element, annotationType, null, - new MergedAnnotationAttributesProcessor(annotationType, null, false, false)); + new MergedAnnotationAttributesProcessor()); AnnotationUtils.postProcessAnnotationAttributes(element, attributes, false, false); return attributes; } @@ -377,62 +391,238 @@ public class AnnotatedElementUtils { public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElement element, String annotationName, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + Assert.hasLength(annotationName, "annotationName must not be null or empty"); AnnotationAttributes attributes = searchWithGetSemantics(element, null, annotationName, - new MergedAnnotationAttributesProcessor(null, annotationName, classValuesAsString, nestedAnnotationsAsMap)); + new MergedAnnotationAttributesProcessor(classValuesAsString, nestedAnnotationsAsMap)); AnnotationUtils.postProcessAnnotationAttributes(element, attributes, classValuesAsString, nestedAnnotationsAsMap); return attributes; } /** - * Find the first annotation of the specified {@code annotationType} within + * Get the first annotation of the specified {@code annotationType} within * the annotation hierarchy above the supplied {@code element}, * merge that annotation's attributes with matching attributes from * annotations in lower levels of the annotation hierarchy, and synthesize * the result back into an annotation of the specified {@code annotationType}. *

{@link AliasFor @AliasFor} semantics are fully supported, both * within a single annotation and within the annotation hierarchy. + *

This method delegates to {@link #getMergedAnnotationAttributes(AnnotatedElement, Class)} + * and {@link AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement)}. * @param element the annotated element * @param annotationType the annotation type to find * @return the merged, synthesized {@code Annotation}, or {@code null} if not found * @since 4.2 - * @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) * @see #getMergedAnnotationAttributes(AnnotatedElement, Class) + * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement) */ - public static A findMergedAnnotation(AnnotatedElement element, Class annotationType) { + public static A getMergedAnnotation(AnnotatedElement element, Class annotationType) { Assert.notNull(annotationType, "annotationType must not be null"); - AnnotationAttributes attributes = findMergedAnnotationAttributes(element, annotationType, false, false); + + // Shortcut: directly present on the element, with no merging needed? + if (!(element instanceof Class)) { + // Do not use this shortcut against a Class: Inherited annotations + // would get preferred over locally declared composed annotations. + A annotation = element.getAnnotation(annotationType); + if (annotation != null) { + return AnnotationUtils.synthesizeAnnotation(annotation, element); + } + } + + // Exhaustive retrieval of merged annotation attributes... + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, annotationType); return AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element); } /** - * Find the first annotation of the specified {@code annotationName} within - * the annotation hierarchy above the supplied {@code element}, - * merge that annotation's attributes with matching attributes from - * annotations in lower levels of the annotation hierarchy, and synthesize - * the result back into an annotation of the specified {@code annotationName}. - *

{@link AliasFor @AliasFor} semantics are fully supported, both - * within a single annotation and within the annotation hierarchy. - *

This method delegates to {@link #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean)} - * (supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap}) - * and {@link AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement)}. + * Get all annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

{@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element; never {@code null} + * @param annotationType the annotation type to find; never {@code null} + * @return the set of all merged, synthesized {@code Annotations} found, or an empty + * set if none were found + * @since 4.3 + * @see #getMergedAnnotation(AnnotatedElement, Class) + * @see #getAllAnnotationAttributes(AnnotatedElement, String) + * @see #findAllMergedAnnotations(AnnotatedElement, Class) + */ + public static Set getAllMergedAnnotations(AnnotatedElement element, + Class annotationType) { + + Assert.notNull(element, "AnnotatedElement must not be null"); + Assert.notNull(annotationType, "annotationType must not be null"); + + MergedAnnotationAttributesProcessor processor = new MergedAnnotationAttributesProcessor(false, false, true); + searchWithGetSemantics(element, annotationType, null, processor); + return postProcessAndSynthesizeAggregatedResults(element, annotationType, processor.getAggregatedResults()); + } + + /** + * Get all repeatable annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

The container type that holds the repeatable annotations will be looked up + * via {@link java.lang.annotation.Repeatable}. + *

{@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element; never {@code null} + * @param annotationType the annotation type to find; never {@code null} + * @return the set of all merged repeatable {@code Annotations} found, or an empty + * set if none were found + * @since 4.3 + * @see #getMergedAnnotation(AnnotatedElement, Class) + * @see #getAllMergedAnnotations(AnnotatedElement, Class) + * @see #getMergedRepeatableAnnotations(AnnotatedElement, Class, Class) + * @throws IllegalArgumentException if the {@code element} or {@code annotationType} + * is {@code null}, or if the container type cannot be resolved + */ + public static Set getMergedRepeatableAnnotations(AnnotatedElement element, + Class annotationType) { + + return getMergedRepeatableAnnotations(element, annotationType, null); + } + + /** + * Get all repeatable annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

{@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element; never {@code null} + * @param annotationType the annotation type to find; never {@code null} + * @param containerType the type of the container that holds the annotations; + * may be {@code null} if the container type should be looked up via + * {@link java.lang.annotation.Repeatable} + * @return the set of all merged repeatable {@code Annotations} found, or an empty + * set if none were found + * @since 4.3 + * @see #getMergedAnnotation(AnnotatedElement, Class) + * @see #getAllMergedAnnotations(AnnotatedElement, Class) + * @throws IllegalArgumentException if the {@code element} or {@code annotationType} + * is {@code null}, or if the container type cannot be resolved + * @throws AnnotationConfigurationException if the supplied {@code containerType} + * is not a valid container annotation for the supplied {@code annotationType} + */ + public static Set getMergedRepeatableAnnotations(AnnotatedElement element, + Class annotationType, Class containerType) { + + Assert.notNull(element, "AnnotatedElement must not be null"); + Assert.notNull(annotationType, "annotationType must not be null"); + + if (containerType == null) { + containerType = resolveContainerType(annotationType); + } + else { + validateContainerType(annotationType, containerType); + } + + MergedAnnotationAttributesProcessor processor = new MergedAnnotationAttributesProcessor(false, false, true); + searchWithGetSemantics(element, annotationType, null, containerType, processor); + return postProcessAndSynthesizeAggregatedResults(element, annotationType, processor.getAggregatedResults()); + } + + /** + * Get the annotation attributes of all annotations of the specified + * {@code annotationName} in the annotation hierarchy above the supplied + * {@link AnnotatedElement} and store the results in a {@link MultiValueMap}. + *

Note: in contrast to {@link #getMergedAnnotationAttributes(AnnotatedElement, String)}, + * this method does not support attribute overrides. + *

This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element * @param annotationName the fully qualified class name of the annotation type to find - * @return the merged, synthesized {@code Annotation}, or {@code null} if not found - * @since 4.2 - * @see #findMergedAnnotation(AnnotatedElement, Class) - * @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) - * @see AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement) - * @deprecated As of Spring Framework 4.2.3, use {@link #findMergedAnnotation(AnnotatedElement, Class)} instead. + * @return a {@link MultiValueMap} keyed by attribute name, containing the annotation + * attributes from all annotations found, or {@code null} if not found + * @see #getAllAnnotationAttributes(AnnotatedElement, String, boolean, boolean) */ - @Deprecated - @SuppressWarnings("unchecked") - public static A findMergedAnnotation(AnnotatedElement element, String annotationName) { - AnnotationAttributes attributes = findMergedAnnotationAttributes(element, annotationName, false, false); - return AnnotationUtils.synthesizeAnnotation(attributes, (Class) attributes.annotationType(), element); + public static MultiValueMap getAllAnnotationAttributes(AnnotatedElement element, String annotationName) { + return getAllAnnotationAttributes(element, annotationName, false, false); } /** - * Find the first annotation of the specified {@code annotationName} within + * Get the annotation attributes of all annotations of + * the specified {@code annotationName} in the annotation hierarchy above + * the supplied {@link AnnotatedElement} and store the results in a + * {@link MultiValueMap}. + *

Note: in contrast to {@link #getMergedAnnotationAttributes(AnnotatedElement, String)}, + * this method does not support attribute overrides. + *

This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationName the fully qualified class name of the annotation type to find + * @param classValuesAsString whether to convert Class references into Strings or to + * preserve them as Class references + * @param nestedAnnotationsAsMap whether to convert nested Annotation instances into + * {@code AnnotationAttributes} maps or to preserve them as Annotation instances + * @return a {@link MultiValueMap} keyed by attribute name, containing the annotation + * attributes from all annotations found, or {@code null} if not found + */ + public static MultiValueMap getAllAnnotationAttributes(AnnotatedElement element, + String annotationName, final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) { + + final MultiValueMap attributesMap = new LinkedMultiValueMap(); + + searchWithGetSemantics(element, null, annotationName, new SimpleAnnotationProcessor() { + @Override + public Object process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { + AnnotationAttributes annotationAttributes = AnnotationUtils.getAnnotationAttributes( + annotation, classValuesAsString, nestedAnnotationsAsMap); + for (Map.Entry entry : annotationAttributes.entrySet()) { + attributesMap.add(entry.getKey(), entry.getValue()); + } + return CONTINUE; + } + }); + + return (!attributesMap.isEmpty() ? attributesMap : null); + } + + /** + * Determine if an annotation of the specified {@code annotationType} + * is available on the supplied {@link AnnotatedElement} or + * within the annotation hierarchy above the specified element. + *

If this method returns {@code true}, then {@link #findMergedAnnotationAttributes} + * will return a non-null value. + *

This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationType the annotation type to find + * @return {@code true} if a matching annotation is present + * @since 4.3 + * @see #isAnnotated(AnnotatedElement, Class) + */ + public static boolean hasAnnotation(AnnotatedElement element, Class annotationType) { + Assert.notNull(element, "AnnotatedElement must not be null"); + Assert.notNull(annotationType, "annotationType must not be null"); + + // Shortcut: directly present on the element, with no processing needed? + if (element.isAnnotationPresent(annotationType)) { + return true; + } + + return Boolean.TRUE.equals(searchWithFindSemantics(element, annotationType, null, alwaysTrueAnnotationProcessor)); + } + + /** + * Find the first annotation of the specified {@code annotationType} within * the annotation hierarchy above the supplied {@code element} and * merge that annotation's attributes with matching attributes from * annotations in lower levels of the annotation hierarchy. @@ -443,8 +633,8 @@ public class AnnotatedElementUtils { *

In contrast to {@link #getAllAnnotationAttributes}, the search * algorithm used by this method will stop searching the annotation * hierarchy once the first annotation of the specified - * {@code annotationName} has been found. As a consequence, additional - * annotations of the specified {@code annotationName} will be ignored. + * {@code annotationType} has been found. As a consequence, additional + * annotations of the specified {@code annotationType} will be ignored. *

This method follows find semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element @@ -463,8 +653,8 @@ public class AnnotatedElementUtils { public static AnnotationAttributes findMergedAnnotationAttributes(AnnotatedElement element, Class annotationType, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { - AnnotationAttributes attributes = searchWithFindSemantics(element, annotationType, annotationType.getName(), - new MergedAnnotationAttributesProcessor(annotationType, null, classValuesAsString, nestedAnnotationsAsMap)); + AnnotationAttributes attributes = searchWithFindSemantics(element, annotationType, null, + new MergedAnnotationAttributesProcessor(classValuesAsString, nestedAnnotationsAsMap)); AnnotationUtils.postProcessAnnotationAttributes(element, attributes, classValuesAsString, nestedAnnotationsAsMap); return attributes; } @@ -500,105 +690,217 @@ public class AnnotatedElementUtils { String annotationName, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { AnnotationAttributes attributes = searchWithFindSemantics(element, null, annotationName, - new MergedAnnotationAttributesProcessor(null, annotationName, classValuesAsString, nestedAnnotationsAsMap)); + new MergedAnnotationAttributesProcessor(classValuesAsString, nestedAnnotationsAsMap)); AnnotationUtils.postProcessAnnotationAttributes(element, attributes, classValuesAsString, nestedAnnotationsAsMap); return attributes; } /** - * @deprecated As of Spring Framework 4.2, use {@link #getMergedAnnotationAttributes(AnnotatedElement, String)} instead. + * Find the first annotation of the specified {@code annotationType} within + * the annotation hierarchy above the supplied {@code element}, + * merge that annotation's attributes with matching attributes from + * annotations in lower levels of the annotation hierarchy, and synthesize + * the result back into an annotation of the specified {@code annotationType}. + *

{@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. + *

This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationType the annotation type to find + * @return the merged, synthesized {@code Annotation}, or {@code null} if not found + * @since 4.2 + * @see #findAllMergedAnnotations(AnnotatedElement, Class) + * @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + * @see #getMergedAnnotationAttributes(AnnotatedElement, Class) */ - @Deprecated - public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, String annotationName) { - return getMergedAnnotationAttributes(element, annotationName); + public static A findMergedAnnotation(AnnotatedElement element, Class annotationType) { + Assert.notNull(annotationType, "annotationType must not be null"); + + // Shortcut: directly present on the element, with no merging needed? + if (!(element instanceof Class)) { + // Do not use this shortcut against a Class: Inherited annotations + // would get preferred over locally declared composed annotations. + A annotation = element.getAnnotation(annotationType); + if (annotation != null) { + return AnnotationUtils.synthesizeAnnotation(annotation, element); + } + } + + // Exhaustive retrieval of merged annotation attributes... + AnnotationAttributes attributes = findMergedAnnotationAttributes(element, annotationType, false, false); + return AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element); } /** - * @deprecated As of Spring Framework 4.2, use {@link #getMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean)} instead. + * Find the first annotation of the specified {@code annotationName} within + * the annotation hierarchy above the supplied {@code element}, + * merge that annotation's attributes with matching attributes from + * annotations in lower levels of the annotation hierarchy, and synthesize + * the result back into an annotation of the specified {@code annotationName}. + *

{@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. + *

This method delegates to {@link #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean)} + * (supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap}) + * and {@link AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement)}. + *

This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationName the fully qualified class name of the annotation type to find + * @return the merged, synthesized {@code Annotation}, or {@code null} if not found + * @since 4.2 + * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + * @see AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement) + * @deprecated As of Spring Framework 4.2.3, use {@link #findMergedAnnotation(AnnotatedElement, Class)} instead. */ @Deprecated - public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, String annotationName, - boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + @SuppressWarnings("unchecked") + public static A findMergedAnnotation(AnnotatedElement element, String annotationName) { + AnnotationAttributes attributes = findMergedAnnotationAttributes(element, annotationName, false, false); + return AnnotationUtils.synthesizeAnnotation(attributes, (Class) attributes.annotationType(), element); + } - return getMergedAnnotationAttributes(element, annotationName, classValuesAsString, nestedAnnotationsAsMap); + /** + * Find all annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

{@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element; never {@code null} + * @param annotationType the annotation type to find; never {@code null} + * @return the set of all merged, synthesized {@code Annotations} found, or an empty + * set if none were found + * @since 4.3 + * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see #getAllMergedAnnotations(AnnotatedElement, Class) + */ + public static Set findAllMergedAnnotations(AnnotatedElement element, + Class annotationType) { + + Assert.notNull(element, "AnnotatedElement must not be null"); + Assert.notNull(annotationType, "annotationType must not be null"); + + MergedAnnotationAttributesProcessor processor = new MergedAnnotationAttributesProcessor(false, false, true); + searchWithFindSemantics(element, annotationType, null, processor); + return postProcessAndSynthesizeAggregatedResults(element, annotationType, processor.getAggregatedResults()); } /** - * Get the annotation attributes of all annotations of the specified - * {@code annotationName} in the annotation hierarchy above the supplied - * {@link AnnotatedElement} and store the results in a {@link MultiValueMap}. - *

Note: in contrast to {@link #getMergedAnnotationAttributes(AnnotatedElement, String)}, - * this method does not support attribute overrides. - *

This method follows get semantics as described in the + * Find all repeatable annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

The container type that holds the repeatable annotations will be looked up + * via {@link java.lang.annotation.Repeatable}. + *

{@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

This method follows find semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. - * @param element the annotated element - * @param annotationName the fully qualified class name of the annotation type to find - * @return a {@link MultiValueMap} keyed by attribute name, containing the annotation - * attributes from all annotations found, or {@code null} if not found - * @see #getAllAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + * @param element the annotated element; never {@code null} + * @param annotationType the annotation type to find; never {@code null} + * @return the set of all merged repeatable {@code Annotations} found, or an empty + * set if none were found + * @since 4.3 + * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see #findAllMergedAnnotations(AnnotatedElement, Class) + * @see #findMergedRepeatableAnnotations(AnnotatedElement, Class, Class) + * @throws IllegalArgumentException if the {@code element} or {@code annotationType} + * is {@code null}, or if the container type cannot be resolved */ - public static MultiValueMap getAllAnnotationAttributes(AnnotatedElement element, String annotationName) { - return getAllAnnotationAttributes(element, annotationName, false, false); + public static Set findMergedRepeatableAnnotations(AnnotatedElement element, + Class annotationType) { + + return findMergedRepeatableAnnotations(element, annotationType, null); } /** - * Get the annotation attributes of all annotations of - * the specified {@code annotationName} in the annotation hierarchy above - * the supplied {@link AnnotatedElement} and store the results in a - * {@link MultiValueMap}. - *

Note: in contrast to {@link #getMergedAnnotationAttributes(AnnotatedElement, String)}, - * this method does not support attribute overrides. - *

This method follows get semantics as described in the + * Find all repeatable annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

{@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

This method follows find semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. - * @param element the annotated element - * @param annotationName the fully qualified class name of the annotation type to find - * @param classValuesAsString whether to convert Class references into Strings or to - * preserve them as Class references - * @param nestedAnnotationsAsMap whether to convert nested Annotation instances into - * {@code AnnotationAttributes} maps or to preserve them as Annotation instances - * @return a {@link MultiValueMap} keyed by attribute name, containing the annotation - * attributes from all annotations found, or {@code null} if not found + * @param element the annotated element; never {@code null} + * @param annotationType the annotation type to find; never {@code null} + * @param containerType the type of the container that holds the annotations; + * may be {@code null} if the container type should be looked up via + * {@link java.lang.annotation.Repeatable} + * @return the set of all merged repeatable {@code Annotations} found, or an empty + * set if none were found + * @since 4.3 + * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see #findAllMergedAnnotations(AnnotatedElement, Class) + * @throws IllegalArgumentException if the {@code element} or {@code annotationType} + * is {@code null}, or if the container type cannot be resolved + * @throws AnnotationConfigurationException if the supplied {@code containerType} + * is not a valid container annotation for the supplied {@code annotationType} */ - public static MultiValueMap getAllAnnotationAttributes(AnnotatedElement element, - final String annotationName, final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) { + public static Set findMergedRepeatableAnnotations(AnnotatedElement element, + Class annotationType, Class containerType) { - final MultiValueMap attributesMap = new LinkedMultiValueMap(); + Assert.notNull(element, "AnnotatedElement must not be null"); + Assert.notNull(annotationType, "annotationType must not be null"); - searchWithGetSemantics(element, null, annotationName, new SimpleAnnotationProcessor() { - @Override - public Void process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { - boolean found = annotation.annotationType().getName().equals(annotationName); - if (found) { - AnnotationAttributes annotationAttributes = AnnotationUtils.getAnnotationAttributes( - annotation, classValuesAsString, nestedAnnotationsAsMap); - for (Map.Entry entry : annotationAttributes.entrySet()) { - attributesMap.add(entry.getKey(), entry.getValue()); - } - } - // Continue searching... - return null; - } - }); + if (containerType == null) { + containerType = resolveContainerType(annotationType); + } + else { + validateContainerType(annotationType, containerType); + } - return (!attributesMap.isEmpty() ? attributesMap : null); + MergedAnnotationAttributesProcessor processor = new MergedAnnotationAttributesProcessor(false, false, true); + searchWithFindSemantics(element, annotationType, null, containerType, processor); + return postProcessAndSynthesizeAggregatedResults(element, annotationType, processor.getAggregatedResults()); } /** - * Search for annotations of the specified {@code annotationName} on - * the specified {@code element}, following get semantics. + * Search for annotations of the specified {@code annotationName} or + * {@code annotationType} on the specified {@code element}, following + * get semantics. * @param element the annotated element - * @param annotationType the annotation type on which to find meta-annotations + * @param annotationType the annotation type to find + * @param annotationName the fully qualified class name of the annotation + * type to find (as an alternative to {@code annotationType}) + * @param processor the processor to delegate to + * @return the result of the processor, potentially {@code null} + */ + private static T searchWithGetSemantics(AnnotatedElement element, Class annotationType, + String annotationName, Processor processor) { + + return searchWithGetSemantics(element, annotationType, annotationName, null, processor); + } + + /** + * Search for annotations of the specified {@code annotationName} or + * {@code annotationType} on the specified {@code element}, following + * get semantics. + * @param element the annotated element + * @param annotationType the annotation type to find * @param annotationName the fully qualified class name of the annotation * type to find (as an alternative to {@code annotationType}) + * @param containerType the type of the container that holds repeatable + * annotations, or {@code null} if the annotation is not repeatable * @param processor the processor to delegate to * @return the result of the processor, potentially {@code null} + * @since 4.3 */ - private static T searchWithGetSemantics(AnnotatedElement element, - Class annotationType, String annotationName, Processor processor) { + private static T searchWithGetSemantics(AnnotatedElement element, Class annotationType, + String annotationName, Class containerType, Processor processor) { try { - return searchWithGetSemantics( - element, annotationType, annotationName, processor, new HashSet(), 0); + return searchWithGetSemantics(element, annotationType, annotationName, containerType, processor, + new HashSet(), 0); } catch (Throwable ex) { AnnotationUtils.rethrowAnnotationConfigurationException(ex); @@ -613,17 +915,19 @@ public class AnnotatedElementUtils { *

The {@code metaDepth} parameter is explained in the * {@link Processor#process process()} method of the {@link Processor} API. * @param element the annotated element - * @param annotationType the annotation type on which to find meta-annotations + * @param annotationType the annotation type to find * @param annotationName the fully qualified class name of the annotation * type to find (as an alternative to {@code annotationType}) + * @param containerType the type of the container that holds repeatable + * annotations, or {@code null} if the annotation is not repeatable * @param processor the processor to delegate to * @param visited the set of annotated elements that have already been visited * @param metaDepth the meta-depth of the annotation * @return the result of the processor, potentially {@code null} */ - private static T searchWithGetSemantics(AnnotatedElement element, - Class annotationType, String annotationName, - Processor processor, Set visited, int metaDepth) { + private static T searchWithGetSemantics(AnnotatedElement element, Class annotationType, + String annotationName, Class containerType, Processor processor, + Set visited, int metaDepth) { Assert.notNull(element, "AnnotatedElement must not be null"); @@ -632,12 +936,12 @@ public class AnnotatedElementUtils { // Start searching within locally declared annotations List declaredAnnotations = Arrays.asList(element.getDeclaredAnnotations()); T result = searchWithGetSemanticsInAnnotations(element, declaredAnnotations, - annotationType, annotationName, processor, visited, metaDepth); + annotationType, annotationName, containerType, processor, visited, metaDepth); if (result != null) { return result; } - if (element instanceof Class) { // otherwise getAnnotations doesn't return anything new + if (element instanceof Class) { // otherwise getAnnotations doesn't return anything new List inheritedAnnotations = new ArrayList(); for (Annotation annotation : element.getAnnotations()) { if (!declaredAnnotations.contains(annotation)) { @@ -647,7 +951,7 @@ public class AnnotatedElementUtils { // Continue searching within inherited annotations result = searchWithGetSemanticsInAnnotations(element, inheritedAnnotations, - annotationType, annotationName, processor, visited, metaDepth); + annotationType, annotationName, containerType, processor, visited, metaDepth); if (result != null) { return result; } @@ -662,39 +966,58 @@ public class AnnotatedElementUtils { } /** - * This method is invoked by - * {@link #searchWithGetSemantics(AnnotatedElement, Class, String, Processor, Set, int)} - * to perform the actual search within the supplied list of annotations. + * This method is invoked by {@link #searchWithGetSemantics} to perform + * the actual search within the supplied list of annotations. *

This method should be invoked first with locally declared annotations * and then subsequently with inherited annotations, thereby allowing * local annotations to take precedence over inherited annotations. *

The {@code metaDepth} parameter is explained in the * {@link Processor#process process()} method of the {@link Processor} API. - * @param annotatedElement the element that is annotated with the supplied + * @param element the element that is annotated with the supplied * annotations, used for contextual logging; may be {@code null} if unknown * @param annotations the annotations to search in - * @param annotationType the annotation type on which to find meta-annotations + * @param annotationType the annotation type to find * @param annotationName the fully qualified class name of the annotation * type to find (as an alternative to {@code annotationType}) + * @param containerType the type of the container that holds repeatable + * annotations, or {@code null} if the annotation is not repeatable * @param processor the processor to delegate to * @param visited the set of annotated elements that have already been visited * @param metaDepth the meta-depth of the annotation * @return the result of the processor, potentially {@code null} * @since 4.2 */ - private static T searchWithGetSemanticsInAnnotations(AnnotatedElement annotatedElement, + private static T searchWithGetSemanticsInAnnotations(AnnotatedElement element, List annotations, Class annotationType, String annotationName, - Processor processor, Set visited, int metaDepth) { + Class containerType, Processor processor, Set visited, + int metaDepth) { // Search in annotations for (Annotation annotation : annotations) { - if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation) && - ((annotationType != null ? annotation.annotationType() == annotationType : - annotation.annotationType().getName().equals(annotationName)) || - metaDepth > 0)) { - T result = processor.process(annotatedElement, annotation, metaDepth); - if (result != null) { - return result; + if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { + if (annotation.annotationType() == annotationType || + annotation.annotationType().getName().equals(annotationName) || + processor.alwaysProcesses()) { + T result = processor.process(element, annotation, metaDepth); + if (result != null) { + if (processor.aggregates() && metaDepth == 0) { + processor.getAggregatedResults().add(result); + } + else { + return result; + } + } + } + // Repeatable annotations in container? + else if (annotation.annotationType() == containerType) { + for (Annotation contained : getRawAnnotationsFromContainer(element, annotation)) { + T result = processor.process(element, contained, metaDepth); + if (result != null) { + // No need to post-process since repeatable annotations within a + // container cannot be composed annotations. + processor.getAggregatedResults().add(result); + } + } } } } @@ -703,10 +1026,15 @@ public class AnnotatedElementUtils { for (Annotation annotation : annotations) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { T result = searchWithGetSemantics(annotation.annotationType(), annotationType, - annotationName, processor, visited, metaDepth + 1); + annotationName, containerType, processor, visited, metaDepth + 1); if (result != null) { - processor.postProcess(annotatedElement, annotation, result); - return result; + processor.postProcess(element, annotation, result); + if (processor.aggregates() && metaDepth == 0) { + processor.getAggregatedResults().add(result); + } + else { + return result; + } } } } @@ -715,22 +1043,48 @@ public class AnnotatedElementUtils { } /** - * Search for annotations of the specified {@code annotationName} on - * the specified {@code element}, following find semantics. + * Search for annotations of the specified {@code annotationName} or + * {@code annotationType} on the specified {@code element}, following + * find semantics. * @param element the annotated element - * @param annotationType the annotation type on which to find meta-annotations + * @param annotationType the annotation type to find * @param annotationName the fully qualified class name of the annotation * type to find (as an alternative to {@code annotationType}) * @param processor the processor to delegate to * @return the result of the processor, potentially {@code null} * @since 4.2 */ - private static T searchWithFindSemantics( - AnnotatedElement element, Class annotationType, String annotationName, Processor processor) { + private static T searchWithFindSemantics(AnnotatedElement element, Class annotationType, + String annotationName, Processor processor) { + + return searchWithFindSemantics(element, annotationType, annotationName, null, processor); + } + + /** + * Search for annotations of the specified {@code annotationName} or + * {@code annotationType} on the specified {@code element}, following + * find semantics. + * @param element the annotated element + * @param annotationType the annotation type to find + * @param annotationName the fully qualified class name of the annotation + * type to find (as an alternative to {@code annotationType}) + * @param containerType the type of the container that holds repeatable + * annotations, or {@code null} if the annotation is not repeatable + * @param processor the processor to delegate to + * @return the result of the processor, potentially {@code null} + * @since 4.3 + */ + private static T searchWithFindSemantics(AnnotatedElement element, Class annotationType, + String annotationName, Class containerType, Processor processor) { + + if (containerType != null && !processor.aggregates()) { + throw new IllegalArgumentException( + "Searches for repeatable annotations must supply an aggregating Processor"); + } try { return searchWithFindSemantics( - element, annotationType, annotationName, processor, new HashSet(), 0); + element, annotationType, annotationName, containerType, processor, new HashSet(), 0); } catch (Throwable ex) { AnnotationUtils.rethrowAnnotationConfigurationException(ex); @@ -744,10 +1098,12 @@ public class AnnotatedElementUtils { * have already been visited. *

The {@code metaDepth} parameter is explained in the * {@link Processor#process process()} method of the {@link Processor} API. - * @param element the annotated element - * @param annotationType the annotation type on which to find meta-annotations + * @param element the annotated element; never {@code null} + * @param annotationType the annotation type to find * @param annotationName the fully qualified class name of the annotation * type to find (as an alternative to {@code annotationType}) + * @param containerType the type of the container that holds repeatable + * annotations, or {@code null} if the annotation is not repeatable * @param processor the processor to delegate to * @param visited the set of annotated elements that have already been visited * @param metaDepth the meta-depth of the annotation @@ -755,25 +1111,44 @@ public class AnnotatedElementUtils { * @since 4.2 */ private static T searchWithFindSemantics(AnnotatedElement element, Class annotationType, - String annotationName, Processor processor, Set visited, int metaDepth) { + String annotationName, Class containerType, Processor processor, + Set visited, int metaDepth) { Assert.notNull(element, "AnnotatedElement must not be null"); - Assert.hasLength(annotationName, "annotationName must not be null or empty"); if (visited.add(element)) { try { // Locally declared annotations (ignoring @Inherited) Annotation[] annotations = element.getDeclaredAnnotations(); + List aggregatedResults = (processor.aggregates() ? new ArrayList() : null); // Search in local annotations for (Annotation annotation : annotations) { - if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation) && - ((annotationType != null ? annotation.annotationType() == annotationType : - annotation.annotationType().getName().equals(annotationName)) || - metaDepth > 0)) { - T result = processor.process(element, annotation, metaDepth); - if (result != null) { - return result; + if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { + if (annotation.annotationType() == annotationType + || annotation.annotationType().getName().equals(annotationName) + || processor.alwaysProcesses()) { + + T result = processor.process(element, annotation, metaDepth); + if (result != null) { + if (processor.aggregates() && metaDepth == 0) { + aggregatedResults.add(result); + } + else { + return result; + } + } + } + // Repeatable annotations in container? + else if (annotation.annotationType() == containerType) { + for (Annotation contained : getRawAnnotationsFromContainer(element, annotation)) { + T result = processor.process(element, contained, metaDepth); + if (result != null) { + // No need to post-process since repeatable annotations within a + // container cannot be composed annotations. + aggregatedResults.add(result); + } + } } } } @@ -781,21 +1156,31 @@ public class AnnotatedElementUtils { // Search in meta annotations on local annotations for (Annotation annotation : annotations) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { - T result = searchWithFindSemantics( - annotation.annotationType(), annotationType, annotationName, processor, visited, metaDepth + 1); + T result = searchWithFindSemantics(annotation.annotationType(), annotationType, annotationName, + containerType, processor, visited, metaDepth + 1); if (result != null) { processor.postProcess(annotation.annotationType(), annotation, result); - return result; + if (processor.aggregates() && metaDepth == 0) { + aggregatedResults.add(result); + } + else { + return result; + } } } } + if (processor.aggregates()) { + // Prepend to support top-down ordering within class hierarchies + processor.getAggregatedResults().addAll(0, aggregatedResults); + } + if (element instanceof Method) { Method method = (Method) element; // Search on possibly bridged method Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method); - T result = searchWithFindSemantics(resolvedMethod, annotationType, annotationName, + T result = searchWithFindSemantics(resolvedMethod, annotationType, annotationName, containerType, processor, visited, metaDepth); if (result != null) { return result; @@ -803,8 +1188,8 @@ public class AnnotatedElementUtils { // Search on methods in interfaces declared locally Class[] ifcs = method.getDeclaringClass().getInterfaces(); - result = searchOnInterfaces( - method, annotationType, annotationName, processor, visited, metaDepth, ifcs); + result = searchOnInterfaces(method, annotationType, annotationName, containerType, processor, + visited, metaDepth, ifcs); if (result != null) { return result; } @@ -821,7 +1206,7 @@ public class AnnotatedElementUtils { Method equivalentMethod = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); Method resolvedEquivalentMethod = BridgeMethodResolver.findBridgedMethod(equivalentMethod); result = searchWithFindSemantics(resolvedEquivalentMethod, annotationType, annotationName, - processor, visited, metaDepth); + containerType, processor, visited, metaDepth); if (result != null) { return result; } @@ -831,21 +1216,20 @@ public class AnnotatedElementUtils { } // Search on interfaces declared on superclass - result = searchOnInterfaces(method, annotationType, annotationName, processor, visited, - metaDepth, clazz.getInterfaces()); + result = searchOnInterfaces(method, annotationType, annotationName, containerType, processor, + visited, metaDepth, clazz.getInterfaces()); if (result != null) { return result; } } } - - if (element instanceof Class) { + else if (element instanceof Class) { Class clazz = (Class) element; // Search on interfaces for (Class ifc : clazz.getInterfaces()) { - T result = searchWithFindSemantics( - ifc, annotationType, annotationName, processor, visited, metaDepth); + T result = searchWithFindSemantics(ifc, annotationType, annotationName, containerType, + processor, visited, metaDepth); if (result != null) { return result; } @@ -854,8 +1238,8 @@ public class AnnotatedElementUtils { // Search on superclass Class superclass = clazz.getSuperclass(); if (superclass != null && Object.class != superclass) { - T result = searchWithFindSemantics( - superclass, annotationType, annotationName, processor, visited, metaDepth); + T result = searchWithFindSemantics(superclass, annotationType, annotationName, containerType, + processor, visited, metaDepth); if (result != null) { return result; } @@ -869,14 +1253,15 @@ public class AnnotatedElementUtils { return null; } - private static T searchOnInterfaces(Method method, Class annotationType, String annotationName, - Processor processor, Set visited, int metaDepth, Class[] ifcs) { + private static T searchOnInterfaces(Method method, Class annotationType, + String annotationName, Class containerType, Processor processor, + Set visited, int metaDepth, Class[] ifcs) { for (Class iface : ifcs) { if (AnnotationUtils.isInterfaceWithAnnotatedMethods(iface)) { try { Method equivalentMethod = iface.getMethod(method.getName(), method.getParameterTypes()); - T result = searchWithFindSemantics(equivalentMethod, annotationType, annotationName, + T result = searchWithFindSemantics(equivalentMethod, annotationType, annotationName, containerType, processor, visited, metaDepth); if (result != null) { return result; @@ -891,6 +1276,87 @@ public class AnnotatedElementUtils { return null; } + /** + * Get the array of raw (unsynthesized) annotations from the {@code value} + * attribute of the supplied repeatable annotation {@code container}. + * @since 4.3 + */ + @SuppressWarnings("unchecked") + private static A[] getRawAnnotationsFromContainer(AnnotatedElement element, + Annotation container) { + + try { + return (A[]) AnnotationUtils.getValue(container); + } + catch (Exception ex) { + AnnotationUtils.handleIntrospectionFailure(element, ex); + } + // Unable to read value from repeating annotation container -> ignore it. + return (A[]) EMPTY_ANNOTATION_ARRAY; + } + + /** + * Resolve the container type for the supplied repeatable {@code annotationType}. + *

Delegates to {@link AnnotationUtils#resolveContainerAnnotationType(Class)}. + * @param annotationType the annotation type to resolve the container for + * @return the container type; never {@code null} + * @throws IllegalArgumentException if the container type cannot be resolved + * @since 4.3 + */ + private static Class resolveContainerType(Class annotationType) { + Class containerType = AnnotationUtils.resolveContainerAnnotationType(annotationType); + if (containerType == null) { + throw new IllegalArgumentException( + "annotationType must be a repeatable annotation: failed to resolve container type for " + + annotationType.getName()); + } + return containerType; + } + + /** + * Validate that the supplied {@code containerType} is a proper container + * annotation for the supplied repeatable {@code annotationType} (i.e., + * that it declares a {@code value} attribute that holds an array of the + * {@code annotationType}). + * @since 4.3 + * @throws AnnotationConfigurationException if the supplied {@code containerType} + * is not a valid container annotation for the supplied {@code annotationType} + */ + private static void validateContainerType(Class annotationType, + Class containerType) { + + try { + Method method = containerType.getDeclaredMethod(AnnotationUtils.VALUE); + Class returnType = method.getReturnType(); + if (!returnType.isArray() || returnType.getComponentType() != annotationType) { + String msg = String.format( + "Container type [%s] must declare a 'value' attribute for an array of type [%s]", + containerType.getName(), annotationType.getName()); + throw new AnnotationConfigurationException(msg); + } + } + catch (Exception ex) { + AnnotationUtils.rethrowAnnotationConfigurationException(ex); + String msg = String.format("Invalid declaration of container type [%s] for repeatable annotation [%s]", + containerType.getName(), annotationType.getName()); + throw new AnnotationConfigurationException(msg, ex); + } + } + + /** + * @since 4.3 + */ + private static Set postProcessAndSynthesizeAggregatedResults(AnnotatedElement element, + Class annotationType, List aggregatedResults) { + + Set annotations = new LinkedHashSet(); + for (AnnotationAttributes attributes : aggregatedResults) { + AnnotationUtils.postProcessAnnotationAttributes(element, attributes, false, false); + annotations.add(AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element)); + } + return annotations; + } + /** * Callback interface that is used to process annotations during a search. @@ -899,11 +1365,16 @@ public class AnnotatedElementUtils { * annotations, or all annotations discovered by the currently executing * search. The term "target" in this context refers to a matching * annotation (i.e., a specific annotation type that was found during - * the search). Returning a non-null value from the {@link #process} + * the search). + *

Returning a non-null value from the {@link #process} * method instructs the search algorithm to stop searching further; * whereas, returning {@code null} from the {@link #process} method * instructs the search algorithm to continue searching for additional - * annotations. + * annotations. One exception to this rule applies to processors + * that {@linkplain #aggregates aggregate} results. If an aggregating + * processor returns a non-null value, that value will be added to the + * list of {@linkplain #getAggregatedResults aggregated results} + * and the search algorithm will continue. *

Processors can optionally {@linkplain #postProcess post-process} * the result of the {@link #process} method as the search algorithm * goes back down the annotation hierarchy from an invocation of @@ -916,11 +1387,12 @@ public class AnnotatedElementUtils { /** * Process the supplied annotation. - *

Depending on the use case, the supplied annotation may be an - * actual target annotation that has been found by the search - * algorithm, or it may be some other annotation within the + *

The supplied annotation will be an actual target annotation + * that has been found by the search algorithm, unless this processor + * is configured to {@linkplain #alwaysProcesses always process} + * annotations in which case it may be some other annotation within an * annotation hierarchy. In the latter case, the {@code metaDepth} - * should have a value greater than {@code 0}. In any case, it is + * will have a value greater than {@code 0}. In any case, it is * up to concrete implementations of this method to decide what to * do with the supplied annotation. *

The {@code metaDepth} parameter represents the depth of the @@ -952,20 +1424,93 @@ public class AnnotatedElementUtils { * @param result the result to post-process */ void postProcess(AnnotatedElement annotatedElement, Annotation annotation, T result); + + /** + * Determine if this processor always processes annotations regardless of + * whether or not the target annotation has been found. + * @return {@code true} if this processor always processes annotations + * @since 4.3 + */ + boolean alwaysProcesses(); + + /** + * Determine if this processor aggregates the results returned by {@link #process}. + *

If this method returns {@code true}, then {@link #getAggregatedResults()} + * must return a non-null value. + * @return {@code true} if this processor supports aggregated results + * @see #getAggregatedResults + * @since 4.3 + */ + boolean aggregates(); + + /** + * Get the list of results aggregated by this processor. + *

NOTE: the processor does not aggregate the results + * itself. Rather, the search algorithm that uses this processor is + * responsible for asking this processor if it {@link #aggregates} results + * and then adding the post-processed results to the list returned by this + * method. + * @return the list of results aggregated by this processor; never + * {@code null} unless {@link #aggregates} returns {@code false} + * @see #aggregates + * @since 4.3 + */ + List getAggregatedResults(); } /** - * {@link Processor} that {@linkplain #process processes} annotations - * but does not {@linkplain #postProcess post-process} results. + * {@link Processor} that {@linkplain #process(AnnotatedElement, Annotation, int) + * processes} annotations but does not {@linkplain #postProcess post-process} or + * {@linkplain #aggregates aggregate} results. * @since 4.2 */ private abstract static class SimpleAnnotationProcessor implements Processor { + private final boolean alwaysProcesses; + + public SimpleAnnotationProcessor() { + this(false); + } + + public SimpleAnnotationProcessor(boolean alwaysProcesses) { + this.alwaysProcesses = alwaysProcesses; + } + + @Override + public final boolean alwaysProcesses() { + return this.alwaysProcesses; + } + @Override public final void postProcess(AnnotatedElement annotatedElement, Annotation annotation, T result) { // no-op } + + @Override + public final boolean aggregates() { + return false; + } + + @Override + public final List getAggregatedResults() { + throw new UnsupportedOperationException("SimpleAnnotationProcessor does not support aggregated results"); + } + } + + + /** + * {@link SimpleAnnotationProcessor} that always returns {@link Boolean#TRUE} when + * asked to {@linkplain #process(AnnotatedElement, Annotation, int) process} an + * annotation. + * @since 4.3 + */ + static class AlwaysTrueBooleanAnnotationProcessor extends SimpleAnnotationProcessor { + + @Override + public final Boolean process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { + return Boolean.TRUE; + } } @@ -974,35 +1519,58 @@ public class AnnotatedElementUtils { * target annotation during the {@link #process} phase and then merges * annotation attributes from lower levels in the annotation hierarchy * during the {@link #postProcess} phase. + *

A {@code MergedAnnotationAttributesProcessor} may optionally be + * configured to {@linkplain #aggregates aggregate} results. * @since 4.2 - * @see AnnotationUtils#retrieveAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean) + * @see AnnotationUtils#retrieveAnnotationAttributes * @see AnnotationUtils#postProcessAnnotationAttributes */ private static class MergedAnnotationAttributesProcessor implements Processor { - private final Class annotationType; - - private final String annotationName; - private final boolean classValuesAsString; private final boolean nestedAnnotationsAsMap; - MergedAnnotationAttributesProcessor(Class annotationType, String annotationName, - boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + private final boolean aggregates; + + private final List aggregatedResults; + + MergedAnnotationAttributesProcessor() { + this(false, false, false); + } + + MergedAnnotationAttributesProcessor(boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + this(classValuesAsString, nestedAnnotationsAsMap, false); + } + + MergedAnnotationAttributesProcessor(boolean classValuesAsString, boolean nestedAnnotationsAsMap, + boolean aggregates) { - this.annotationType = annotationType; - this.annotationName = annotationName; this.classValuesAsString = classValuesAsString; this.nestedAnnotationsAsMap = nestedAnnotationsAsMap; + this.aggregates = aggregates; + this.aggregatedResults = (aggregates ? new ArrayList() : null); + } + + @Override + public boolean alwaysProcesses() { + return false; + } + + @Override + public boolean aggregates() { + return this.aggregates; + } + + @Override + public List getAggregatedResults() { + return this.aggregatedResults; } @Override public AnnotationAttributes process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { - boolean found = (this.annotationType != null ? annotation.annotationType() == this.annotationType : - annotation.annotationType().getName().equals(this.annotationName)); - return (found ? AnnotationUtils.retrieveAnnotationAttributes(annotatedElement, annotation, - this.classValuesAsString, this.nestedAnnotationsAsMap) : null); + return AnnotationUtils.retrieveAnnotationAttributes(annotatedElement, annotation, + this.classValuesAsString, this.nestedAnnotationsAsMap); } @Override @@ -1010,15 +1578,36 @@ public class AnnotatedElementUtils { annotation = AnnotationUtils.synthesizeAnnotation(annotation, element); Class targetAnnotationType = attributes.annotationType(); + // Track which attribute values have already been replaced so that we can short + // circuit the search algorithms. + Set valuesAlreadyReplaced = new HashSet(); + for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) { String attributeName = attributeMethod.getName(); String attributeOverrideName = AnnotationUtils.getAttributeOverrideName(attributeMethod, targetAnnotationType); // Explicit annotation attribute override declared via @AliasFor if (attributeOverrideName != null) { - if (attributes.containsKey(attributeOverrideName)) { - overrideAttribute(element, annotation, attributes, attributeName, attributeOverrideName); + if (valuesAlreadyReplaced.contains(attributeOverrideName)) { + continue; + } + + List targetAttributeNames = new ArrayList(); + targetAttributeNames.add(attributeOverrideName); + valuesAlreadyReplaced.add(attributeOverrideName); + + // Ensure all aliased attributes in the target annotation are overridden. (SPR-14069) + List aliases = AnnotationUtils.getAttributeAliasMap(targetAnnotationType).get(attributeOverrideName); + if (aliases != null) { + for (String alias : aliases) { + if (!valuesAlreadyReplaced.contains(alias)) { + targetAttributeNames.add(alias); + valuesAlreadyReplaced.add(alias); + } + } } + + overrideAttributes(element, annotation, attributes, attributeName, targetAttributeNames); } // Implicit annotation attribute override based on convention else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) { @@ -1027,13 +1616,25 @@ public class AnnotatedElementUtils { } } - private void overrideAttribute(AnnotatedElement element, Annotation annotation, - AnnotationAttributes attributes, String sourceAttributeName, String targetAttributeName) { + private void overrideAttributes(AnnotatedElement element, Annotation annotation, + AnnotationAttributes attributes, String sourceAttributeName, List targetAttributeNames) { + + Object adaptedValue = getAdaptedValue(element, annotation, sourceAttributeName); + + for (String targetAttributeName : targetAttributeNames) { + attributes.put(targetAttributeName, adaptedValue); + } + } + + private void overrideAttribute(AnnotatedElement element, Annotation annotation, AnnotationAttributes attributes, + String sourceAttributeName, String targetAttributeName) { + + attributes.put(targetAttributeName, getAdaptedValue(element, annotation, sourceAttributeName)); + } + private Object getAdaptedValue(AnnotatedElement element, Annotation annotation, String sourceAttributeName) { Object value = AnnotationUtils.getValue(annotation, sourceAttributeName); - Object adaptedValue = AnnotationUtils.adaptValue( - element, value, this.classValuesAsString, this.nestedAnnotationsAsMap); - attributes.put(targetAttributeName, adaptedValue); + return AnnotationUtils.adaptValue(element, value, this.classValuesAsString, this.nestedAnnotationsAsMap); } } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributeExtractor.java index 86ca194b..7fd8dd42 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributeExtractor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributeExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; /** @@ -44,7 +43,7 @@ interface AnnotationAttributeExtractor { * type supported by this extractor. * @return the annotated element, or {@code null} if unknown */ - AnnotatedElement getAnnotatedElement(); + Object getAnnotatedElement(); /** * Get the underlying source of annotation attributes. diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java index 395d2dba..0cf257d6 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,7 @@ import org.springframework.util.StringUtils; * *

Provides 'pseudo-reification' to avoid noisy Map generics in the calling * code as well as convenience methods for looking up annotation attributes - * in a type-safe fashion, including support for attribute aliases configured - * via {@link AliasFor @AliasFor}. + * in a type-safe fashion. * * @author Chris Beams * @author Sam Brannen @@ -45,22 +44,36 @@ import org.springframework.util.StringUtils; * @since 3.1.1 * @see AnnotationUtils#getAnnotationAttributes * @see AnnotatedElementUtils - * @see AliasFor */ @SuppressWarnings("serial") public class AnnotationAttributes extends LinkedHashMap { + private static final String UNKNOWN = "unknown"; + private final Class annotationType; private final String displayName; + boolean validated = false; + /** * Create a new, empty {@link AnnotationAttributes} instance. */ public AnnotationAttributes() { this.annotationType = null; - this.displayName = "unknown"; + this.displayName = UNKNOWN; + } + + /** + * Create a new, empty {@link AnnotationAttributes} instance with the + * given initial capacity to optimize performance. + * @param initialCapacity initial size of the underlying map + */ + public AnnotationAttributes(int initialCapacity) { + super(initialCapacity); + this.annotationType = null; + this.displayName = UNKNOWN; } /** @@ -71,33 +84,62 @@ public class AnnotationAttributes extends LinkedHashMap { * @since 4.2 */ public AnnotationAttributes(Class annotationType) { - Assert.notNull(annotationType, "annotationType must not be null"); + Assert.notNull(annotationType, "'annotationType' must not be null"); this.annotationType = annotationType; this.displayName = annotationType.getName(); } /** - * Create a new, empty {@link AnnotationAttributes} instance with the - * given initial capacity to optimize performance. - * @param initialCapacity initial size of the underlying map + * Create a new, empty {@link AnnotationAttributes} instance for the + * specified {@code annotationType}. + * @param annotationType the annotation type name represented by this + * {@code AnnotationAttributes} instance; never {@code null} + * @param classLoader the ClassLoader to try to load the annotation type on, + * or {@code null} to just store the annotation type name + * @since 4.3.2 */ - public AnnotationAttributes(int initialCapacity) { - super(initialCapacity); - this.annotationType = null; - this.displayName = "unknown"; + public AnnotationAttributes(String annotationType, ClassLoader classLoader) { + Assert.notNull(annotationType, "'annotationType' must not be null"); + this.annotationType = getAnnotationType(annotationType, classLoader); + this.displayName = annotationType; + } + + @SuppressWarnings("unchecked") + private static Class getAnnotationType(String annotationType, ClassLoader classLoader) { + if (classLoader != null) { + try { + return (Class) classLoader.loadClass(annotationType); + } + catch (ClassNotFoundException ex) { + // Annotation Class not resolvable + } + } + return null; } /** - * Create a new {@link AnnotationAttributes} instance, wrapping the - * provided map and all its key-value pairs. - * @param map original source of annotation attribute key-value - * pairs + * Create a new {@link AnnotationAttributes} instance, wrapping the provided + * map and all its key-value pairs. + * @param map original source of annotation attribute key-value pairs * @see #fromMap(Map) */ public AnnotationAttributes(Map map) { super(map); this.annotationType = null; - this.displayName = "unknown"; + this.displayName = UNKNOWN; + } + + /** + * Create a new {@link AnnotationAttributes} instance, wrapping the provided + * map and all its key-value pairs. + * @param other original source of annotation attribute key-value pairs + * @see #fromMap(Map) + */ + public AnnotationAttributes(AnnotationAttributes other) { + super(other); + this.annotationType = other.annotationType; + this.displayName = other.displayName; + this.validated = other.validated; } @@ -144,8 +186,10 @@ public class AnnotationAttributes extends LinkedHashMap { * @throws AnnotationConfigurationException if the attribute and its * alias are both present with different non-empty values * @since 4.2 - * @see ObjectUtils#isEmpty(Object) + * @deprecated as of Spring 4.3.2, in favor of built-in alias resolution + * in {@link #getString} itself */ + @Deprecated public String getAliasedString(String attributeName, Class annotationType, Object annotationSource) { @@ -188,7 +232,10 @@ public class AnnotationAttributes extends LinkedHashMap { * @throws AnnotationConfigurationException if the attribute and its * alias are both present with different non-empty values * @since 4.2 + * @deprecated as of Spring 4.3.2, in favor of built-in alias resolution + * in {@link #getStringArray} itself */ + @Deprecated public String[] getAliasedStringArray(String attributeName, Class annotationType, Object annotationSource) { @@ -286,7 +333,10 @@ public class AnnotationAttributes extends LinkedHashMap { * @throws AnnotationConfigurationException if the attribute and its * alias are both present with different non-empty values * @since 4.2 + * @deprecated as of Spring 4.3.2, in favor of built-in alias resolution + * in {@link #getClassArray} itself */ + @Deprecated public Class[] getAliasedClassArray(String attributeName, Class annotationType, Object annotationSource) { @@ -378,7 +428,7 @@ public class AnnotationAttributes extends LinkedHashMap { */ @SuppressWarnings("unchecked") private T getRequiredAttribute(String attributeName, Class expectedType) { - Assert.hasText(attributeName, "attributeName must not be null or empty"); + Assert.hasText(attributeName, "'attributeName' must not be null or empty"); Object value = get(attributeName); assertAttributePresence(attributeName, value); assertNotException(attributeName, value); @@ -418,9 +468,9 @@ public class AnnotationAttributes extends LinkedHashMap { private T getRequiredAttributeWithAlias(String attributeName, Class annotationType, Object annotationSource, Class expectedType) { - Assert.hasText(attributeName, "attributeName must not be null or empty"); - Assert.notNull(annotationType, "annotationType must not be null"); - Assert.notNull(expectedType, "expectedType must not be null"); + Assert.hasText(attributeName, "'attributeName' must not be null or empty"); + Assert.notNull(annotationType, "'annotationType' must not be null"); + Assert.notNull(expectedType, "'expectedType' must not be null"); T attributeValue = getAttribute(attributeName, expectedType); @@ -433,8 +483,8 @@ public class AnnotationAttributes extends LinkedHashMap { if (!attributeEmpty && !aliasEmpty && !ObjectUtils.nullSafeEquals(attributeValue, aliasValue)) { String elementName = (annotationSource == null ? "unknown element" : annotationSource.toString()); - String msg = String.format("In annotation [%s] declared on [%s], attribute [%s] and its alias [%s] " + - "are present with values of [%s] and [%s], but only one is permitted.", + String msg = String.format("In annotation [%s] declared on [%s], attribute [%s] and its " + + "alias [%s] are present with values of [%s] and [%s], but only one is permitted.", annotationType.getName(), elementName, attributeName, aliasName, ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue)); throw new AnnotationConfigurationException(msg); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java index beecd919..01ddd5be 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.springframework.core.DecoratingProxy; import org.springframework.core.OrderComparator; /** @@ -81,10 +82,13 @@ public class AnnotationAwareOrderComparator extends OrderComparator { } } else if (obj != null) { - return OrderUtils.getOrder(obj.getClass()); + order = OrderUtils.getOrder(obj.getClass()); + if (order == null && obj instanceof DecoratingProxy) { + order = OrderUtils.getOrder(((DecoratingProxy) obj).getDecoratedClass()); + } } - return null; + return order; } /** @@ -94,13 +98,17 @@ public class AnnotationAwareOrderComparator extends OrderComparator { * multiple matches but only one object to be returned. */ public Integer getPriority(Object obj) { + Integer priority = null; if (obj instanceof Class) { - return OrderUtils.getPriority((Class) obj); + priority = OrderUtils.getPriority((Class) obj); } else if (obj != null) { - return OrderUtils.getPriority(obj.getClass()); + priority = OrderUtils.getPriority(obj.getClass()); + if (priority == null && obj instanceof DecoratingProxy) { + priority = OrderUtils.getOrder(((DecoratingProxy) obj).getDecoratedClass()); + } } - return null; + return priority; } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 09eb167e..2fe059a5 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -22,6 +22,7 @@ import java.lang.reflect.Array; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Collections; @@ -111,6 +112,7 @@ public abstract class AnnotationUtils { */ public static final String VALUE = "value"; + private static final String REPEATABLE_CLASS_NAME = "java.lang.annotation.Repeatable"; private static final Map findAnnotationCache = new ConcurrentReferenceHashMap(256); @@ -308,6 +310,7 @@ public abstract class AnnotationUtils { * @since 4.2 * @see #getRepeatableAnnotations(AnnotatedElement, Class, Class) * @see #getDeclaredRepeatableAnnotations(AnnotatedElement, Class, Class) + * @see AnnotatedElementUtils#getMergedRepeatableAnnotations(AnnotatedElement, Class) * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod * @see java.lang.annotation.Repeatable * @see java.lang.reflect.AnnotatedElement#getAnnotationsByType @@ -343,6 +346,7 @@ public abstract class AnnotationUtils { * @see #getRepeatableAnnotations(AnnotatedElement, Class) * @see #getDeclaredRepeatableAnnotations(AnnotatedElement, Class) * @see #getDeclaredRepeatableAnnotations(AnnotatedElement, Class, Class) + * @see AnnotatedElementUtils#getMergedRepeatableAnnotations(AnnotatedElement, Class, Class) * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod * @see java.lang.annotation.Repeatable * @see java.lang.reflect.AnnotatedElement#getAnnotationsByType @@ -388,6 +392,7 @@ public abstract class AnnotationUtils { * @see #getRepeatableAnnotations(AnnotatedElement, Class) * @see #getRepeatableAnnotations(AnnotatedElement, Class, Class) * @see #getDeclaredRepeatableAnnotations(AnnotatedElement, Class, Class) + * @see AnnotatedElementUtils#getMergedRepeatableAnnotations(AnnotatedElement, Class) * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod * @see java.lang.annotation.Repeatable * @see java.lang.reflect.AnnotatedElement#getDeclaredAnnotationsByType @@ -423,6 +428,7 @@ public abstract class AnnotationUtils { * @see #getRepeatableAnnotations(AnnotatedElement, Class) * @see #getRepeatableAnnotations(AnnotatedElement, Class, Class) * @see #getDeclaredRepeatableAnnotations(AnnotatedElement, Class) + * @see AnnotatedElementUtils#getMergedRepeatableAnnotations(AnnotatedElement, Class, Class) * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod * @see java.lang.annotation.Repeatable * @see java.lang.reflect.AnnotatedElement#getDeclaredAnnotationsByType @@ -616,7 +622,7 @@ public abstract class AnnotationUtils { static boolean isInterfaceWithAnnotatedMethods(Class iface) { Boolean found = annotatedInterfaceCache.get(iface); if (found != null) { - return found.booleanValue(); + return found; } found = Boolean.FALSE; for (Method ifcMethod : iface.getMethods()) { @@ -631,7 +637,7 @@ public abstract class AnnotationUtils { } } annotatedInterfaceCache.put(iface, found); - return found.booleanValue(); + return found; } /** @@ -883,14 +889,14 @@ public abstract class AnnotationUtils { AnnotationCacheKey cacheKey = new AnnotationCacheKey(annotationType, metaAnnotationType); Boolean metaPresent = metaPresentCache.get(cacheKey); if (metaPresent != null) { - return metaPresent.booleanValue(); + return metaPresent; } metaPresent = Boolean.FALSE; if (findAnnotation(annotationType, metaAnnotationType, false) != null) { metaPresent = Boolean.TRUE; } metaPresentCache.put(cacheKey, metaPresent); - return metaPresent.booleanValue(); + return metaPresent; } /** @@ -1013,6 +1019,13 @@ public abstract class AnnotationUtils { public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement annotatedElement, Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + return getAnnotationAttributes( + (Object) annotatedElement, annotation, classValuesAsString, nestedAnnotationsAsMap); + } + + private static AnnotationAttributes getAnnotationAttributes(Object annotatedElement, + Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + AnnotationAttributes attributes = retrieveAnnotationAttributes(annotatedElement, annotation, classValuesAsString, nestedAnnotationsAsMap); postProcessAnnotationAttributes(annotatedElement, attributes, classValuesAsString, nestedAnnotationsAsMap); @@ -1047,7 +1060,7 @@ public abstract class AnnotationUtils { * @since 4.2 * @see #postProcessAnnotationAttributes */ - static AnnotationAttributes retrieveAnnotationAttributes(AnnotatedElement annotatedElement, Annotation annotation, + static AnnotationAttributes retrieveAnnotationAttributes(Object annotatedElement, Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { Class annotationType = annotation.annotationType(); @@ -1091,14 +1104,14 @@ public abstract class AnnotationUtils { * {@code Annotation} instances * @return the adapted value, or the original value if no adaptation is needed */ - static Object adaptValue(AnnotatedElement annotatedElement, Object value, boolean classValuesAsString, + static Object adaptValue(Object annotatedElement, Object value, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { if (classValuesAsString) { - if (value instanceof Class) { + if (value instanceof Class) { return ((Class) value).getName(); } - else if (value instanceof Class[]) { + else if (value instanceof Class[]) { Class[] clazzArray = (Class[]) value; String[] classNames = new String[clazzArray.length]; for (int i = 0; i < clazzArray.length; i++) { @@ -1137,6 +1150,63 @@ public abstract class AnnotationUtils { return value; } + /** + * Register the annotation-declared default values for the given attributes, + * if available. + * @param attributes the annotation attributes to process + * @since 4.3.2 + */ + public static void registerDefaultValues(AnnotationAttributes attributes) { + // Only do defaults scanning for public annotations; we'd run into + // IllegalAccessExceptions otherwise, and we don't want to mess with + // accessibility in a SecurityManager environment. + Class annotationType = attributes.annotationType(); + if (annotationType != null && Modifier.isPublic(annotationType.getModifiers())) { + // Check declared default values of attributes in the annotation type. + for (Method annotationAttribute : getAttributeMethods(annotationType)) { + String attributeName = annotationAttribute.getName(); + Object defaultValue = annotationAttribute.getDefaultValue(); + if (defaultValue != null && !attributes.containsKey(attributeName)) { + if (defaultValue instanceof Annotation) { + defaultValue = getAnnotationAttributes((Annotation) defaultValue, false, true); + } + else if (defaultValue instanceof Annotation[]) { + Annotation[] realAnnotations = (Annotation[]) defaultValue; + AnnotationAttributes[] mappedAnnotations = new AnnotationAttributes[realAnnotations.length]; + for (int i = 0; i < realAnnotations.length; i++) { + mappedAnnotations[i] = getAnnotationAttributes(realAnnotations[i], false, true); + } + defaultValue = mappedAnnotations; + } + attributes.put(attributeName, new DefaultValueHolder(defaultValue)); + } + } + } + } + + /** + * Post-process the supplied {@link AnnotationAttributes}, preserving nested + * annotations as {@code Annotation} instances. + *

Specifically, this method enforces attribute alias semantics + * for annotation attributes that are annotated with {@link AliasFor @AliasFor} + * and replaces default value placeholders with their original default values. + * @param annotatedElement the element that is annotated with an annotation or + * annotation hierarchy from which the supplied attributes were created; + * may be {@code null} if unknown + * @param attributes the annotation attributes to post-process + * @param classValuesAsString whether to convert Class references into Strings (for + * compatibility with {@link org.springframework.core.type.AnnotationMetadata}) + * or to preserve them as Class references + * @since 4.3.2 + * @see #postProcessAnnotationAttributes(Object, AnnotationAttributes, boolean, boolean) + * @see #getDefaultValue(Class, String) + */ + public static void postProcessAnnotationAttributes(Object annotatedElement, + AnnotationAttributes attributes, boolean classValuesAsString) { + + postProcessAnnotationAttributes(annotatedElement, attributes, classValuesAsString, false); + } + /** * Post-process the supplied {@link AnnotationAttributes}. *

Specifically, this method enforces attribute alias semantics @@ -1154,10 +1224,10 @@ public abstract class AnnotationUtils { * {@link org.springframework.core.type.AnnotationMetadata}) or to preserve them as * {@code Annotation} instances * @since 4.2 - * @see #retrieveAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean) + * @see #retrieveAnnotationAttributes(Object, Annotation, boolean, boolean) * @see #getDefaultValue(Class, String) */ - static void postProcessAnnotationAttributes(AnnotatedElement annotatedElement, + static void postProcessAnnotationAttributes(Object annotatedElement, AnnotationAttributes attributes, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { // Abort? @@ -1171,51 +1241,55 @@ public abstract class AnnotationUtils { // circuit the search algorithms. Set valuesAlreadyReplaced = new HashSet(); - // Validate @AliasFor configuration - Map> aliasMap = getAttributeAliasMap(annotationType); - for (String attributeName : aliasMap.keySet()) { - if (valuesAlreadyReplaced.contains(attributeName)) { - continue; - } - Object value = attributes.get(attributeName); - boolean valuePresent = (value != null && !(value instanceof DefaultValueHolder)); - - for (String aliasedAttributeName : aliasMap.get(attributeName)) { - if (valuesAlreadyReplaced.contains(aliasedAttributeName)) { + if (!attributes.validated) { + // Validate @AliasFor configuration + Map> aliasMap = getAttributeAliasMap(annotationType); + for (String attributeName : aliasMap.keySet()) { + if (valuesAlreadyReplaced.contains(attributeName)) { continue; } + Object value = attributes.get(attributeName); + boolean valuePresent = (value != null && !(value instanceof DefaultValueHolder)); - Object aliasedValue = attributes.get(aliasedAttributeName); - boolean aliasPresent = (aliasedValue != null && !(aliasedValue instanceof DefaultValueHolder)); - - // Something to validate or replace with an alias? - if (valuePresent || aliasPresent) { - if (valuePresent && aliasPresent) { - // Since annotation attributes can be arrays, we must use ObjectUtils.nullSafeEquals(). - if (!ObjectUtils.nullSafeEquals(value, aliasedValue)) { - String elementAsString = (annotatedElement != null ? annotatedElement.toString() : "unknown element"); - throw new AnnotationConfigurationException(String.format( - "In AnnotationAttributes for annotation [%s] declared on %s, " + - "attribute '%s' and its alias '%s' are declared with values of [%s] and [%s], " + - "but only one is permitted.", annotationType.getName(), elementAsString, - attributeName, aliasedAttributeName, ObjectUtils.nullSafeToString(value), - ObjectUtils.nullSafeToString(aliasedValue))); - } + for (String aliasedAttributeName : aliasMap.get(attributeName)) { + if (valuesAlreadyReplaced.contains(aliasedAttributeName)) { + continue; } - else if (aliasPresent) { - // Replace value with aliasedValue - attributes.put(attributeName, - adaptValue(annotatedElement, aliasedValue, classValuesAsString, nestedAnnotationsAsMap)); - valuesAlreadyReplaced.add(attributeName); - } - else { - // Replace aliasedValue with value - attributes.put(aliasedAttributeName, - adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap)); - valuesAlreadyReplaced.add(aliasedAttributeName); + + Object aliasedValue = attributes.get(aliasedAttributeName); + boolean aliasPresent = (aliasedValue != null && !(aliasedValue instanceof DefaultValueHolder)); + + // Something to validate or replace with an alias? + if (valuePresent || aliasPresent) { + if (valuePresent && aliasPresent) { + // Since annotation attributes can be arrays, we must use ObjectUtils.nullSafeEquals(). + if (!ObjectUtils.nullSafeEquals(value, aliasedValue)) { + String elementAsString = + (annotatedElement != null ? annotatedElement.toString() : "unknown element"); + throw new AnnotationConfigurationException(String.format( + "In AnnotationAttributes for annotation [%s] declared on %s, " + + "attribute '%s' and its alias '%s' are declared with values of [%s] and [%s], " + + "but only one is permitted.", annotationType.getName(), elementAsString, + attributeName, aliasedAttributeName, ObjectUtils.nullSafeToString(value), + ObjectUtils.nullSafeToString(aliasedValue))); + } + } + else if (aliasPresent) { + // Replace value with aliasedValue + attributes.put(attributeName, + adaptValue(annotatedElement, aliasedValue, classValuesAsString, nestedAnnotationsAsMap)); + valuesAlreadyReplaced.add(attributeName); + } + else { + // Replace aliasedValue with value + attributes.put(aliasedAttributeName, + adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap)); + valuesAlreadyReplaced.add(aliasedAttributeName); + } } } } + attributes.validated = true; } // Replace any remaining placeholders with actual default values @@ -1355,8 +1429,12 @@ public abstract class AnnotationUtils { * @see #synthesizeAnnotation(Map, Class, AnnotatedElement) * @see #synthesizeAnnotation(Class) */ - @SuppressWarnings("unchecked") public static A synthesizeAnnotation(A annotation, AnnotatedElement annotatedElement) { + return synthesizeAnnotation(annotation, (Object) annotatedElement); + } + + @SuppressWarnings("unchecked") + static A synthesizeAnnotation(A annotation, Object annotatedElement) { if (annotation == null) { return null; } @@ -1461,7 +1539,7 @@ public abstract class AnnotationUtils { * @see #synthesizeAnnotation(Annotation, AnnotatedElement) * @see #synthesizeAnnotation(Map, Class, AnnotatedElement) */ - public static Annotation[] synthesizeAnnotationArray(Annotation[] annotations, AnnotatedElement annotatedElement) { + static Annotation[] synthesizeAnnotationArray(Annotation[] annotations, Object annotatedElement) { if (annotations == null) { return null; } @@ -1489,7 +1567,7 @@ public abstract class AnnotationUtils { * {@code @AliasFor} is detected * @since 4.2.1 * @see #synthesizeAnnotation(Map, Class, AnnotatedElement) - * @see #synthesizeAnnotationArray(Annotation[], AnnotatedElement) + * @see #synthesizeAnnotationArray(Annotation[], Object) */ @SuppressWarnings("unchecked") static A[] synthesizeAnnotationArray(Map[] maps, Class annotationType) { @@ -1577,7 +1655,7 @@ public abstract class AnnotationUtils { private static boolean isSynthesizable(Class annotationType) { Boolean synthesizable = synthesizableCache.get(annotationType); if (synthesizable != null) { - return synthesizable.booleanValue(); + return synthesizable; } synthesizable = Boolean.FALSE; @@ -1605,7 +1683,7 @@ public abstract class AnnotationUtils { } synthesizableCache.put(annotationType, synthesizable); - return synthesizable.booleanValue(); + return synthesizable; } /** @@ -1720,6 +1798,29 @@ public abstract class AnnotationUtils { return (method != null && method.getName().equals("annotationType") && method.getParameterTypes().length == 0); } + /** + * Resolve the container type for the supplied repeatable {@code annotationType}. + *

Automatically detects a container annotation declared via + * {@link java.lang.annotation.Repeatable}. If the supplied annotation type + * is not annotated with {@code @Repeatable}, this method simply returns + * {@code null}. + * @since 4.2 + */ + @SuppressWarnings("unchecked") + static Class resolveContainerAnnotationType(Class annotationType) { + try { + Annotation repeatable = getAnnotation(annotationType, REPEATABLE_CLASS_NAME); + if (repeatable != null) { + Object value = getValue(repeatable); + return (Class) value; + } + } + catch (Exception ex) { + handleIntrospectionFailure(annotationType, ex); + } + return null; + } + /** * If the supplied throwable is an {@link AnnotationConfigurationException}, * it will be cast to an {@code AnnotationConfigurationException} and thrown, @@ -1774,7 +1875,7 @@ public abstract class AnnotationUtils { /** * Cache key for the AnnotatedElement cache. */ - private static class AnnotationCacheKey { + private static final class AnnotationCacheKey implements Comparable { private final AnnotatedElement element; @@ -1801,13 +1902,25 @@ public abstract class AnnotationUtils { public int hashCode() { return (this.element.hashCode() * 29 + this.annotationType.hashCode()); } + + @Override + public String toString() { + return "@" + this.annotationType + " on " + this.element; + } + + @Override + public int compareTo(AnnotationCacheKey other) { + int result = this.element.toString().compareTo(other.element.toString()); + if (result == 0) { + result = this.annotationType.getName().compareTo(other.annotationType.getName()); + } + return result; + } } private static class AnnotationCollector { - private static final String REPEATABLE_CLASS_NAME = "java.lang.annotation.Repeatable"; - private final Class annotationType; private final Class containerAnnotationType; @@ -1825,21 +1938,6 @@ public abstract class AnnotationUtils { this.declaredMode = declaredMode; } - @SuppressWarnings("unchecked") - static Class resolveContainerAnnotationType(Class annotationType) { - try { - Annotation repeatable = getAnnotation(annotationType, REPEATABLE_CLASS_NAME); - if (repeatable != null) { - Object value = AnnotationUtils.getValue(repeatable); - return (Class) value; - } - } - catch (Exception ex) { - handleIntrospectionFailure(annotationType, ex); - } - return null; - } - Set getResult(AnnotatedElement element) { process(element); return Collections.unmodifiableSet(this.result); @@ -1951,12 +2049,19 @@ public abstract class AnnotationUtils { this.aliasedAnnotationType = (Annotation.class == aliasFor.annotation() ? this.sourceAnnotationType : aliasFor.annotation()); this.aliasedAttributeName = getAliasedAttributeName(aliasFor, sourceAttribute); + if (this.aliasedAnnotationType == this.sourceAnnotationType && + this.aliasedAttributeName.equals(this.sourceAttributeName)) { + String msg = String.format("@AliasFor declaration on attribute '%s' in annotation [%s] points to " + + "itself. Specify 'annotation' to point to a same-named attribute on a meta-annotation.", + sourceAttribute.getName(), declaringClass.getName()); + throw new AnnotationConfigurationException(msg); + } try { this.aliasedAttribute = this.aliasedAnnotationType.getDeclaredMethod(this.aliasedAttributeName); } catch (NoSuchMethodException ex) { String msg = String.format( - "Attribute [%s] in annotation [%s] is declared as an @AliasFor nonexistent attribute [%s] in annotation [%s].", + "Attribute '%s' in annotation [%s] is declared as an @AliasFor nonexistent attribute '%s' in annotation [%s].", this.sourceAttributeName, this.sourceAnnotationType.getName(), this.aliasedAttributeName, this.aliasedAnnotationType.getName()); throw new AnnotationConfigurationException(msg, ex); @@ -1968,8 +2073,8 @@ public abstract class AnnotationUtils { private void validate() { // Target annotation is not meta-present? if (!this.isAliasPair && !isAnnotationMetaPresent(this.sourceAnnotationType, this.aliasedAnnotationType)) { - String msg = String.format("@AliasFor declaration on attribute [%s] in annotation [%s] declares " + - "an alias for attribute [%s] in meta-annotation [%s] which is not meta-present.", + String msg = String.format("@AliasFor declaration on attribute '%s' in annotation [%s] declares " + + "an alias for attribute '%s' in meta-annotation [%s] which is not meta-present.", this.sourceAttributeName, this.sourceAnnotationType.getName(), this.aliasedAttributeName, this.aliasedAnnotationType.getName()); throw new AnnotationConfigurationException(msg); @@ -1978,14 +2083,14 @@ public abstract class AnnotationUtils { if (this.isAliasPair) { AliasFor mirrorAliasFor = this.aliasedAttribute.getAnnotation(AliasFor.class); if (mirrorAliasFor == null) { - String msg = String.format("Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s].", + String msg = String.format("Attribute '%s' in annotation [%s] must be declared as an @AliasFor [%s].", this.aliasedAttributeName, this.sourceAnnotationType.getName(), this.sourceAttributeName); throw new AnnotationConfigurationException(msg); } String mirrorAliasedAttributeName = getAliasedAttributeName(mirrorAliasFor, this.aliasedAttribute); if (!this.sourceAttributeName.equals(mirrorAliasedAttributeName)) { - String msg = String.format("Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s], not [%s].", + String msg = String.format("Attribute '%s' in annotation [%s] must be declared as an @AliasFor [%s], not [%s].", this.aliasedAttributeName, this.sourceAnnotationType.getName(), this.sourceAttributeName, mirrorAliasedAttributeName); throw new AnnotationConfigurationException(msg); @@ -1994,9 +2099,10 @@ public abstract class AnnotationUtils { Class returnType = this.sourceAttribute.getReturnType(); Class aliasedReturnType = this.aliasedAttribute.getReturnType(); - if (returnType != aliasedReturnType) { - String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] " + - "and attribute [%s] in annotation [%s] must declare the same return type.", + if (returnType != aliasedReturnType && + (!aliasedReturnType.isArray() || returnType != aliasedReturnType.getComponentType())) { + String msg = String.format("Misconfigured aliases: attribute '%s' in annotation [%s] " + + "and attribute '%s' in annotation [%s] must declare the same return type.", this.sourceAttributeName, this.sourceAnnotationType.getName(), this.aliasedAttributeName, this.aliasedAnnotationType.getName()); throw new AnnotationConfigurationException(msg); @@ -2013,16 +2119,16 @@ public abstract class AnnotationUtils { Object aliasedDefaultValue = aliasedAttribute.getDefaultValue(); if (defaultValue == null || aliasedDefaultValue == null) { - String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] " + - "and attribute [%s] in annotation [%s] must declare default values.", + String msg = String.format("Misconfigured aliases: attribute '%s' in annotation [%s] " + + "and attribute '%s' in annotation [%s] must declare default values.", this.sourceAttributeName, this.sourceAnnotationType.getName(), aliasedAttribute.getName(), aliasedAttribute.getDeclaringClass().getName()); throw new AnnotationConfigurationException(msg); } if (!ObjectUtils.nullSafeEquals(defaultValue, aliasedDefaultValue)) { - String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] " + - "and attribute [%s] in annotation [%s] must declare the same default value.", + String msg = String.format("Misconfigured aliases: attribute '%s' in annotation [%s] " + + "and attribute '%s' in annotation [%s] must declare the same default value.", this.sourceAttributeName, this.sourceAnnotationType.getName(), aliasedAttribute.getName(), aliasedAttribute.getDeclaringClass().getName()); throw new AnnotationConfigurationException(msg); @@ -2125,15 +2231,16 @@ public abstract class AnnotationUtils { /** * Get the name of the aliased attribute configured via the supplied - * {@link AliasFor @AliasFor} annotation on the supplied {@code attribute}. + * {@link AliasFor @AliasFor} annotation on the supplied {@code attribute}, + * or the original attribute if no aliased one specified (indicating that + * the reference goes to a same-named attribute on a meta-annotation). *

This method returns the value of either the {@code attribute} * or {@code value} attribute of {@code @AliasFor}, ensuring that only * one of the attributes has been declared while simultaneously ensuring * that at least one of the attributes has been declared. * @param aliasFor the {@code @AliasFor} annotation from which to retrieve * the aliased attribute name - * @param attribute the attribute that is annotated with {@code @AliasFor}, - * used solely for building an exception message + * @param attribute the attribute that is annotated with {@code @AliasFor} * @return the name of the aliased attribute (never {@code null} or empty) * @throws AnnotationConfigurationException if invalid configuration of * {@code @AliasFor} is detected @@ -2146,23 +2253,15 @@ public abstract class AnnotationUtils { // Ensure user did not declare both 'value' and 'attribute' in @AliasFor if (attributeDeclared && valueDeclared) { - String msg = String.format("In @AliasFor declared on attribute [%s] in annotation [%s], attribute 'attribute' " + + String msg = String.format("In @AliasFor declared on attribute '%s' in annotation [%s], attribute 'attribute' " + "and its alias 'value' are present with values of [%s] and [%s], but only one is permitted.", attribute.getName(), attribute.getDeclaringClass().getName(), attributeName, value); throw new AnnotationConfigurationException(msg); } + // Either explicit attribute name or pointing to same-named attribute by default attributeName = (attributeDeclared ? attributeName : value); - - // Ensure user declared either 'value' or 'attribute' in @AliasFor - if (!StringUtils.hasText(attributeName)) { - String msg = String.format( - "@AliasFor declaration on attribute [%s] in annotation [%s] is missing required 'attribute' value.", - attribute.getName(), attribute.getDeclaringClass().getName()); - throw new AnnotationConfigurationException(msg); - } - - return attributeName.trim(); + return (StringUtils.hasText(attributeName) ? attributeName.trim() : attribute.getName()); } @Override diff --git a/spring-core/src/main/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractor.java index 805c769b..0ea2cf00 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import org.springframework.util.ReflectionUtils; @@ -32,7 +31,7 @@ import org.springframework.util.ReflectionUtils; * @see AliasFor * @see AbstractAliasAwareAnnotationAttributeExtractor * @see MapAnnotationAttributeExtractor - * @see AnnotationUtils#synthesizeAnnotation(Annotation, AnnotatedElement) + * @see AnnotationUtils#synthesizeAnnotation */ class DefaultAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttributeExtractor { @@ -42,7 +41,7 @@ class DefaultAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAt * @param annotatedElement the element that is annotated with the supplied * annotation; may be {@code null} if unknown */ - DefaultAnnotationAttributeExtractor(Annotation annotation, AnnotatedElement annotatedElement) { + DefaultAnnotationAttributeExtractor(Annotation annotation, Object annotatedElement) { super(annotation.annotationType(), annotatedElement, annotation); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java index 005ae431..a2491fe5 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +18,15 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Array; import java.lang.reflect.Method; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import static org.springframework.core.annotation.AnnotationUtils.*; - /** * Implementation of the {@link AnnotationAttributeExtractor} strategy that * is backed by a {@link Map}. @@ -87,10 +87,10 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib private static Map enrichAndValidateAttributes( Map originalAttributes, Class annotationType) { - Map attributes = new HashMap(originalAttributes); - Map> attributeAliasMap = getAttributeAliasMap(annotationType); + Map attributes = new LinkedHashMap(originalAttributes); + Map> attributeAliasMap = AnnotationUtils.getAttributeAliasMap(annotationType); - for (Method attributeMethod : getAttributeMethods(annotationType)) { + for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotationType)) { String attributeName = attributeMethod.getName(); Object attributeValue = attributes.get(attributeName); @@ -111,7 +111,7 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib // if aliases not present, check default if (attributeValue == null) { - Object defaultValue = getDefaultValue(annotationType, attributeName); + Object defaultValue = AnnotationUtils.getDefaultValue(annotationType, attributeName); if (defaultValue != null) { attributeValue = defaultValue; attributes.put(attributeName, attributeValue); @@ -121,7 +121,7 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib // if still null if (attributeValue == null) { throw new IllegalArgumentException(String.format( - "Attributes map [%s] returned null for required attribute [%s] defined by annotation type [%s].", + "Attributes map %s returned null for required attribute '%s' defined by annotation type [%s].", attributes, attributeName, annotationType.getName())); } @@ -132,13 +132,21 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib if (!ClassUtils.isAssignable(requiredReturnType, actualReturnType)) { boolean converted = false; + // Single element overriding an array of the same type? + if (requiredReturnType.isArray() && requiredReturnType.getComponentType() == actualReturnType) { + Object array = Array.newInstance(requiredReturnType.getComponentType(), 1); + Array.set(array, 0, attributeValue); + attributes.put(attributeName, array); + converted = true; + } + // Nested map representing a single annotation? - if (Annotation.class.isAssignableFrom(requiredReturnType) && + else if (Annotation.class.isAssignableFrom(requiredReturnType) && Map.class.isAssignableFrom(actualReturnType)) { Class nestedAnnotationType = (Class) requiredReturnType; Map map = (Map) attributeValue; - attributes.put(attributeName, synthesizeAnnotation(map, nestedAnnotationType, null)); + attributes.put(attributeName, AnnotationUtils.synthesizeAnnotation(map, nestedAnnotationType, null)); converted = true; } @@ -149,14 +157,14 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib Class nestedAnnotationType = (Class) requiredReturnType.getComponentType(); Map[] maps = (Map[]) attributeValue; - attributes.put(attributeName, synthesizeAnnotationArray(maps, nestedAnnotationType)); + attributes.put(attributeName, AnnotationUtils.synthesizeAnnotationArray(maps, nestedAnnotationType)); converted = true; } if (!converted) { throw new IllegalArgumentException(String.format( - "Attributes map [%s] returned a value of type [%s] for attribute [%s], " - + "but a value of type [%s] is required as defined by annotation type [%s].", + "Attributes map %s returned a value of type [%s] for attribute '%s', " + + "but a value of type [%s] is required as defined by annotation type [%s].", attributes, actualReturnType.getName(), attributeName, requiredReturnType.getName(), annotationType.getName())); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java index c8614eb5..27e14c43 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java @@ -27,11 +27,9 @@ import java.util.concurrent.ConcurrentHashMap; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; -import static org.springframework.core.annotation.AnnotationUtils.*; -import static org.springframework.util.ReflectionUtils.*; - /** * {@link InvocationHandler} for an {@link Annotation} that Spring has * synthesized (i.e., wrapped in a dynamic proxy) with additional @@ -63,22 +61,21 @@ class SynthesizedAnnotationInvocationHandler implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - if (isEqualsMethod(method)) { + if (ReflectionUtils.isEqualsMethod(method)) { return annotationEquals(args[0]); } - if (isHashCodeMethod(method)) { + if (ReflectionUtils.isHashCodeMethod(method)) { return annotationHashCode(); } - if (isToStringMethod(method)) { + if (ReflectionUtils.isToStringMethod(method)) { return annotationToString(); } - if (isAnnotationTypeMethod(method)) { + if (AnnotationUtils.isAnnotationTypeMethod(method)) { return annotationType(); } - if (!isAttributeMethod(method)) { - String msg = String.format("Method [%s] is unsupported for synthesized annotation type [%s]", - method, annotationType()); - throw new AnnotationConfigurationException(msg); + if (!AnnotationUtils.isAttributeMethod(method)) { + throw new AnnotationConfigurationException(String.format( + "Method [%s] is unsupported for synthesized annotation type [%s]", method, annotationType())); } return getAttributeValue(method); } @@ -100,10 +97,10 @@ class SynthesizedAnnotationInvocationHandler implements InvocationHandler { // Synthesize nested annotations before returning them. if (value instanceof Annotation) { - value = synthesizeAnnotation((Annotation) value, this.attributeExtractor.getAnnotatedElement()); + value = AnnotationUtils.synthesizeAnnotation((Annotation) value, this.attributeExtractor.getAnnotatedElement()); } else if (value instanceof Annotation[]) { - value = synthesizeAnnotationArray((Annotation[]) value, this.attributeExtractor.getAnnotatedElement()); + value = AnnotationUtils.synthesizeAnnotationArray((Annotation[]) value, this.attributeExtractor.getAnnotatedElement()); } this.valueCache.put(attributeName, value); @@ -164,9 +161,9 @@ class SynthesizedAnnotationInvocationHandler implements InvocationHandler { return false; } - for (Method attributeMethod : getAttributeMethods(annotationType())) { + for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotationType())) { Object thisValue = getAttributeValue(attributeMethod); - Object otherValue = invokeMethod(attributeMethod, other); + Object otherValue = ReflectionUtils.invokeMethod(attributeMethod, other); if (!ObjectUtils.nullSafeEquals(thisValue, otherValue)) { return false; } @@ -181,7 +178,7 @@ class SynthesizedAnnotationInvocationHandler implements InvocationHandler { private int annotationHashCode() { int result = 0; - for (Method attributeMethod : getAttributeMethods(annotationType())) { + for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotationType())) { Object value = getAttributeValue(attributeMethod); int hashCode; if (value.getClass().isArray()) { @@ -239,7 +236,7 @@ class SynthesizedAnnotationInvocationHandler implements InvocationHandler { private String annotationToString() { StringBuilder sb = new StringBuilder("@").append(annotationType().getName()).append("("); - Iterator iterator = getAttributeMethods(annotationType()).iterator(); + Iterator iterator = AnnotationUtils.getAttributeMethods(annotationType()).iterator(); while (iterator.hasNext()) { Method attributeMethod = iterator.next(); sb.append(attributeMethod.getName()); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizingMethodParameter.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizingMethodParameter.java index a6915e76..eb287c47 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizingMethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizingMethodParameter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; import org.springframework.core.MethodParameter; @@ -34,7 +35,8 @@ import org.springframework.core.MethodParameter; public class SynthesizingMethodParameter extends MethodParameter { /** - * Create a new {@code SynthesizingMethodParameter} for the given method. + * Create a new {@code SynthesizingMethodParameter} for the given method, + * with nesting level 1. * @param method the Method to specify a parameter for * @param parameterIndex the index of the parameter: -1 for the method * return type; 0 for the first method parameter; 1 for the second method @@ -44,6 +46,51 @@ public class SynthesizingMethodParameter extends MethodParameter { super(method, parameterIndex); } + /** + * Create a new {@code SynthesizingMethodParameter} for the given method. + * @param method the Method to specify a parameter for + * @param parameterIndex the index of the parameter: -1 for the method + * return type; 0 for the first method parameter; 1 for the second method + * parameter, etc. + * @param nestingLevel the nesting level of the target type + * (typically 1; e.g. in case of a List of Lists, 1 would indicate the + * nested List, whereas 2 would indicate the element of the nested List) + */ + public SynthesizingMethodParameter(Method method, int parameterIndex, int nestingLevel) { + super(method, parameterIndex, nestingLevel); + } + + /** + * Create a new {@code SynthesizingMethodParameter} for the given constructor, + * with nesting level 1. + * @param constructor the Constructor to specify a parameter for + * @param parameterIndex the index of the parameter + */ + public SynthesizingMethodParameter(Constructor constructor, int parameterIndex) { + super(constructor, parameterIndex); + } + + /** + * Create a new {@code SynthesizingMethodParameter} for the given constructor. + * @param constructor the Constructor to specify a parameter for + * @param parameterIndex the index of the parameter + * @param nestingLevel the nesting level of the target type + * (typically 1; e.g. in case of a List of Lists, 1 would indicate the + * nested List, whereas 2 would indicate the element of the nested List) + */ + public SynthesizingMethodParameter(Constructor constructor, int parameterIndex, int nestingLevel) { + super(constructor, parameterIndex, nestingLevel); + } + + /** + * Copy constructor, resulting in an independent {@code SynthesizingMethodParameter} + * based on the same metadata and cache state that the original object was in. + * @param original the original SynthesizingMethodParameter object to copy from + */ + protected SynthesizingMethodParameter(SynthesizingMethodParameter original) { + super(original); + } + @Override protected A adaptAnnotation(A annotation) { @@ -55,4 +102,10 @@ public class SynthesizingMethodParameter extends MethodParameter { return AnnotationUtils.synthesizeAnnotationArray(annotations, getAnnotatedElement()); } + + @Override + public SynthesizingMethodParameter clone() { + return new SynthesizingMethodParameter(this); + } + } diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index dde18ce8..ffbbd7ab 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -343,7 +343,7 @@ public class TypeDescriptor implements Serializable { if (streamAvailable && StreamDelegate.isStream(this.type)) { return StreamDelegate.getStreamElementType(this); } - return getRelatedIfResolvable(this, this.resolvableType.asCollection().getGeneric()); + return getRelatedIfResolvable(this, this.resolvableType.asCollection().getGeneric(0)); } /** @@ -467,7 +467,7 @@ public class TypeDescriptor implements Serializable { return false; } for (Annotation ann : getAnnotations()) { - if (!other.hasAnnotation(ann.annotationType())) { + if (!ann.equals(other.getAnnotation(ann.annotationType()))) { return false; } } @@ -706,7 +706,7 @@ public class TypeDescriptor implements Serializable { } public static TypeDescriptor getStreamElementType(TypeDescriptor source) { - return getRelatedIfResolvable(source, source.resolvableType.as(Stream.class).getGeneric()); + return getRelatedIfResolvable(source, source.resolvableType.as(Stream.class).getGeneric(0)); } } diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java index 9280c4ea..7541d550 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,18 +27,19 @@ public interface ConverterRegistry { /** * Add a plain converter to this registry. - * The convertible sourceType/targetType pair is derived from the Converter's parameterized types. + * The convertible source/target type pair is derived from the Converter's parameterized types. * @throws IllegalArgumentException if the parameterized types could not be resolved */ void addConverter(Converter converter); /** * Add a plain converter to this registry. - * The convertible sourceType/targetType pair is specified explicitly. - * Allows for a Converter to be reused for multiple distinct pairs without having to create a Converter class for each pair. + * The convertible source/target type pair is specified explicitly. + *

Allows for a Converter to be reused for multiple distinct pairs without + * having to create a Converter class for each pair. * @since 3.1 */ - void addConverter(Class sourceType, Class targetType, Converter converter); + void addConverter(Class sourceType, Class targetType, Converter converter); /** * Add a generic converter to this registry. @@ -47,7 +48,7 @@ public interface ConverterRegistry { /** * Add a ranged converter factory to this registry. - * The convertible sourceType/rangeType pair is derived from the ConverterFactory's parameterized types. + * The convertible source/target type pair is derived from the ConverterFactory's parameterized types. * @throws IllegalArgumentException if the parameterized types could not be resolved. */ void addConverterFactory(ConverterFactory converterFactory); diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/AbstractConditionalEnumConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/AbstractConditionalEnumConverter.java new file mode 100644 index 00000000..e6c94f65 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/AbstractConditionalEnumConverter.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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. + */ + +package org.springframework.core.convert.support; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; +import org.springframework.util.ClassUtils; + +/** + * A {@link ConditionalConverter} base implementation for enum-based converters. + * + * @author Stephane Nicoll + * @since 4.3 + */ +abstract class AbstractConditionalEnumConverter implements ConditionalConverter { + + private final ConversionService conversionService; + + + protected AbstractConditionalEnumConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + for (Class interfaceType : ClassUtils.getAllInterfacesForClass(sourceType.getType())) { + if (this.conversionService.canConvert(TypeDescriptor.valueOf(interfaceType), targetType)) { + return false; + } + } + return true; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java index e93eabe9..1806afd2 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.core.convert.converter.GenericConverter; * Internal utilities for the conversion package. * * @author Keith Donald + * @author Stephane Nicoll * @since 3.0 */ abstract class ConversionUtils { @@ -65,4 +66,16 @@ abstract class ConversionUtils { } } + public static Class getEnumType(Class targetType) { + Class enumType = targetType; + while (enumType != null && !enumType.isEnum()) { + enumType = enumType.getSuperclass(); + } + if (enumType == null) { + throw new IllegalArgumentException( + "The target type " + targetType.getName() + " does not refer to an enum"); + } + return enumType; + } + } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java index 408d1367..86df6337 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -141,8 +141,10 @@ public class DefaultConversionService extends GenericConversionService { converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter()); converterRegistry.addConverterFactory(new StringToEnumConverterFactory()); - converterRegistry.addConverter(Enum.class, String.class, - new EnumToStringConverter((ConversionService) converterRegistry)); + converterRegistry.addConverter(new EnumToStringConverter((ConversionService) converterRegistry)); + + converterRegistry.addConverterFactory(new IntegerToEnumConverterFactory()); + converterRegistry.addConverter(new EnumToIntegerConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new StringToLocaleConverter()); converterRegistry.addConverter(Locale.class, String.class, new ObjectToStringConverter()); diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/EnumToIntegerConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/EnumToIntegerConverter.java new file mode 100644 index 00000000..0028ef54 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/EnumToIntegerConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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. + */ + +package org.springframework.core.convert.support; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; + +/** + * Calls {@link Enum#ordinal()} to convert a source Enum to a Integer. + * This converter will not match enums with interfaces that can be converted. + * + * @author Yanming Zhou + * @since 4.3 + */ +final class EnumToIntegerConverter extends AbstractConditionalEnumConverter implements Converter, Integer> { + + public EnumToIntegerConverter(ConversionService conversionService) { + super(conversionService); + } + + @Override + public Integer convert(Enum source) { + return source.ordinal(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/EnumToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/EnumToStringConverter.java index 02a5f19f..ccb06f51 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/EnumToStringConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/EnumToStringConverter.java @@ -17,10 +17,7 @@ package org.springframework.core.convert.support; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.ConditionalConverter; import org.springframework.core.convert.converter.Converter; -import org.springframework.util.ClassUtils; /** * Calls {@link Enum#name()} to convert a source Enum to a String. @@ -30,24 +27,10 @@ import org.springframework.util.ClassUtils; * @author Phillip Webb * @since 3.0 */ -final class EnumToStringConverter implements Converter, String>, ConditionalConverter { - - private final ConversionService conversionService; - +final class EnumToStringConverter extends AbstractConditionalEnumConverter implements Converter, String> { public EnumToStringConverter(ConversionService conversionService) { - this.conversionService = conversionService; - } - - - @Override - public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { - for (Class interfaceType : ClassUtils.getAllInterfacesForClass(sourceType.getType())) { - if (this.conversionService.canConvert(TypeDescriptor.valueOf(interfaceType), targetType)) { - return false; - } - } - return true; + super(conversionService); } @Override diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java index 52df6a77..e051e6cc 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -103,7 +103,7 @@ public class GenericConversionService implements ConfigurableConversionService { } @Override - public void addConverter(Class sourceType, Class targetType, Converter converter) { + public void addConverter(Class sourceType, Class targetType, Converter converter) { addConverter(new ConverterAdapter( converter, ResolvableType.forClass(sourceType), ResolvableType.forClass(targetType))); } @@ -435,7 +435,7 @@ public class GenericConversionService implements ConfigurableConversionService { /** * Key for use with the converter cache. */ - private static final class ConverterCacheKey { + private static final class ConverterCacheKey implements Comparable { private final TypeDescriptor sourceType; @@ -470,6 +470,17 @@ public class GenericConversionService implements ConfigurableConversionService { return ("ConverterCacheKey [sourceType = " + this.sourceType + ", targetType = " + this.targetType + "]"); } + + @Override + public int compareTo(ConverterCacheKey other) { + int result = this.sourceType.getResolvableType().toString().compareTo( + other.sourceType.getResolvableType().toString()); + if (result == 0) { + result = this.targetType.getResolvableType().toString().compareTo( + other.targetType.getResolvableType().toString()); + } + return result; + } } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java new file mode 100644 index 00000000..03204e73 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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. + */ + +package org.springframework.core.convert.support; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; + +/** + * Converts from a Integer to a {@link java.lang.Enum} by calling {@link Class#getEnumConstants()}. + * + * @author Yanming Zhou + * @author Stephane Nicoll + * @since 4.3 + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +final class IntegerToEnumConverterFactory implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + return new IntegerToEnum(ConversionUtils.getEnumType(targetType)); + } + + + private class IntegerToEnum implements Converter { + + private final Class enumType; + + public IntegerToEnum(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(Integer source) { + return this.enumType.getEnumConstants()[source]; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java index fed1e45d..5631f2ca 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -155,7 +155,7 @@ final class ObjectToObjectConverter implements ConditionalGenericConverter { method.getParameterTypes()[0] == sourceClass); } else if (member instanceof Constructor) { - Constructor ctor = (Constructor) member; + Constructor ctor = (Constructor) member; return (ctor.getParameterTypes()[0] == sourceClass); } else { @@ -185,9 +185,6 @@ final class ObjectToObjectConverter implements ConditionalGenericConverter { method = ClassUtils.getStaticMethod(targetClass, "of", sourceClass); if (method == null) { method = ClassUtils.getStaticMethod(targetClass, "from", sourceClass); - if (method == null) { - return null; - } } } return method; diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java index 923239b9..865d1e83 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.core.convert.converter.ConverterFactory; * Converts from a String to a {@link java.lang.Enum} by calling {@link Enum#valueOf(Class, String)}. * * @author Keith Donald + * @author Stephane Nicoll * @since 3.0 */ @SuppressWarnings({"unchecked", "rawtypes"}) @@ -30,15 +31,7 @@ final class StringToEnumConverterFactory implements ConverterFactory Converter getConverter(Class targetType) { - Class enumType = targetType; - while (enumType != null && !enumType.isEnum()) { - enumType = enumType.getSuperclass(); - } - if (enumType == null) { - throw new IllegalArgumentException( - "The target type " + targetType.getName() + " does not refer to an enum"); - } - return new StringToEnum(enumType); + return new StringToEnum(ConversionUtils.getEnumType(targetType)); } diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java index c44f5d62..25268f84 100644 --- a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -547,6 +547,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment { } @Override + @Deprecated public Class getPropertyAsClass(String key, Class targetType) { return this.propertyResolver.getPropertyAsClass(key, targetType); } diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java index b5f18271..1c001d96 100644 --- a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -131,6 +131,15 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe } } + @Override + public boolean containsProperty(String key) { + return (getProperty(key) != null); + } + + @Override + public String getProperty(String key) { + return getProperty(key, String.class); + } @Override public String getProperty(String key, String defaultValue) { @@ -144,6 +153,12 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe return (value != null ? value : defaultValue); } + @Override + @Deprecated + public Class getPropertyAsClass(String key, Class targetValueType) { + throw new UnsupportedOperationException(); + } + @Override public String getRequiredProperty(String key) throws IllegalStateException { String value = getProperty(key); diff --git a/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java index e1ec8a27..bc626d64 100644 --- a/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,10 @@ package org.springframework.core.env; import org.springframework.core.convert.support.ConfigurableConversionService; /** - * Configuration interface to be implemented by most if not all {@link PropertyResolver - * PropertyResolver} types. Provides facilities for accessing and customizing the - * {@link org.springframework.core.convert.ConversionService ConversionService} used when - * converting property values from one type to another. + * Configuration interface to be implemented by most if not all {@link PropertyResolver} + * types. Provides facilities for accessing and customizing the + * {@link org.springframework.core.convert.ConversionService ConversionService} + * used when converting property values from one type to another. * * @author Chris Beams * @since 3.1 @@ -30,7 +30,7 @@ import org.springframework.core.convert.support.ConfigurableConversionService; public interface ConfigurablePropertyResolver extends PropertyResolver { /** - * @return the {@link ConfigurableConversionService} used when performing type + * Return the {@link ConfigurableConversionService} used when performing type * conversions on properties. *

The configurable nature of the returned conversion service allows for * the convenient addition and removal of individual {@code Converter} instances: @@ -46,10 +46,10 @@ public interface ConfigurablePropertyResolver extends PropertyResolver { /** * Set the {@link ConfigurableConversionService} to be used when performing type * conversions on properties. - *

Note: as an alternative to fully replacing the {@code - * ConversionService}, consider adding or removing individual {@code Converter} - * instances by drilling into {@link #getConversionService()} and calling methods - * such as {@code #addConverter}. + *

Note: as an alternative to fully replacing the + * {@code ConversionService}, consider adding or removing individual + * {@code Converter} instances by drilling into {@link #getConversionService()} + * and calling methods such as {@code #addConverter}. * @see PropertyResolver#getProperty(String, Class) * @see #getConversionService() * @see org.springframework.core.convert.converter.ConverterRegistry#addConverter diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java index 17570f77..b7b89930 100644 --- a/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ package org.springframework.core.env; * Interface for resolving properties against any underlying source. * * @author Chris Beams + * @author Juergen Hoeller * @since 3.1 * @see Environment * @see PropertySourcesPropertyResolver @@ -27,14 +28,14 @@ package org.springframework.core.env; public interface PropertyResolver { /** - * Return whether the given property key is available for resolution, i.e., - * the value for the given key is not {@code null}. + * Return whether the given property key is available for resolution, + * i.e. if the value for the given key is not {@code null}. */ boolean containsProperty(String key); /** - * Return the property value associated with the given key, or {@code null} - * if the key cannot be resolved. + * Return the property value associated with the given key, + * or {@code null} if the key cannot be resolved. * @param key the property name to resolve * @see #getProperty(String, String) * @see #getProperty(String, Class) @@ -53,8 +54,8 @@ public interface PropertyResolver { String getProperty(String key, String defaultValue); /** - * Return the property value associated with the given key, or {@code null} - * if the key cannot be resolved. + * Return the property value associated with the given key, + * or {@code null} if the key cannot be resolved. * @param key the property name to resolve * @param targetType the expected type of the property value * @see #getRequiredProperty(String, Class) @@ -62,8 +63,8 @@ public interface PropertyResolver { T getProperty(String key, Class targetType); /** - * Return the property value associated with the given key, or - * {@code defaultValue} if the key cannot be resolved. + * Return the property value associated with the given key, + * or {@code defaultValue} if the key cannot be resolved. * @param key the property name to resolve * @param targetType the expected type of the property value * @param defaultValue the default value to return if no value is found @@ -75,10 +76,13 @@ public interface PropertyResolver { * Convert the property value associated with the given key to a {@code Class} * of type {@code T} or {@code null} if the key cannot be resolved. * @throws org.springframework.core.convert.ConversionException if class specified - * by property value cannot be found or loaded or if targetType is not assignable + * by property value cannot be found or loaded or if targetType is not assignable * from class specified by property value * @see #getProperty(String, Class) + * @deprecated as of 4.3, in favor of {@link #getProperty} with manual conversion + * to {@code Class} via the application's {@code ClassLoader} */ + @Deprecated Class getPropertyAsClass(String key, Class targetType); /** diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertySourcesPropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/PropertySourcesPropertyResolver.java index edf0b729..32462043 100644 --- a/spring-core/src/main/java/org/springframework/core/env/PropertySourcesPropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/PropertySourcesPropertyResolver.java @@ -24,6 +24,7 @@ import org.springframework.util.ClassUtils; * an underlying set of {@link PropertySources}. * * @author Chris Beams + * @author Juergen Hoeller * @since 3.1 * @see PropertySource * @see PropertySources @@ -71,56 +72,38 @@ public class PropertySourcesPropertyResolver extends AbstractPropertyResolver { } protected T getProperty(String key, Class targetValueType, boolean resolveNestedPlaceholders) { - boolean debugEnabled = logger.isDebugEnabled(); - if (logger.isTraceEnabled()) { - logger.trace(String.format("getProperty(\"%s\", %s)", key, targetValueType.getSimpleName())); - } if (this.propertySources != null) { for (PropertySource propertySource : this.propertySources) { - if (debugEnabled) { - logger.debug(String.format("Searching for key '%s' in [%s]", key, propertySource.getName())); + if (logger.isTraceEnabled()) { + logger.trace(String.format("Searching for key '%s' in [%s]", key, propertySource.getName())); } Object value = propertySource.getProperty(key); if (value != null) { - Class valueType = value.getClass(); if (resolveNestedPlaceholders && value instanceof String) { value = resolveNestedPlaceholders((String) value); } - if (debugEnabled) { - logger.debug(String.format("Found key '%s' in [%s] with type [%s] and value '%s'", - key, propertySource.getName(), valueType.getSimpleName(), value)); - } - if (!this.conversionService.canConvert(valueType, targetValueType)) { - throw new IllegalArgumentException(String.format( - "Cannot convert value [%s] from source type [%s] to target type [%s]", - value, valueType.getSimpleName(), targetValueType.getSimpleName())); - } + logKeyFound(key, propertySource, value); return this.conversionService.convert(value, targetValueType); } } } - if (debugEnabled) { - logger.debug(String.format("Could not find key '%s' in any property source. Returning [null]", key)); + if (logger.isDebugEnabled()) { + logger.debug(String.format("Could not find key '%s' in any property source", key)); } return null; } @Override + @Deprecated public Class getPropertyAsClass(String key, Class targetValueType) { - boolean debugEnabled = logger.isDebugEnabled(); - if (logger.isTraceEnabled()) { - logger.trace(String.format("getPropertyAsClass(\"%s\", %s)", key, targetValueType.getSimpleName())); - } if (this.propertySources != null) { for (PropertySource propertySource : this.propertySources) { - if (debugEnabled) { - logger.debug(String.format("Searching for key '%s' in [%s]", key, propertySource.getName())); + if (logger.isTraceEnabled()) { + logger.trace(String.format("Searching for key '%s' in [%s]", key, propertySource.getName())); } Object value = propertySource.getProperty(key); if (value != null) { - if (debugEnabled) { - logger.debug(String.format("Found key '%s' in [%s] with value '%s'", key, propertySource.getName(), value)); - } + logKeyFound(key, propertySource, value); Class clazz; if (value instanceof String) { try { @@ -131,7 +114,7 @@ public class PropertySourcesPropertyResolver extends AbstractPropertyResolver { } } else if (value instanceof Class) { - clazz = (Class)value; + clazz = (Class) value; } else { clazz = value.getClass(); @@ -145,22 +128,42 @@ public class PropertySourcesPropertyResolver extends AbstractPropertyResolver { } } } - if (debugEnabled) { - logger.debug(String.format("Could not find key '%s' in any property source. Returning [null]", key)); + if (logger.isDebugEnabled()) { + logger.debug(String.format("Could not find key '%s' in any property source", key)); } return null; } + /** + * Log the given key as found in the given {@link PropertySource}, resulting in + * the given value. + *

The default implementation writes a debug log message, including the value. + * Subclasses may override this to change the log level and/or the log message. + * @param key the key found + * @param propertySource the {@code PropertySource} that the key has been found in + * @param value the corresponding value + * @since 4.3.1 + */ + protected void logKeyFound(String key, PropertySource propertySource, Object value) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Found key '%s' in [%s] with type [%s] and value '%s'", + key, propertySource.getName(), value.getClass().getSimpleName(), value)); + } + } + @SuppressWarnings("serial") + @Deprecated private static class ClassConversionException extends ConversionException { public ClassConversionException(Class actual, Class expected) { - super(String.format("Actual type %s is not assignable to expected type %s", actual.getName(), expected.getName())); + super(String.format("Actual type %s is not assignable to expected type %s", + actual.getName(), expected.getName())); } public ClassConversionException(String actual, Class expected, Exception ex) { - super(String.format("Could not find/load class %s during attempt to convert to %s", actual, expected.getName()), ex); + super(String.format("Could not find/load class %s during attempt to convert to %s", + actual, expected.getName()), ex); } } diff --git a/spring-core/src/main/java/org/springframework/core/env/SystemEnvironmentPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/SystemEnvironmentPropertySource.java index d60d3b50..d72a822e 100644 --- a/spring-core/src/main/java/org/springframework/core/env/SystemEnvironmentPropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/SystemEnvironmentPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,8 @@ import org.springframework.util.Assert; * Specialization of {@link MapPropertySource} designed for use with * {@linkplain AbstractEnvironment#getSystemEnvironment() system environment variables}. * Compensates for constraints in Bash and other shells that do not allow for variables - * containing the period character; also allows for uppercase variations on property - * names for more idiomatic shell use. + * containing the period character and/or hyphen character; also allows for uppercase + * variations on property names for more idiomatic shell use. * *

For example, a call to {@code getProperty("foo.bar")} will attempt to find a value * for the original property or any 'equivalent' property, returning the first found: @@ -35,8 +35,9 @@ import org.springframework.util.Assert; *

  • {@code FOO.BAR} - original, with upper case
  • *
  • {@code FOO_BAR} - with underscores and upper case
  • * + * Any hyphen variant of the above would work as well, or even mix dot/hyphen variants. * - * The same applies for calls to {@link #containsProperty(String)}, which returns + *

    The same applies for calls to {@link #containsProperty(String)}, which returns * {@code true} if any of the above properties are present, otherwise {@code false}. * *

    This feature is particularly useful when specifying active or default profiles as @@ -102,29 +103,42 @@ public class SystemEnvironmentPropertySource extends MapPropertySource { */ private String resolvePropertyName(String name) { Assert.notNull(name, "Property name must not be null"); + String resolvedName = checkPropertyName(name); + if (resolvedName != null) { + return resolvedName; + } + String uppercasedName = name.toUpperCase(); + if (!name.equals(uppercasedName)) { + resolvedName = checkPropertyName(uppercasedName); + if (resolvedName != null) { + return resolvedName; + } + } + return name; + } + + private String checkPropertyName(String name) { + // Check name as-is if (containsKey(name)) { return name; } - - String usName = name.replace('.', '_'); - if (!name.equals(usName) && containsKey(usName)) { - return usName; + // Check name with just dots replaced + String noDotName = name.replace('.', '_'); + if (!name.equals(noDotName) && containsKey(noDotName)) { + return noDotName; } - - String ucName = name.toUpperCase(); - if (!name.equals(ucName)) { - if (containsKey(ucName)) { - return ucName; - } - else { - String usUcName = ucName.replace('.', '_'); - if (!ucName.equals(usUcName) && containsKey(usUcName)) { - return usUcName; - } - } + // Check name with just hyphens replaced + String noHyphenName = name.replace('-', '_'); + if (!name.equals(noHyphenName) && containsKey(noHyphenName)) { + return noHyphenName; } - - return name; + // Check name with dots and hyphens replaced + String noDotNoHyphenName = noDotName.replace('-', '_'); + if (!noDotName.equals(noDotNoHyphenName) && containsKey(noDotNoHyphenName)) { + return noDotNoHyphenName; + } + // Give up + return null; } private boolean containsKey(String name) { diff --git a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java index a81f6c38..8f61f36b 100644 --- a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,7 +72,7 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { } /** - * This implementation returns a File reference for the underlying class path + * This implementation returns a File reference for the given URI-identified * resource, provided that it refers to a file in the file system. * @see org.springframework.util.ResourceUtils#getFile(java.net.URI, String) */ diff --git a/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java b/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java index 2f22b14c..b7c5219f 100644 --- a/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java +++ b/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,9 @@ package org.springframework.core.io; import java.net.MalformedURLException; import java.net.URL; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -42,6 +45,8 @@ public class DefaultResourceLoader implements ResourceLoader { private ClassLoader classLoader; + private final Set protocolResolvers = new LinkedHashSet(4); + /** * Create a new DefaultResourceLoader. @@ -84,10 +89,40 @@ public class DefaultResourceLoader implements ResourceLoader { return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader()); } + /** + * Register the given resolver with this resource loader, allowing for + * additional protocols to be handled. + *

    Any such resolver will be invoked ahead of this loader's standard + * resolution rules. It may therefore also override any default rules. + * @since 4.3 + * @see #getProtocolResolvers() + */ + public void addProtocolResolver(ProtocolResolver resolver) { + Assert.notNull(resolver, "ProtocolResolver must not be null"); + this.protocolResolvers.add(resolver); + } + + /** + * Return the collection of currently registered protocol resolvers, + * allowing for introspection as well as modification. + * @since 4.3 + */ + public Collection getProtocolResolvers() { + return this.protocolResolvers; + } + @Override public Resource getResource(String location) { Assert.notNull(location, "Location must not be null"); + + for (ProtocolResolver protocolResolver : this.protocolResolvers) { + Resource resource = protocolResolver.resolve(location, this); + if (resource != null) { + return resource; + } + } + if (location.startsWith("/")) { return getResourceByPath(location); } diff --git a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java index 294a3e37..9179d921 100644 --- a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import org.springframework.util.StringUtils; /** * {@link Resource} implementation for {@code java.io.File} handles. - * Obviously supports resolution as File, and also as URL. + * Supports resolution as a {@code File} and also as a {@code URL}. * Implements the extended {@link WritableResource} interface. * * @author Juergen Hoeller @@ -85,7 +85,6 @@ public class FileSystemResource extends AbstractResource implements WritableReso return this.path; } - /** * This implementation returns whether the underlying file exists. * @see java.io.File#exists() diff --git a/spring-core/src/main/java/org/springframework/core/io/PathResource.java b/spring-core/src/main/java/org/springframework/core/io/PathResource.java index f84ccfa9..af046049 100644 --- a/spring-core/src/main/java/org/springframework/core/io/PathResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/PathResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -182,8 +182,8 @@ public class PathResource extends AbstractResource implements WritableResource { return this.path.toFile(); } catch (UnsupportedOperationException ex) { - // only Paths on the default file system can be converted to a File - // do exception translation for cases where conversion is not possible + // Only paths on the default file system can be converted to a File: + // Do exception translation for cases where conversion is not possible. throw new FileNotFoundException(this.path + " cannot be resolved to " + "absolute file path"); } } diff --git a/spring-core/src/main/java/org/springframework/core/io/ProtocolResolver.java b/spring-core/src/main/java/org/springframework/core/io/ProtocolResolver.java new file mode 100644 index 00000000..700c866e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ProtocolResolver.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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. + */ + +package org.springframework.core.io; + +/** + * A resolution strategy for protocol-specific resource handles. + * + *

    Used as an SPI for {@link DefaultResourceLoader}, allowing for + * custom protocols to be handled without subclassing the loader + * implementation (or application context implementation). + * + * @author Juergen Hoeller + * @since 4.3 + * @see DefaultResourceLoader#addProtocolResolver + */ +public interface ProtocolResolver { + + /** + * Resolve the given location against the given resource loader + * if this implementation's protocol matches. + * @param location the user-specified resource location + * @param resourceLoader the associated resource loader + * @return a corresponding {@code Resource} handle if the given location + * matches this resolver's protocol, or {@code null} otherwise + */ + Resource resolve(String location, ResourceLoader resourceLoader); + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/Resource.java b/spring-core/src/main/java/org/springframework/core/io/Resource.java index e3fc2a4e..ac4fcb29 100644 --- a/spring-core/src/main/java/org/springframework/core/io/Resource.java +++ b/spring-core/src/main/java/org/springframework/core/io/Resource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,26 +37,26 @@ import java.net.URL; * @see #getFile() * @see WritableResource * @see ContextResource - * @see FileSystemResource - * @see ClassPathResource * @see UrlResource + * @see ClassPathResource + * @see FileSystemResource + * @see PathResource * @see ByteArrayResource * @see InputStreamResource - * @see PathResource */ public interface Resource extends InputStreamSource { /** - * Return whether this resource actually exists in physical form. + * Determine whether this resource actually exists in physical form. *

    This method performs a definitive existence check, whereas the - * existence of a {@code Resource} handle only guarantees a - * valid descriptor handle. + * existence of a {@code Resource} handle only guarantees a valid + * descriptor handle. */ boolean exists(); /** - * Return whether the contents of this resource can be read, - * e.g. via {@link #getInputStream()} or {@link #getFile()}. + * Indicate whether the contents of this resource can be read via + * {@link #getInputStream()}. *

    Will be {@code true} for typical resource descriptors; * note that actual content reading may still fail when attempted. * However, a value of {@code false} is a definitive indication @@ -66,8 +66,8 @@ public interface Resource extends InputStreamSource { boolean isReadable(); /** - * Return whether this resource represents a handle with an open - * stream. If true, the InputStream cannot be read multiple times, + * Indicate whether this resource represents a handle with an open stream. + * If {@code true}, the InputStream cannot be read multiple times, * and must be read and closed to avoid resource leaks. *

    Will be {@code false} for typical resource descriptors. */ @@ -84,6 +84,7 @@ public interface Resource extends InputStreamSource { * Return a URI handle for this resource. * @throws IOException if the resource cannot be resolved as URI, * i.e. if the resource is not available as descriptor + * @since 2.5 */ URI getURI() throws IOException; diff --git a/spring-core/src/main/java/org/springframework/core/io/UrlResource.java b/spring-core/src/main/java/org/springframework/core/io/UrlResource.java index 41aa4a03..584b67f6 100644 --- a/spring-core/src/main/java/org/springframework/core/io/UrlResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/UrlResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ import org.springframework.util.StringUtils; /** * {@link Resource} implementation for {@code java.net.URL} locators. - *

    Supports resolution as a {@code URL} and also as a {@code File} in + * Supports resolution as a {@code URL} and also as a {@code File} in * case of the {@code "file:"} protocol. * * @author Juergen Hoeller @@ -61,6 +61,7 @@ public class UrlResource extends AbstractFileResolvingResource { * Create a new {@code UrlResource} based on the given URI object. * @param uri a URI * @throws MalformedURLException if the given URL path is not valid + * @since 2.5 */ public UrlResource(URI uri) throws MalformedURLException { Assert.notNull(uri, "URI must not be null"); @@ -133,6 +134,7 @@ public class UrlResource extends AbstractFileResolvingResource { } } + /** * Determine a cleaned URL for the given original URL. * @param originalUrl the original URL @@ -151,7 +153,6 @@ public class UrlResource extends AbstractFileResolvingResource { } } - /** * This implementation opens an InputStream for the given URL. *

    It sets the {@code useCaches} flag to {@code false}, diff --git a/spring-core/src/main/java/org/springframework/core/io/support/DefaultPropertySourceFactory.java b/spring-core/src/main/java/org/springframework/core/io/support/DefaultPropertySourceFactory.java new file mode 100644 index 00000000..5ff4c9df --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/DefaultPropertySourceFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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. + */ + +package org.springframework.core.io.support; + +import java.io.IOException; + +import org.springframework.core.env.PropertySource; + +/** + * The default implementation for {@link PropertySourceFactory}, + * wrapping every resource in a {@link ResourcePropertySource}. + * + * @author Juergen Hoeller + * @since 4.3 + * @see PropertySourceFactory + * @see ResourcePropertySource + */ +public class DefaultPropertySourceFactory implements PropertySourceFactory { + + @Override + public PropertySource createPropertySource(String name, EncodedResource resource) throws IOException { + return (name != null ? new ResourcePropertySource(name, resource) : new ResourcePropertySource(resource)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java index d69433b1..0e049403 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -26,6 +26,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; +import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.LinkedHashSet; @@ -144,6 +145,9 @@ import org.springframework.util.StringUtils; * root of expanded directories. This originates from a limitation in the JDK's * {@code ClassLoader.getResources()} method which only returns file system * locations for a passed-in empty String (indicating potential roots to search). + * This {@code ResourcePatternResolver} implementation is trying to mitigate the + * jar root lookup limitation through {@link URLClassLoader} introspection and + * "java.class.path" manifest evaluation; however, without portability guarantees. * *

    WARNING: Ant-style patterns with "classpath:" resources are not * guaranteed to find matching resources if the root package to search is available @@ -166,6 +170,7 @@ import org.springframework.util.StringUtils; * @author Colin Sampaleanu * @author Marius Bogoevici * @author Costin Leau + * @author Phil Webb * @since 1.0.2 * @see #CLASSPATH_ALL_URL_PREFIX * @see org.springframework.util.AntPathMatcher @@ -384,6 +389,12 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol } } } + + if (classLoader == ClassLoader.getSystemClassLoader()) { + // "java.class.path" manifest evaluation... + addClassPathManifestEntries(result); + } + if (classLoader != null) { try { // Hierarchy traversal... @@ -398,6 +409,41 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol } } + /** + * Determine jar file references from the "java.class.path." manifest property and add them + * to the given set of resources in the form of pointers to the root of the jar file content. + * @param result the set of resources to add jar roots to + * @since 4.3 + */ + protected void addClassPathManifestEntries(Set result) { + try { + String javaClassPathProperty = System.getProperty("java.class.path"); + for (String url : StringUtils.delimitedListToStringArray( + javaClassPathProperty, System.getProperty("path.separator"))) { + try { + if (url.endsWith(ResourceUtils.JAR_FILE_EXTENSION)) { + UrlResource jarResource = new UrlResource(ResourceUtils.JAR_URL_PREFIX + + ResourceUtils.FILE_URL_PREFIX + url + ResourceUtils.JAR_URL_SEPARATOR); + if (jarResource.exists()) { + result.add(jarResource); + } + } + } + catch (MalformedURLException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Cannot search for matching files underneath [" + url + + "] because it cannot be converted to a valid 'jar:' URL: " + ex.getMessage()); + } + } + } + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to evaluate 'java.class.path' manifest entries: " + ex); + } + } + } + /** * Find all resources that match the given location pattern via the * Ant-style PathMatcher. Supports resources in jar files and zip files @@ -416,11 +462,18 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol Set result = new LinkedHashSet(16); for (Resource rootDirResource : rootDirResources) { rootDirResource = resolveRootDirResource(rootDirResource); - if (rootDirResource.getURL().getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { - result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirResource, subPattern, getPathMatcher())); + URL rootDirURL = rootDirResource.getURL(); + if (equinoxResolveMethod != null) { + if (rootDirURL.getProtocol().startsWith("bundle")) { + rootDirURL = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirURL); + rootDirResource = new UrlResource(rootDirURL); + } } - else if (isJarResource(rootDirResource)) { - result.addAll(doFindPathMatchingJarResources(rootDirResource, subPattern)); + if (rootDirURL.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { + result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirURL, subPattern, getPathMatcher())); + } + else if (ResourceUtils.isJarURL(rootDirURL) || isJarResource(rootDirResource)) { + result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirURL, subPattern)); } else { result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern)); @@ -458,56 +511,61 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol /** * Resolve the specified resource for path matching. - *

    The default implementation detects an Equinox OSGi "bundleresource:" - * / "bundleentry:" URL and resolves it into a standard jar file URL that - * can be traversed using Spring's standard jar file traversal algorithm. + *

    By default, Equinox OSGi "bundleresource:" / "bundleentry:" URL will be + * resolved into a standard jar file URL that be traversed using Spring's + * standard jar file traversal algorithm. For any preceding custom resolution, + * override this method and replace the resource handle accordingly. * @param original the resource to resolve * @return the resolved resource (may be identical to the passed-in resource) * @throws IOException in case of resolution failure */ protected Resource resolveRootDirResource(Resource original) throws IOException { - if (equinoxResolveMethod != null) { - URL url = original.getURL(); - if (url.getProtocol().startsWith("bundle")) { - return new UrlResource((URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, url)); - } - } return original; } /** * Return whether the given resource handle indicates a jar resource * that the {@code doFindPathMatchingJarResources} method can handle. - *

    The default implementation checks against the URL protocols - * "jar", "zip" and "wsjar" (the latter are used by BEA WebLogic Server - * and IBM WebSphere, respectively, but can be treated like jar files). + *

    By default, the URL protocols "jar", "zip", "vfszip and "wsjar" + * will be treated as jar resources. This template method allows for + * detecting further kinds of jar-like resources, e.g. through + * {@code instanceof} checks on the resource handle type. * @param resource the resource handle to check * (usually the root directory to start path matching from) * @see #doFindPathMatchingJarResources * @see org.springframework.util.ResourceUtils#isJarURL */ protected boolean isJarResource(Resource resource) throws IOException { - return ResourceUtils.isJarURL(resource.getURL()); + return false; } /** * Find all resources in jar files that match the given location pattern * via the Ant-style PathMatcher. * @param rootDirResource the root directory as Resource + * @param rootDirURL the pre-resolved root directory URL * @param subPattern the sub pattern to match (below the root directory) * @return a mutable Set of matching Resource instances * @throws IOException in case of I/O errors + * @since 4.3 * @see java.net.JarURLConnection * @see org.springframework.util.PathMatcher */ - protected Set doFindPathMatchingJarResources(Resource rootDirResource, String subPattern) + @SuppressWarnings("deprecation") + protected Set doFindPathMatchingJarResources(Resource rootDirResource, URL rootDirURL, String subPattern) throws IOException { - URLConnection con = rootDirResource.getURL().openConnection(); + // Check deprecated variant for potential overriding first... + Set result = doFindPathMatchingJarResources(rootDirResource, subPattern); + if (result != null) { + return result; + } + + URLConnection con = rootDirURL.openConnection(); JarFile jarFile; String jarFileUrl; String rootEntryPath; - boolean newJarFile = false; + boolean closeJarFile; if (con instanceof JarURLConnection) { // Should usually be the case for traditional JAR files. @@ -517,13 +575,14 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol jarFileUrl = jarCon.getJarFileURL().toExternalForm(); JarEntry jarEntry = jarCon.getJarEntry(); rootEntryPath = (jarEntry != null ? jarEntry.getName() : ""); + closeJarFile = !jarCon.getUseCaches(); } else { // No JarURLConnection -> need to resort to URL file parsing. // We'll assume URLs of the format "jar:path!/entry", with the protocol // being arbitrary as long as following the entry format. // We'll also handle paths with and without leading "file:" prefix. - String urlFile = rootDirResource.getURL().getFile(); + String urlFile = rootDirURL.getFile(); try { int separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR); if (separatorIndex != -1) { @@ -536,7 +595,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol jarFileUrl = urlFile; rootEntryPath = ""; } - newJarFile = true; + closeJarFile = true; } catch (ZipException ex) { if (logger.isDebugEnabled()) { @@ -555,7 +614,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol // The Sun JRE does not return a slash here, but BEA JRockit does. rootEntryPath = rootEntryPath + "/"; } - Set result = new LinkedHashSet(8); + result = new LinkedHashSet(8); for (Enumeration entries = jarFile.entries(); entries.hasMoreElements();) { JarEntry entry = entries.nextElement(); String entryPath = entry.getName(); @@ -569,14 +628,29 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol return result; } finally { - // Close jar file, but only if freshly obtained - - // not from JarURLConnection, which might cache the file reference. - if (newJarFile) { + if (closeJarFile) { jarFile.close(); } } } + /** + * Find all resources in jar files that match the given location pattern + * via the Ant-style PathMatcher. + * @param rootDirResource the root directory as Resource + * @param subPattern the sub pattern to match (below the root directory) + * @return a mutable Set of matching Resource instances + * @throws IOException in case of I/O errors + * @deprecated as of Spring 4.3, in favor of + * {@link #doFindPathMatchingJarResources(Resource, URL, String)} + */ + @Deprecated + protected Set doFindPathMatchingJarResources(Resource rootDirResource, String subPattern) + throws IOException { + + return null; + } + /** * Resolve the given jar file URL into a JarFile object. */ @@ -706,6 +780,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol } return; } + Arrays.sort(dirContents); for (File content : dirContents) { String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/"); if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) { @@ -732,8 +807,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol private static class VfsResourceMatchingDelegate { public static Set findMatchingResources( - Resource rootResource, String locationPattern, PathMatcher pathMatcher) throws IOException { - Object root = VfsPatternUtils.findRoot(rootResource.getURL()); + URL rootDirURL, String locationPattern, PathMatcher pathMatcher) throws IOException { + + Object root = VfsPatternUtils.findRoot(rootDirURL); PatternVirtualFileVisitor visitor = new PatternVirtualFileVisitor(VfsPatternUtils.getPath(root), locationPattern, pathMatcher); VfsPatternUtils.visit(root, visitor); diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java new file mode 100644 index 00000000..4ab2cfdf --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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. + */ + +package org.springframework.core.io.support; + +import java.io.IOException; + +import org.springframework.core.env.PropertySource; + +/** + * Strategy interface for creating resource-based {@link PropertySource} wrappers. + * + * @author Juergen Hoeller + * @since 4.3 + * @see DefaultPropertySourceFactory + */ +public interface PropertySourceFactory { + + /** + * Create a {@link PropertySource} that wraps the given resource. + * @param name the name of the property source + * @param resource the resource (potentially encoded) to wrap + * @return the new {@link PropertySource} (never {@code null}) + * @throws IOException if resource resolution failed + */ + PropertySource createPropertySource(String name, EncodedResource resource) throws IOException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternUtils.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternUtils.java index 9c0c12ee..e9721a95 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,11 @@ package org.springframework.core.io.support; import org.springframework.core.io.ResourceLoader; -import org.springframework.util.Assert; import org.springframework.util.ResourceUtils; /** * Utility class for determining whether a given URL is a resource - * location that can be loaded via a ResourcePatternResolver. + * location that can be loaded via a {@link ResourcePatternResolver}. * *

    Callers will usually assume that a location is a relative path * if the {@link #isUrl(String)} method returns {@code false}. @@ -59,7 +58,6 @@ public abstract class ResourcePatternUtils { * @see PathMatchingResourcePatternResolver */ public static ResourcePatternResolver getResourcePatternResolver(ResourceLoader resourceLoader) { - Assert.notNull(resourceLoader, "ResourceLoader must not be null"); if (resourceLoader instanceof ResourcePatternResolver) { return (ResourcePatternResolver) resourceLoader; } diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourceRegion.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourceRegion.java new file mode 100644 index 00000000..a76be606 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourceRegion.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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. + */ + +package org.springframework.core.io.support; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * Region of a {@link Resource} implementation, materialized by a {@code position} + * within the {@link Resource} and a byte {@code count} for the length of that region. + * + * @author Arjen Poutsma + * @since 4.3 + */ +public class ResourceRegion { + + private final Resource resource; + + private final long position; + + private final long count; + + + /** + * Create a new {@code ResourceRegion} from a given {@link Resource}. + * This region of a resource is represented by a start {@code position} + * and a byte {@code count} within the given {@code Resource}. + * @param resource a Resource + * @param position the start position of the region in that resource + * @param count the byte count of the region in that resource + */ + public ResourceRegion(Resource resource, long position, long count) { + Assert.notNull(resource, "Resource must not be null"); + Assert.isTrue(position >= 0, "'position' must be larger than or equal to 0"); + Assert.isTrue(count >= 0, "'count' must be larger than or equal to 0"); + this.resource = resource; + this.position = position; + this.count = count; + } + + + /** + * Return the underlying {@link Resource} for this {@code ResourceRegion} + */ + public Resource getResource() { + return this.resource; + } + + /** + * Return the start position of this region in the underlying {@link Resource} + */ + public long getPosition() { + return this.position; + } + + /** + * Return the byte count of this region in the underlying {@link Resource} + */ + public long getCount() { + return this.count; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java b/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java index d4772499..908b54d3 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.core.io.support; import java.io.IOException; +import java.lang.reflect.Constructor; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -31,6 +32,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.io.UrlResource; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -132,10 +134,12 @@ public abstract class SpringFactoriesLoader { throw new IllegalArgumentException( "Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]"); } - return (T) instanceClass.newInstance(); + Constructor constructor = instanceClass.getDeclaredConstructor(); + ReflectionUtils.makeAccessible(constructor); + return (T) constructor.newInstance(); } catch (Throwable ex) { - throw new IllegalArgumentException("Cannot instantiate factory class: " + factoryClass.getName(), ex); + throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex); } } diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/SerializationDelegate.java b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializationDelegate.java new file mode 100644 index 00000000..1e661d6a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializationDelegate.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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. + */ + +package org.springframework.core.serializer.support; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.springframework.core.serializer.DefaultDeserializer; +import org.springframework.core.serializer.DefaultSerializer; +import org.springframework.core.serializer.Deserializer; +import org.springframework.core.serializer.Serializer; +import org.springframework.util.Assert; + +/** + * A convenient delegate with pre-arranged configuration state for common + * serialization needs. Implements {@link Serializer} and {@link Deserializer} + * itself, so can also be passed into such more specific callback methods. + * + * @author Juergen Hoeller + * @since 4.3 + */ +public class SerializationDelegate implements Serializer, Deserializer { + + private final Serializer serializer; + + private final Deserializer deserializer; + + + /** + * Create a {@code SerializationDelegate} with a default serializer/deserializer + * for the given {@code ClassLoader}. + * @see DefaultDeserializer + * @see DefaultDeserializer#DefaultDeserializer(ClassLoader) + */ + public SerializationDelegate(ClassLoader classLoader) { + this.serializer = new DefaultSerializer(); + this.deserializer = new DefaultDeserializer(classLoader); + } + + /** + * Create a {@code SerializationDelegate} with the given serializer/deserializer. + * @param serializer the {@link Serializer} to use (never {@code null)} + * @param deserializer the {@link Deserializer} to use (never {@code null)} + */ + public SerializationDelegate(Serializer serializer, Deserializer deserializer) { + Assert.notNull(serializer, "Serializer must not be null"); + Assert.notNull(deserializer, "Deserializer must not be null"); + this.serializer = serializer; + this.deserializer = deserializer; + } + + + @Override + public void serialize(Object object, OutputStream outputStream) throws IOException { + this.serializer.serialize(object, outputStream); + } + + @Override + public Object deserialize(InputStream inputStream) throws IOException { + return this.deserializer.deserialize(inputStream); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/style/ToStringCreator.java b/spring-core/src/main/java/org/springframework/core/style/ToStringCreator.java index 6f682939..a7291f6f 100644 --- a/spring-core/src/main/java/org/springframework/core/style/ToStringCreator.java +++ b/spring-core/src/main/java/org/springframework/core/style/ToStringCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,7 +82,7 @@ public class ToStringCreator { * @return this, to support call-chaining */ public ToStringCreator append(String fieldName, byte value) { - return append(fieldName, new Byte(value)); + return append(fieldName, Byte.valueOf(value)); } /** @@ -92,7 +92,7 @@ public class ToStringCreator { * @return this, to support call-chaining */ public ToStringCreator append(String fieldName, short value) { - return append(fieldName, new Short(value)); + return append(fieldName, Short.valueOf(value)); } /** @@ -102,7 +102,7 @@ public class ToStringCreator { * @return this, to support call-chaining */ public ToStringCreator append(String fieldName, int value) { - return append(fieldName, new Integer(value)); + return append(fieldName, Integer.valueOf(value)); } /** @@ -112,7 +112,7 @@ public class ToStringCreator { * @return this, to support call-chaining */ public ToStringCreator append(String fieldName, long value) { - return append(fieldName, new Long(value)); + return append(fieldName, Long.valueOf(value)); } /** @@ -122,7 +122,7 @@ public class ToStringCreator { * @return this, to support call-chaining */ public ToStringCreator append(String fieldName, float value) { - return append(fieldName, new Float(value)); + return append(fieldName, Float.valueOf(value)); } /** @@ -132,7 +132,7 @@ public class ToStringCreator { * @return this, to support call-chaining */ public ToStringCreator append(String fieldName, double value) { - return append(fieldName, new Double(value)); + return append(fieldName, Double.valueOf(value)); } /** diff --git a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java index 05e759ab..0483932a 100644 --- a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,6 +65,8 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator implement private ThreadFactory threadFactory; + private TaskDecorator taskDecorator; + /** * Create a new SimpleAsyncTaskExecutor with default thread name prefix. @@ -109,6 +111,20 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator implement return this.threadFactory; } + /** + * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} + * about to be executed. + *

    Note that such a decorator is not necessarily being applied to the + * user-supplied {@code Runnable}/{@code Callable} but rather to the actual + * execution callback (which may be a wrapper around the user-supplied task). + *

    The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + * @since 4.3 + */ + public final void setTaskDecorator(TaskDecorator taskDecorator) { + this.taskDecorator = taskDecorator; + } + /** * Set the maximum number of parallel accesses allowed. * -1 indicates no concurrency limit at all. @@ -163,12 +179,13 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator implement @Override public void execute(Runnable task, long startTimeout) { Assert.notNull(task, "Runnable must not be null"); + Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task); if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) { this.concurrencyThrottle.beforeAccess(); - doExecute(new ConcurrencyThrottlingRunnable(task)); + doExecute(new ConcurrencyThrottlingRunnable(taskToUse)); } else { - doExecute(task); + doExecute(taskToUse); } } diff --git a/spring-core/src/main/java/org/springframework/core/task/TaskDecorator.java b/spring-core/src/main/java/org/springframework/core/task/TaskDecorator.java new file mode 100644 index 00000000..d36a3ab3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/TaskDecorator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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. + */ + +package org.springframework.core.task; + +/** + * A callback interface for a decorator to be applied to any {@link Runnable} + * about to be executed. + * + *

    Note that such a decorator is not necessarily being applied to the + * user-supplied {@code Runnable}/{@code Callable} but rather to the actual + * execution callback (which may be a wrapper around the user-supplied task). + * + *

    The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + * + * @author Juergen Hoeller + * @since 4.3 + * @see TaskExecutor#execute(Runnable) + * @see SimpleAsyncTaskExecutor#setTaskDecorator + */ +public interface TaskDecorator { + + /** + * Decorate the given {@code Runnable}, returning a potentially wrapped + * {@code Runnable} for actual execution. + * @param runnable the original {@code Runnable} + * @return the decorated {@code Runnable} + */ + Runnable decorate(Runnable runnable); + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java index fad9ae09..165533df 100644 --- a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java +++ b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.util.concurrent.FutureTask; import java.util.concurrent.RejectedExecutionException; import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskRejectedException; import org.springframework.util.Assert; import org.springframework.util.concurrent.ListenableFuture; @@ -45,6 +46,8 @@ public class TaskExecutorAdapter implements AsyncListenableTaskExecutor { private final Executor concurrentExecutor; + private TaskDecorator taskDecorator; + /** * Create a new TaskExecutorAdapter, @@ -57,6 +60,21 @@ public class TaskExecutorAdapter implements AsyncListenableTaskExecutor { } + /** + * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} + * about to be executed. + *

    Note that such a decorator is not necessarily being applied to the + * user-supplied {@code Runnable}/{@code Callable} but rather to the actual + * execution callback (which may be a wrapper around the user-supplied task). + *

    The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + * @since 4.3 + */ + public final void setTaskDecorator(TaskDecorator taskDecorator) { + this.taskDecorator = taskDecorator; + } + + /** * Delegates to the specified JDK concurrent executor. * @see java.util.concurrent.Executor#execute(Runnable) @@ -64,7 +82,7 @@ public class TaskExecutorAdapter implements AsyncListenableTaskExecutor { @Override public void execute(Runnable task) { try { - this.concurrentExecutor.execute(task); + doExecute(this.concurrentExecutor, this.taskDecorator, task); } catch (RejectedExecutionException ex) { throw new TaskRejectedException( @@ -80,12 +98,12 @@ public class TaskExecutorAdapter implements AsyncListenableTaskExecutor { @Override public Future submit(Runnable task) { try { - if (this.concurrentExecutor instanceof ExecutorService) { + if (this.taskDecorator == null && this.concurrentExecutor instanceof ExecutorService) { return ((ExecutorService) this.concurrentExecutor).submit(task); } else { FutureTask future = new FutureTask(task, null); - this.concurrentExecutor.execute(future); + doExecute(this.concurrentExecutor, this.taskDecorator, future); return future; } } @@ -98,12 +116,12 @@ public class TaskExecutorAdapter implements AsyncListenableTaskExecutor { @Override public Future submit(Callable task) { try { - if (this.concurrentExecutor instanceof ExecutorService) { + if (this.taskDecorator == null && this.concurrentExecutor instanceof ExecutorService) { return ((ExecutorService) this.concurrentExecutor).submit(task); } else { FutureTask future = new FutureTask(task); - this.concurrentExecutor.execute(future); + doExecute(this.concurrentExecutor, this.taskDecorator, future); return future; } } @@ -117,7 +135,7 @@ public class TaskExecutorAdapter implements AsyncListenableTaskExecutor { public ListenableFuture submitListenable(Runnable task) { try { ListenableFutureTask future = new ListenableFutureTask(task, null); - this.concurrentExecutor.execute(future); + doExecute(this.concurrentExecutor, this.taskDecorator, future); return future; } catch (RejectedExecutionException ex) { @@ -130,7 +148,7 @@ public class TaskExecutorAdapter implements AsyncListenableTaskExecutor { public ListenableFuture submitListenable(Callable task) { try { ListenableFutureTask future = new ListenableFutureTask(task); - this.concurrentExecutor.execute(future); + doExecute(this.concurrentExecutor, this.taskDecorator, future); return future; } catch (RejectedExecutionException ex) { @@ -139,4 +157,20 @@ public class TaskExecutorAdapter implements AsyncListenableTaskExecutor { } } + + /** + * Actually execute the given {@code Runnable} (which may be a user-supplied task + * or a wrapper around a user-supplied task) with the given executor. + * @param concurrentExecutor the underlying JDK concurrent executor to delegate to + * @param taskDecorator the specified decorator to be applied, if any + * @param runnable the runnable to execute + * @throws RejectedExecutionException if the given runnable cannot be accepted + * @since 4.3 + */ + protected void doExecute(Executor concurrentExecutor, TaskDecorator taskDecorator, Runnable runnable) + throws RejectedExecutionException{ + + concurrentExecutor.execute(taskDecorator != null ? taskDecorator.decorate(runnable) : runnable); + } + } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AbstractRecursiveAnnotationVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AbstractRecursiveAnnotationVisitor.java index 2cc12717..bd42e059 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/AbstractRecursiveAnnotationVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AbstractRecursiveAnnotationVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,7 +58,7 @@ abstract class AbstractRecursiveAnnotationVisitor extends AnnotationVisitor { @Override public AnnotationVisitor visitAnnotation(String attributeName, String asmTypeDescriptor) { String annotationType = Type.getType(asmTypeDescriptor).getClassName(); - AnnotationAttributes nestedAttributes = new AnnotationAttributes(); + AnnotationAttributes nestedAttributes = new AnnotationAttributes(annotationType, this.classLoader); this.attributes.put(attributeName, nestedAttributes); return new RecursiveAnnotationAttributesVisitor(annotationType, nestedAttributes, this.classLoader); } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java index ab001b50..f35ecd68 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java @@ -44,8 +44,6 @@ import org.springframework.util.ObjectUtils; */ final class AnnotationAttributesReadingVisitor extends RecursiveAnnotationAttributesVisitor { - private final String annotationType; - private final MultiValueMap attributesMap; private final Map> metaAnnotationMap; @@ -55,38 +53,41 @@ final class AnnotationAttributesReadingVisitor extends RecursiveAnnotationAttrib MultiValueMap attributesMap, Map> metaAnnotationMap, ClassLoader classLoader) { - super(annotationType, new AnnotationAttributes(), classLoader); - this.annotationType = annotationType; + super(annotationType, new AnnotationAttributes(annotationType, classLoader), classLoader); this.attributesMap = attributesMap; this.metaAnnotationMap = metaAnnotationMap; } @Override - public void doVisitEnd(Class annotationClass) { - super.doVisitEnd(annotationClass); - List attributes = this.attributesMap.get(this.annotationType); - if (attributes == null) { - this.attributesMap.add(this.annotationType, this.attributes); - } - else { - attributes.add(0, this.attributes); - } - Set visited = new LinkedHashSet(); - Annotation[] metaAnnotations = AnnotationUtils.getAnnotations(annotationClass); - if (!ObjectUtils.isEmpty(metaAnnotations)) { - for (Annotation metaAnnotation : metaAnnotations) { - if (!AnnotationUtils.isInJavaLangAnnotationPackage(metaAnnotation)) { - recursivelyCollectMetaAnnotations(visited, metaAnnotation); + public void visitEnd() { + super.visitEnd(); + + Class annotationClass = this.attributes.annotationType(); + if (annotationClass != null) { + List attributeList = this.attributesMap.get(this.annotationType); + if (attributeList == null) { + this.attributesMap.add(this.annotationType, this.attributes); + } + else { + attributeList.add(0, this.attributes); + } + Set visited = new LinkedHashSet(); + Annotation[] metaAnnotations = AnnotationUtils.getAnnotations(annotationClass); + if (!ObjectUtils.isEmpty(metaAnnotations)) { + for (Annotation metaAnnotation : metaAnnotations) { + if (!AnnotationUtils.isInJavaLangAnnotationPackage(metaAnnotation)) { + recursivelyCollectMetaAnnotations(visited, metaAnnotation); + } } } - } - if (this.metaAnnotationMap != null) { - Set metaAnnotationTypeNames = new LinkedHashSet(visited.size()); - for (Annotation ann : visited) { - metaAnnotationTypeNames.add(ann.annotationType().getName()); + if (this.metaAnnotationMap != null) { + Set metaAnnotationTypeNames = new LinkedHashSet(visited.size()); + for (Annotation ann : visited) { + metaAnnotationTypeNames.add(ann.annotationType().getName()); + } + this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames); } - this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames); } } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java index eea3d825..3b3a17dd 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java @@ -131,7 +131,8 @@ public class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisito public AnnotationAttributes getAnnotationAttributes(String annotationName, boolean classValuesAsString) { AnnotationAttributes raw = AnnotationReadingVisitorUtils.getMergedAnnotationAttributes( this.attributesMap, this.metaAnnotationMap, annotationName); - return AnnotationReadingVisitorUtils.convertClassValues(this.classLoader, raw, classValuesAsString); + return AnnotationReadingVisitorUtils.convertClassValues( + "class '" + getClassName() + "'", this.classLoader, raw, classValuesAsString); } @Override @@ -148,7 +149,7 @@ public class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisito } for (AnnotationAttributes raw : attributes) { for (Map.Entry entry : AnnotationReadingVisitorUtils.convertClassValues( - this.classLoader, raw, classValuesAsString).entrySet()) { + "class '" + getClassName() + "'", this.classLoader, raw, classValuesAsString).entrySet()) { allAttributes.add(entry.getKey(), entry.getValue()); } } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java index 0d7ba35f..93c3e76d 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java @@ -41,25 +41,29 @@ import org.springframework.util.ObjectUtils; */ abstract class AnnotationReadingVisitorUtils { - public static AnnotationAttributes convertClassValues(ClassLoader classLoader, AnnotationAttributes original, - boolean classValuesAsString) { + public static AnnotationAttributes convertClassValues(Object annotatedElement, + ClassLoader classLoader, AnnotationAttributes original, boolean classValuesAsString) { if (original == null) { return null; } - AnnotationAttributes result = new AnnotationAttributes(original.size()); - for (Map.Entry entry : original.entrySet()) { + AnnotationAttributes result = new AnnotationAttributes(original); + AnnotationUtils.postProcessAnnotationAttributes(annotatedElement, result, classValuesAsString); + + for (Map.Entry entry : result.entrySet()) { try { Object value = entry.getValue(); if (value instanceof AnnotationAttributes) { - value = convertClassValues(classLoader, (AnnotationAttributes) value, classValuesAsString); + value = convertClassValues( + annotatedElement, classLoader, (AnnotationAttributes) value, classValuesAsString); } else if (value instanceof AnnotationAttributes[]) { AnnotationAttributes[] values = (AnnotationAttributes[]) value; for (int i = 0; i < values.length; i++) { - values[i] = convertClassValues(classLoader, values[i], classValuesAsString); + values[i] = convertClassValues(annotatedElement, classLoader, values[i], classValuesAsString); } + value = values; } else if (value instanceof Type) { value = (classValuesAsString ? ((Type) value).getClassName() : @@ -67,7 +71,8 @@ abstract class AnnotationReadingVisitorUtils { } else if (value instanceof Type[]) { Type[] array = (Type[]) value; - Object[] convArray = (classValuesAsString ? new String[array.length] : new Class[array.length]); + Object[] convArray = + (classValuesAsString ? new String[array.length] : new Class[array.length]); for (int i = 0; i < array.length; i++) { convArray[i] = (classValuesAsString ? array[i].getClassName() : classLoader.loadClass(array[i].getClassName())); @@ -75,11 +80,11 @@ abstract class AnnotationReadingVisitorUtils { value = convArray; } else if (classValuesAsString) { - if (value instanceof Class) { + if (value instanceof Class) { value = ((Class) value).getName(); } - else if (value instanceof Class[]) { - Class[] clazzArray = (Class[]) value; + else if (value instanceof Class[]) { + Class[] clazzArray = (Class[]) value; String[] newValue = new String[clazzArray.length]; for (int i = 0; i < clazzArray.length; i++) { newValue[i] = clazzArray[i].getName(); @@ -87,13 +92,14 @@ abstract class AnnotationReadingVisitorUtils { value = newValue; } } - result.put(entry.getKey(), value); + entry.setValue(value); } catch (Exception ex) { // Class not found - can't resolve class reference in annotation attribute. result.put(entry.getKey(), ex); } } + return result; } @@ -123,13 +129,12 @@ abstract class AnnotationReadingVisitorUtils { return null; } - // To start with, we populate the results with a copy of all attribute - // values from the target annotation. A copy is necessary so that we do - // not inadvertently mutate the state of the metadata passed to this - // method. - AnnotationAttributes results = new AnnotationAttributes(attributesList.get(0)); + // To start with, we populate the result with a copy of all attribute values + // from the target annotation. A copy is necessary so that we do not + // inadvertently mutate the state of the metadata passed to this method. + AnnotationAttributes result = new AnnotationAttributes(attributesList.get(0)); - Set overridableAttributeNames = new HashSet(results.keySet()); + Set overridableAttributeNames = new HashSet(result.keySet()); overridableAttributeNames.remove(AnnotationUtils.VALUE); // Since the map is a LinkedMultiValueMap, we depend on the ordering of @@ -152,14 +157,14 @@ abstract class AnnotationReadingVisitorUtils { if (value != null) { // Store the value, potentially overriding a value from an attribute // of the same name found higher in the annotation hierarchy. - results.put(overridableAttributeName, value); + result.put(overridableAttributeName, value); } } } } } - return results; + return result; } } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java index 00bd9cb1..9dfcfd1a 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java @@ -122,7 +122,8 @@ public class MethodMetadataReadingVisitor extends MethodVisitor implements Metho public AnnotationAttributes getAnnotationAttributes(String annotationName, boolean classValuesAsString) { AnnotationAttributes raw = AnnotationReadingVisitorUtils.getMergedAnnotationAttributes( this.attributesMap, this.metaAnnotationMap, annotationName); - return AnnotationReadingVisitorUtils.convertClassValues(this.classLoader, raw, classValuesAsString); + return AnnotationReadingVisitorUtils.convertClassValues( + "method '" + getMethodName() + "'", this.classLoader, raw, classValuesAsString); } @Override @@ -137,8 +138,9 @@ public class MethodMetadataReadingVisitor extends MethodVisitor implements Metho } MultiValueMap allAttributes = new LinkedMultiValueMap(); for (AnnotationAttributes annotationAttributes : this.attributesMap.get(annotationName)) { - for (Map.Entry entry : AnnotationReadingVisitorUtils.convertClassValues( - this.classLoader, annotationAttributes, classValuesAsString).entrySet()) { + AnnotationAttributes convertedAttributes = AnnotationReadingVisitorUtils.convertClassValues( + "method '" + getMethodName() + "'", this.classLoader, annotationAttributes, classValuesAsString); + for (Map.Entry entry : convertedAttributes.entrySet()) { allAttributes.add(entry.getKey(), entry.getValue()); } } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationArrayVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationArrayVisitor.java index 3c5bd9a4..9c466b77 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationArrayVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationArrayVisitor.java @@ -69,7 +69,7 @@ class RecursiveAnnotationArrayVisitor extends AbstractRecursiveAnnotationVisitor @Override public AnnotationVisitor visitAnnotation(String attributeName, String asmTypeDescriptor) { String annotationType = Type.getType(asmTypeDescriptor).getClassName(); - AnnotationAttributes nestedAttributes = new AnnotationAttributes(); + AnnotationAttributes nestedAttributes = new AnnotationAttributes(annotationType, this.classLoader); this.allNestedAttributes.add(nestedAttributes); return new RecursiveAnnotationAttributesVisitor(annotationType, nestedAttributes, this.classLoader); } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationAttributesVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationAttributesVisitor.java index 0d2176f0..ff7b92fb 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationAttributesVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationAttributesVisitor.java @@ -16,10 +16,6 @@ package org.springframework.core.type.classreading; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; - import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; @@ -30,7 +26,7 @@ import org.springframework.core.annotation.AnnotationUtils; */ class RecursiveAnnotationAttributesVisitor extends AbstractRecursiveAnnotationVisitor { - private final String annotationType; + protected final String annotationType; public RecursiveAnnotationAttributesVisitor( @@ -42,49 +38,8 @@ class RecursiveAnnotationAttributesVisitor extends AbstractRecursiveAnnotationVi @Override - public final void visitEnd() { - try { - Class annotationClass = this.classLoader.loadClass(this.annotationType); - doVisitEnd(annotationClass); - } - catch (ClassNotFoundException ex) { - logger.debug("Failed to class-load type while reading annotation metadata. " + - "This is a non-fatal error, but certain annotation metadata may be unavailable.", ex); - } - } - - protected void doVisitEnd(Class annotationClass) { - registerDefaultValues(annotationClass); - } - - private void registerDefaultValues(Class annotationClass) { - // Only do defaults scanning for public annotations; we'd run into - // IllegalAccessExceptions otherwise, and we don't want to mess with - // accessibility in a SecurityManager environment. - if (Modifier.isPublic(annotationClass.getModifiers())) { - // Check declared default values of attributes in the annotation type. - Method[] annotationAttributes = annotationClass.getMethods(); - for (Method annotationAttribute : annotationAttributes) { - String attributeName = annotationAttribute.getName(); - Object defaultValue = annotationAttribute.getDefaultValue(); - if (defaultValue != null && !this.attributes.containsKey(attributeName)) { - if (defaultValue instanceof Annotation) { - defaultValue = AnnotationAttributes.fromMap(AnnotationUtils.getAnnotationAttributes( - (Annotation) defaultValue, false, true)); - } - else if (defaultValue instanceof Annotation[]) { - Annotation[] realAnnotations = (Annotation[]) defaultValue; - AnnotationAttributes[] mappedAnnotations = new AnnotationAttributes[realAnnotations.length]; - for (int i = 0; i < realAnnotations.length; i++) { - mappedAnnotations[i] = AnnotationAttributes.fromMap( - AnnotationUtils.getAnnotationAttributes(realAnnotations[i], false, true)); - } - defaultValue = mappedAnnotations; - } - this.attributes.put(attributeName, defaultValue); - } - } - } + public void visitEnd() { + AnnotationUtils.registerDefaultValues(this.attributes); } } diff --git a/spring-core/src/main/java/org/springframework/lang/UsesSunMisc.java b/spring-core/src/main/java/org/springframework/lang/UsesSunMisc.java new file mode 100644 index 00000000..5bcbd4a9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/lang/UsesSunMisc.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * 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. + */ + +package org.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the annotated element uses an API from the {@code sun.misc} + * package. + * + * @author Stephane Nicoll + * @since 4.3 + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +@Documented +public @interface UsesSunMisc { +} diff --git a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java index 635f1234..5e91704b 100644 --- a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java +++ b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,6 +74,8 @@ public class AntPathMatcher implements PathMatcher { private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?\\}"); + private static final char[] WILDCARD_CHARS = { '*', '?', '{' }; + private String pathSeparator; @@ -81,7 +83,7 @@ public class AntPathMatcher implements PathMatcher { private boolean caseSensitive = true; - private boolean trimTokens = true; + private boolean trimTokens = false; private volatile Boolean cachePatterns; @@ -130,7 +132,7 @@ public class AntPathMatcher implements PathMatcher { /** * Specify whether to trim tokenized paths and patterns. - *

    Default is {@code true}. + *

    Default is {@code false}. */ public void setTrimTokens(boolean trimTokens) { this.trimTokens = trimTokens; @@ -188,6 +190,10 @@ public class AntPathMatcher implements PathMatcher { } String[] pattDirs = tokenizePattern(pattern); + if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) { + return false; + } + String[] pathDirs = tokenizePath(path); int pattIdxStart = 0; @@ -307,6 +313,59 @@ public class AntPathMatcher implements PathMatcher { return true; } + private boolean isPotentialMatch(String path, String[] pattDirs) { + if (!this.trimTokens) { + char[] pathChars = path.toCharArray(); + int pos = 0; + for (String pattDir : pattDirs) { + int skipped = skipSeparator(path, pos, this.pathSeparator); + pos += skipped; + skipped = skipSegment(pathChars, pos, pattDir); + if (skipped < pattDir.length()) { + if (skipped > 0) { + return true; + } + return (pattDir.length() > 0) && isWildcardChar(pattDir.charAt(0)); + } + pos += skipped; + } + } + return true; + } + + private int skipSegment(char[] chars, int pos, String prefix) { + int skipped = 0; + for (char c : prefix.toCharArray()) { + if (isWildcardChar(c)) { + return skipped; + } + else if (pos + skipped >= chars.length) { + return 0; + } + else if (chars[pos + skipped] == c) { + skipped++; + } + } + return skipped; + } + + private int skipSeparator(String path, int pos, String separator) { + int skipped = 0; + while (path.startsWith(separator, pos + skipped)) { + skipped += separator.length(); + } + return skipped; + } + + private boolean isWildcardChar(char c) { + for (char candidate : WILDCARD_CHARS) { + if (c == candidate) { + return true; + } + } + return false; + } + /** * Tokenize the given path pattern into parts, based on this matcher's settings. *

    Performs caching based on {@link #setCachePatterns}, delegating to diff --git a/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java b/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java index 9cea1c99..4bea60c8 100644 --- a/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java +++ b/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -263,13 +263,16 @@ public class AutoPopulatingList implements List, Serializable { public ElementInstantiationException(String msg) { super(msg); } + + public ElementInstantiationException(String message, Throwable cause) { + super(message, cause); + } } /** * Reflective implementation of the ElementFactory interface, * using {@code Class.newInstance()} on a given element class. - * @see Class#newInstance() */ private static class ReflectiveElementFactory implements ElementFactory, Serializable { @@ -288,12 +291,12 @@ public class AutoPopulatingList implements List, Serializable { return this.elementClass.newInstance(); } catch (InstantiationException ex) { - throw new ElementInstantiationException("Unable to instantiate element class [" + - this.elementClass.getName() + "]. Root cause is " + ex); + throw new ElementInstantiationException( + "Unable to instantiate element class: " + this.elementClass.getName(), ex); } catch (IllegalAccessException ex) { - throw new ElementInstantiationException("Cannot access element class [" + - this.elementClass.getName() + "]. Root cause is " + ex); + throw new ElementInstantiationException( + "Could not access element constructor: " + this.elementClass.getName(), ex); } } } diff --git a/spring-core/src/main/java/org/springframework/util/Base64Utils.java b/spring-core/src/main/java/org/springframework/util/Base64Utils.java index 17c48614..f10b60d6 100644 --- a/spring-core/src/main/java/org/springframework/util/Base64Utils.java +++ b/spring-core/src/main/java/org/springframework/util/Base64Utils.java @@ -30,11 +30,11 @@ import org.springframework.lang.UsesJava8; * Codec present, {@link #encode}/{@link #decode} calls will throw an IllegalStateException. * However, as of Spring 4.2, {@link #encodeToString} and {@link #decodeFromString} will * nevertheless work since they can delegate to the JAXB DatatypeConverter as a fallback. - * However, this does not apply when using the ...UrlSafe... methods for RFC 4648 "URL and + * However, this does not apply when using the "UrlSafe" methods for RFC 4648 "URL and * Filename Safe Alphabet"; a delegate is required. - *

    - * Note: Apache Commons Codec does not add padding ({@code =}) when encoding with - * the URL and Filename Safe Alphabet. + * + *

    Note: Apache Commons Codec does not add padding ({@code =}) when encoding + * with the URL and Filename Safe Alphabet. * * @author Juergen Hoeller * @author Gary Russell diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index 93dd3d61..6d152017 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,16 +56,16 @@ public abstract class ClassUtils { /** Prefix for internal non-primitive array class names: "[L" */ private static final String NON_PRIMITIVE_ARRAY_PREFIX = "[L"; - /** The package separator character '.' */ + /** The package separator character: '.' */ private static final char PACKAGE_SEPARATOR = '.'; - /** The path separator character '/' */ + /** The path separator character: '/' */ private static final char PATH_SEPARATOR = '/'; - /** The inner class separator character '$' */ + /** The inner class separator character: '$' */ private static final char INNER_CLASS_SEPARATOR = '$'; - /** The CGLIB class separator character "$$" */ + /** The CGLIB class separator: "$$" */ public static final String CGLIB_CLASS_SEPARATOR = "$$"; /** The ".class" file suffix */ @@ -1158,7 +1158,6 @@ public abstract class ClassUtils { */ public static Class createCompositeInterface(Class[] interfaces, ClassLoader classLoader) { Assert.notEmpty(interfaces, "Interfaces must not be empty"); - Assert.notNull(classLoader, "ClassLoader must not be null"); return Proxy.getProxyClass(classLoader, interfaces); } diff --git a/spring-core/src/main/java/org/springframework/util/DigestUtils.java b/spring-core/src/main/java/org/springframework/util/DigestUtils.java index 8de0b513..36b1a583 100644 --- a/spring-core/src/main/java/org/springframework/util/DigestUtils.java +++ b/spring-core/src/main/java/org/springframework/util/DigestUtils.java @@ -23,14 +23,15 @@ import java.security.NoSuchAlgorithmException; /** * Miscellaneous methods for calculating digests. + * *

    Mainly for internal use within the framework; consider - * Apache Commons Codec for a - * more comprehensive suite of digest utilities. + * Apache Commons Codec + * for a more comprehensive suite of digest utilities. * * @author Arjen Poutsma + * @author Juergen Hoeller * @author Craig Andrews * @since 3.0 - * @see org.apache.commons.codec.digest.DigestUtils */ public abstract class DigestUtils { @@ -50,8 +51,8 @@ public abstract class DigestUtils { } /** - * Calculate the MD5 digest of the given InputStream. - * @param inputStream the inputStream to calculate the digest over + * Calculate the MD5 digest of the given stream. + * @param inputStream the InputStream to calculate the digest over * @return the digest * @since 4.2 */ @@ -60,8 +61,7 @@ public abstract class DigestUtils { } /** - * Return a hexadecimal string representation of the MD5 digest of the given - * bytes. + * Return a hexadecimal string representation of the MD5 digest of the given bytes. * @param bytes the bytes to calculate the digest over * @return a hexadecimal digest string */ @@ -70,9 +70,8 @@ public abstract class DigestUtils { } /** - * Return a hexadecimal string representation of the MD5 digest of the given - * inputStream. - * @param inputStream the inputStream to calculate the digest over + * Return a hexadecimal string representation of the MD5 digest of the given stream. + * @param inputStream the InputStream to calculate the digest over * @return a hexadecimal digest string * @since 4.2 */ @@ -128,7 +127,12 @@ public abstract class DigestUtils { return messageDigest.digest(); } else { - return messageDigest.digest(StreamUtils.copyToByteArray(inputStream)); + final byte[] buffer = new byte[StreamUtils.BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = inputStream.read(buffer)) != -1) { + messageDigest.update(buffer, 0, bytesRead); + } + return messageDigest.digest(); } } diff --git a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java index cde4e615..51dd02c6 100644 --- a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java +++ b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java @@ -36,7 +36,7 @@ import java.util.Map; @SuppressWarnings("serial") public class LinkedCaseInsensitiveMap extends LinkedHashMap { - private final Map caseInsensitiveKeys; + private Map caseInsensitiveKeys; private final Locale locale; @@ -151,6 +151,14 @@ public class LinkedCaseInsensitiveMap extends LinkedHashMap { super.clear(); } + @Override + @SuppressWarnings("unchecked") + public Object clone() { + LinkedCaseInsensitiveMap copy = (LinkedCaseInsensitiveMap) super.clone(); + copy.caseInsensitiveKeys = new HashMap(this.caseInsensitiveKeys); + return copy; + } + /** * Convert the given key to a case-insensitive key. diff --git a/spring-core/src/main/java/org/springframework/util/MimeType.java b/spring-core/src/main/java/org/springframework/util/MimeType.java index b3e585fb..e73602d2 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeType.java +++ b/spring-core/src/main/java/org/springframework/util/MimeType.java @@ -22,6 +22,7 @@ import java.util.BitSet; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -50,19 +51,12 @@ public class MimeType implements Comparable, Serializable { private static final long serialVersionUID = 4085923477777865903L; - protected static final String WILDCARD_TYPE = "*"; - private static final BitSet TOKEN; + protected static final String WILDCARD_TYPE = "*"; private static final String PARAM_CHARSET = "charset"; - - private final String type; - - private final String subtype; - - private final Map parameters; - + private static final BitSet TOKEN; static { // variable names refer to RFC 2616, section 2.2 @@ -100,6 +94,13 @@ public class MimeType implements Comparable, Serializable { } + private final String type; + + private final String subtype; + + private final Map parameters; + + /** * Create a new {@code MimeType} for the given primary type. *

    The {@linkplain #getSubtype() subtype} is set to "*", @@ -126,11 +127,23 @@ public class MimeType implements Comparable, Serializable { * Create a new {@code MimeType} for the given type, subtype, and character set. * @param type the primary type * @param subtype the subtype - * @param charSet the character set + * @param charset the character set + * @throws IllegalArgumentException if any of the parameters contains illegal characters + */ + public MimeType(String type, String subtype, Charset charset) { + this(type, subtype, Collections.singletonMap(PARAM_CHARSET, charset.name())); + } + + /** + * Copy-constructor that copies the type, subtype, parameters of the given {@code MimeType}, + * and allows to set the specified character set. + * @param other the other media type + * @param charset the character set * @throws IllegalArgumentException if any of the parameters contains illegal characters + * @since 4.3 */ - public MimeType(String type, String subtype, Charset charSet) { - this(type, subtype, Collections.singletonMap(PARAM_CHARSET, charSet.name())); + public MimeType(MimeType other, Charset charset) { + this(other.getType(), other.getSubtype(), addCharsetParameter(charset, other.getParameters())); } /** @@ -261,12 +274,24 @@ public class MimeType implements Comparable, Serializable { /** * Return the character set, as indicated by a {@code charset} parameter, if any. * @return the character set, or {@code null} if not available + * @since 4.3 */ - public Charset getCharSet() { + public Charset getCharset() { String charSet = getParameter(PARAM_CHARSET); return (charSet != null ? Charset.forName(unquote(charSet)) : null); } + /** + * Return the character set, as indicated by a {@code charset} parameter, if any. + * @return the character set, or {@code null} if not available + * @deprecated as of Spring 4.3, in favor of {@link #getCharset()} with its name + * aligned with the Java return type name + */ + @Deprecated + public Charset getCharSet() { + return getCharset(); + } + /** * Return a generic parameter value, given a parameter name. * @param name the parameter name @@ -374,50 +399,6 @@ public class MimeType implements Comparable, Serializable { return false; } - /** - * Compares this {@code MediaType} to another alphabetically. - * @param other media type to compare to - * @see MimeTypeUtils#sortBySpecificity(List) - */ - @Override - public int compareTo(MimeType other) { - int comp = getType().compareToIgnoreCase(other.getType()); - if (comp != 0) { - return comp; - } - comp = getSubtype().compareToIgnoreCase(other.getSubtype()); - if (comp != 0) { - return comp; - } - comp = getParameters().size() - other.getParameters().size(); - if (comp != 0) { - return comp; - } - TreeSet thisAttributes = new TreeSet(String.CASE_INSENSITIVE_ORDER); - thisAttributes.addAll(getParameters().keySet()); - TreeSet otherAttributes = new TreeSet(String.CASE_INSENSITIVE_ORDER); - otherAttributes.addAll(other.getParameters().keySet()); - Iterator thisAttributesIterator = thisAttributes.iterator(); - Iterator otherAttributesIterator = otherAttributes.iterator(); - while (thisAttributesIterator.hasNext()) { - String thisAttribute = thisAttributesIterator.next(); - String otherAttribute = otherAttributesIterator.next(); - comp = thisAttribute.compareToIgnoreCase(otherAttribute); - if (comp != 0) { - return comp; - } - String thisValue = getParameters().get(thisAttribute); - String otherValue = other.getParameters().get(otherAttribute); - if (otherValue == null) { - otherValue = ""; - } - comp = thisValue.compareTo(otherValue); - if (comp != 0) { - return comp; - } - } - return 0; - } @Override public boolean equals(Object other) { @@ -439,22 +420,22 @@ public class MimeType implements Comparable, Serializable { * for {@link Charset}s. * @since 4.2 */ - private boolean parametersAreEqual(MimeType that) { - if (this.parameters.size() != that.parameters.size()) { + private boolean parametersAreEqual(MimeType other) { + if (this.parameters.size() != other.parameters.size()) { return false; } for (String key : this.parameters.keySet()) { - if (!that.parameters.containsKey(key)) { + if (!other.parameters.containsKey(key)) { return false; } if (PARAM_CHARSET.equals(key)) { - if (!ObjectUtils.nullSafeEquals(this.getCharSet(), that.getCharSet())) { + if (!ObjectUtils.nullSafeEquals(getCharset(), other.getCharset())) { return false; } } - else if (!ObjectUtils.nullSafeEquals(this.parameters.get(key), that.parameters.get(key))) { + else if (!ObjectUtils.nullSafeEquals(this.parameters.get(key), other.parameters.get(key))) { return false; } } @@ -493,6 +474,52 @@ public class MimeType implements Comparable, Serializable { } } + /** + * Compares this {@code MediaType} to another alphabetically. + * @param other media type to compare to + * @see MimeTypeUtils#sortBySpecificity(List) + */ + @Override + public int compareTo(MimeType other) { + int comp = getType().compareToIgnoreCase(other.getType()); + if (comp != 0) { + return comp; + } + comp = getSubtype().compareToIgnoreCase(other.getSubtype()); + if (comp != 0) { + return comp; + } + comp = getParameters().size() - other.getParameters().size(); + if (comp != 0) { + return comp; + } + TreeSet thisAttributes = new TreeSet(String.CASE_INSENSITIVE_ORDER); + thisAttributes.addAll(getParameters().keySet()); + TreeSet otherAttributes = new TreeSet(String.CASE_INSENSITIVE_ORDER); + otherAttributes.addAll(other.getParameters().keySet()); + Iterator thisAttributesIterator = thisAttributes.iterator(); + Iterator otherAttributesIterator = otherAttributes.iterator(); + while (thisAttributesIterator.hasNext()) { + String thisAttribute = thisAttributesIterator.next(); + String otherAttribute = otherAttributesIterator.next(); + comp = thisAttribute.compareToIgnoreCase(otherAttribute); + if (comp != 0) { + return comp; + } + String thisValue = getParameters().get(thisAttribute); + String otherValue = other.getParameters().get(otherAttribute); + if (otherValue == null) { + otherValue = ""; + } + comp = thisValue.compareTo(otherValue); + if (comp != 0) { + return comp; + } + } + return 0; + } + + /** * Parse the given String value into a {@code MimeType} object, * with this method name following the 'valueOf' naming convention @@ -503,6 +530,12 @@ public class MimeType implements Comparable, Serializable { return MimeTypeUtils.parseMimeType(value); } + private static Map addCharsetParameter(Charset charset, Map parameters) { + Map map = new LinkedHashMap(parameters); + map.put(PARAM_CHARSET, charset.name()); + return map; + } + public static class SpecificityComparator implements Comparator { diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index 0bf62d58..a0ad5cc0 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -220,6 +220,9 @@ public abstract class MimeTypeUtils { throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty"); } String[] parts = StringUtils.tokenizeToStringArray(mimeType, ";"); + if (parts.length == 0) { + throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty"); + } String fullType = parts[0].trim(); // java.net.HttpURLConnection returns a *; q=.2 Accept header diff --git a/spring-core/src/main/java/org/springframework/util/NumberUtils.java b/spring-core/src/main/java/org/springframework/util/NumberUtils.java index 9df023c3..46b3f868 100644 --- a/spring-core/src/main/java/org/springframework/util/NumberUtils.java +++ b/spring-core/src/main/java/org/springframework/util/NumberUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,39 +87,29 @@ public abstract class NumberUtils { return (T) number; } else if (Byte.class == targetClass) { - long value = number.longValue(); + long value = checkedLongValue(number, targetClass); if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { raiseOverflowException(number, targetClass); } return (T) Byte.valueOf(number.byteValue()); } else if (Short.class == targetClass) { - long value = number.longValue(); + long value = checkedLongValue(number, targetClass); if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { raiseOverflowException(number, targetClass); } return (T) Short.valueOf(number.shortValue()); } else if (Integer.class == targetClass) { - long value = number.longValue(); + long value = checkedLongValue(number, targetClass); if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) { raiseOverflowException(number, targetClass); } return (T) Integer.valueOf(number.intValue()); } else if (Long.class == targetClass) { - BigInteger bigInt = null; - if (number instanceof BigInteger) { - bigInt = (BigInteger) number; - } - else if (number instanceof BigDecimal) { - bigInt = ((BigDecimal) number).toBigInteger(); - } - // Effectively analogous to JDK 8's BigInteger.longValueExact() - if (bigInt != null && (bigInt.compareTo(LONG_MIN) < 0 || bigInt.compareTo(LONG_MAX) > 0)) { - raiseOverflowException(number, targetClass); - } - return (T) Long.valueOf(number.longValue()); + long value = checkedLongValue(number, targetClass); + return (T) Long.valueOf(value); } else if (BigInteger.class == targetClass) { if (number instanceof BigDecimal) { @@ -148,11 +138,35 @@ public abstract class NumberUtils { } } + /** + * Check for a {@code BigInteger}/{@code BigDecimal} long overflow + * before returning the given number as a long value. + * @param number the number to convert + * @param targetClass the target class to convert to + * @return the long value, if convertible without overflow + * @throws IllegalArgumentException if there is an overflow + * @see #raiseOverflowException + */ + private static long checkedLongValue(Number number, Class targetClass) { + BigInteger bigInt = null; + if (number instanceof BigInteger) { + bigInt = (BigInteger) number; + } + else if (number instanceof BigDecimal) { + bigInt = ((BigDecimal) number).toBigInteger(); + } + // Effectively analogous to JDK 8's BigInteger.longValueExact() + if (bigInt != null && (bigInt.compareTo(LONG_MIN) < 0 || bigInt.compareTo(LONG_MAX) > 0)) { + raiseOverflowException(number, targetClass); + } + return number.longValue(); + } + /** * Raise an overflow exception for the given number and target class. * @param number the number we tried to convert * @param targetClass the target class we tried to convert to - * @throws IllegalArgumentException + * @throws IllegalArgumentException if there is an overflow */ private static void raiseOverflowException(Number number, Class targetClass) { throw new IllegalArgumentException("Could not convert number [" + number + "] of type [" + diff --git a/spring-core/src/main/java/org/springframework/util/ResizableByteArrayOutputStream.java b/spring-core/src/main/java/org/springframework/util/ResizableByteArrayOutputStream.java index cb2cdfa2..c4487ccf 100644 --- a/spring-core/src/main/java/org/springframework/util/ResizableByteArrayOutputStream.java +++ b/spring-core/src/main/java/org/springframework/util/ResizableByteArrayOutputStream.java @@ -23,7 +23,7 @@ import java.io.ByteArrayOutputStream; *

      *
    • has public {@link org.springframework.util.ResizableByteArrayOutputStream#grow(int)} * and {@link org.springframework.util.ResizableByteArrayOutputStream#resize(int)} methods - * to get more control over the the size of the internal buffer
    • + * to get more control over the size of the internal buffer *
    • has a higher initial capacity (256) by default
    • *
    * diff --git a/spring-core/src/main/java/org/springframework/util/ResourceUtils.java b/spring-core/src/main/java/org/springframework/util/ResourceUtils.java index 3ea92707..9108053f 100644 --- a/spring-core/src/main/java/org/springframework/util/ResourceUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ResourceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -235,6 +235,7 @@ public abstract class ResourceUtils { * @return a corresponding File object * @throws FileNotFoundException if the URL cannot be resolved to * a file in the file system + * @since 2.5 */ public static File getFile(URI resourceUri) throws FileNotFoundException { return getFile(resourceUri, "URI"); @@ -249,6 +250,7 @@ public abstract class ResourceUtils { * @return a corresponding File object * @throws FileNotFoundException if the URL cannot be resolved to * a file in the file system + * @since 2.5 */ public static File getFile(URI resourceUri, String description) throws FileNotFoundException { Assert.notNull(resourceUri, "Resource URI must not be null"); diff --git a/spring-core/src/main/java/org/springframework/util/StopWatch.java b/spring-core/src/main/java/org/springframework/util/StopWatch.java index 1ba39880..b4701164 100644 --- a/spring-core/src/main/java/org/springframework/util/StopWatch.java +++ b/spring-core/src/main/java/org/springframework/util/StopWatch.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -134,7 +134,7 @@ public class StopWatch { /** * Stop the current task. The results are undefined if timing * methods are called without invoking at least one pair - * {@code #start()} / {@code #stop()} methods. + * {@code start()} / {@code stop()} methods. * @see #start() */ public void stop() throws IllegalStateException { diff --git a/spring-core/src/main/java/org/springframework/util/StreamUtils.java b/spring-core/src/main/java/org/springframework/util/StreamUtils.java index 1bc2170c..c04ce058 100644 --- a/spring-core/src/main/java/org/springframework/util/StreamUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StreamUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ import java.nio.charset.Charset; * * @author Juergen Hoeller * @author Phillip Webb + * @author Brian Clozel * @since 3.2.2 * @see FileCopyUtils */ @@ -131,6 +132,62 @@ public abstract class StreamUtils { return byteCount; } + /** + * Copy a range of content of the given InputStream to the given OutputStream. + *

    If the specified range exceeds the length of the InputStream, this copies + * up to the end of the stream and returns the actual number of copied bytes. + *

    Leaves both streams open when done. + * @param in the InputStream to copy from + * @param out the OutputStream to copy to + * @param start the position to start copying from + * @param end the position to end copying + * @return the number of bytes copied + * @throws IOException in case of I/O errors + * @since 4.3 + */ + public static long copyRange(InputStream in, OutputStream out, long start, long end) throws IOException { + long skipped = in.skip(start); + if (skipped < start) { + throw new IOException("Skipped only " + skipped + " bytes out of " + start + " required."); + } + long bytesToCopy = end - start + 1; + byte buffer[] = new byte[StreamUtils.BUFFER_SIZE]; + while (bytesToCopy > 0) { + int bytesRead = in.read(buffer); + if (bytesRead == -1) { + break; + } + else if (bytesRead <= bytesToCopy) { + out.write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + } + else { + out.write(buffer, 0, (int) bytesToCopy); + bytesToCopy = 0; + } + } + return end - start + 1 - bytesToCopy; + } + + /** + * Drain the remaining content of the given InputStream. + * Leaves the InputStream open when done. + * @param in the InputStream to drain + * @return the number of bytes read + * @throws IOException in case of I/O errors + * @since 4.3 + */ + public static int drain(InputStream in) throws IOException { + Assert.notNull(in, "No InputStream specified"); + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead = -1; + int byteCount = 0; + while ((bytesRead = in.read(buffer)) != -1) { + byteCount += bytesRead; + } + return byteCount; + } + /** * Return an efficient empty {@link InputStream}. * @return a {@link ByteArrayInputStream} based on an empty byte array diff --git a/spring-core/src/main/java/org/springframework/util/UpdateMessageDigestInputStream.java b/spring-core/src/main/java/org/springframework/util/UpdateMessageDigestInputStream.java index 37890be9..90bfa4a7 100644 --- a/spring-core/src/main/java/org/springframework/util/UpdateMessageDigestInputStream.java +++ b/spring-core/src/main/java/org/springframework/util/UpdateMessageDigestInputStream.java @@ -33,7 +33,7 @@ abstract class UpdateMessageDigestInputStream extends InputStream { * Update the message digest with the rest of the bytes in this stream. *

    Using this method is more optimized since it avoids creating new * byte arrays for each call. - * @param messageDigest The message digest to update + * @param messageDigest the message digest to update * @throws IOException when propagated from {@link #read()} */ public void updateMessageDigest(MessageDigest messageDigest) throws IOException { @@ -47,7 +47,7 @@ abstract class UpdateMessageDigestInputStream extends InputStream { * Update the message digest with the next len bytes in this stream. *

    Using this method is more optimized since it avoids creating new * byte arrays for each call. - * @param messageDigest The message digest to update + * @param messageDigest the message digest to update * @param len how many bytes to read from this stream and use to update the message digest * @throws IOException when propagated from {@link #read()} */ diff --git a/spring-core/src/main/java/org/springframework/util/backoff/BackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/BackOff.java index 0543ac91..2eb4b3ce 100644 --- a/spring-core/src/main/java/org/springframework/util/backoff/BackOff.java +++ b/spring-core/src/main/java/org/springframework/util/backoff/BackOff.java @@ -26,7 +26,7 @@ package org.springframework.util.backoff; * BackOffExecution exec = backOff.start(); * * // In the operation recovery/retry loop: - * long waitInterval = exec.nextBackOffMillis(); + * long waitInterval = exec.nextBackOff(); * if (waitInterval == BackOffExecution.STOP) { * // do not retry operation * } diff --git a/spring-core/src/main/java/org/springframework/util/comparator/CompoundComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/CompoundComparator.java index a1051404..c04735c8 100644 --- a/spring-core/src/main/java/org/springframework/util/comparator/CompoundComparator.java +++ b/spring-core/src/main/java/org/springframework/util/comparator/CompoundComparator.java @@ -24,7 +24,7 @@ import java.util.List; import org.springframework.util.Assert; /** - * A comparator that chains a sequence of one or more more Comparators. + * A comparator that chains a sequence of one or more Comparators. * *

    A compound comparator calls each Comparator in sequence until a single * Comparator returns a non-zero result, or the comparators are exhausted and diff --git a/spring-core/src/main/java/org/springframework/util/comparator/InstanceComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/InstanceComparator.java index 19d9d9bf..8f033bad 100644 --- a/spring-core/src/main/java/org/springframework/util/comparator/InstanceComparator.java +++ b/spring-core/src/main/java/org/springframework/util/comparator/InstanceComparator.java @@ -43,7 +43,7 @@ public class InstanceComparator implements Comparator { /** * Create a new {@link InstanceComparator} instance. * @param instanceOrder the ordered list of classes that should be used when comparing - * objects. Classes earlier in the list will be be given a higher priority. + * objects. Classes earlier in the list will be given a higher priority. */ public InstanceComparator(Class... instanceOrder) { Assert.notNull(instanceOrder, "'instanceOrder' must not be null"); diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java index f42a88f4..1a6cd14f 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import java.util.function.BiFunction; import org.springframework.lang.UsesJava8; - /** * Adapts a {@link CompletableFuture} into a {@link ListenableFuture}. * @@ -38,6 +37,7 @@ public class CompletableToListenableFutureAdapter implements ListenableFuture private final ListenableFutureCallbackRegistry callbacks = new ListenableFutureCallbackRegistry(); + public CompletableToListenableFutureAdapter(CompletableFuture completableFuture) { this.completableFuture = completableFuture; this.completableFuture.handle(new BiFunction() { @@ -54,6 +54,7 @@ public class CompletableToListenableFutureAdapter implements ListenableFuture }); } + @Override public void addCallback(ListenableFutureCallback callback) { this.callbacks.addCallback(callback); diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java b/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java index caac2874..20f21420 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,7 @@ package org.springframework.util.concurrent; /** - * Defines the contract for failure callbacks that accept the result of a - * {@link ListenableFuture}. + * Failure callback for a {@link ListenableFuture}. * * @author Sebastien Deleuze * @since 4.1 @@ -26,8 +25,9 @@ package org.springframework.util.concurrent; public interface FailureCallback { /** - * Called when the {@link ListenableFuture} fails to complete. - * @param ex the exception that triggered the failure + * Called when the {@link ListenableFuture} completes with failure. + *

    Note that Exceptions raised by this method are ignored. + * @param ex the failure */ void onFailure(Throwable ex); diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java index 37aac931..2a36c9e6 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,10 +24,9 @@ import java.util.concurrent.TimeoutException; import org.springframework.util.Assert; /** - * Abstract class that adapts a {@link Future} parameterized over S into a {@code - * Future} parameterized over T. All methods are delegated to the adaptee, where {@link - * #get()} and {@link #get(long, TimeUnit)} call {@link #adapt(Object)} on the adaptee's - * result. + * Abstract class that adapts a {@link Future} parameterized over S into a {@code Future} + * parameterized over T. All methods are delegated to the adaptee, where {@link #get()} + * and {@link #get(long, TimeUnit)} call {@link #adapt(Object)} on the adaptee's result. * * @author Arjen Poutsma * @since 4.0 diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java index 6250b08f..5b842973 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,11 @@ package org.springframework.util.concurrent; import java.util.concurrent.Future; /** - * Extends the {@link Future} interface with the capability to accept completion - * callbacks. If the future has already completed when the callback is added, the - * callback will be triggered immediately. + * Extend {@link Future} with the capability to accept completion callbacks. + * If the future has completed when the callback is added, the callback is + * triggered immediately. *

    Inspired by {@code com.google.common.util.concurrent.ListenableFuture}. - + * * @author Arjen Poutsma * @author Sebastien Deleuze * @since 4.0 @@ -31,20 +31,15 @@ import java.util.concurrent.Future; public interface ListenableFuture extends Future { /** - * Registers the given callback to this {@code ListenableFuture}. The callback will - * be triggered when this {@code Future} is complete or, if it is already complete, - * immediately. + * Register the given {@code ListenableFutureCallback}. * @param callback the callback to register */ void addCallback(ListenableFutureCallback callback); /** - * Registers the given success and failure callbacks to this {@code ListenableFuture}. - * The callback will be triggered when this {@code Future} is complete or, if it is - * already complete immediately. This is a Java 8 lambdas compliant alternative to - * {@link #addCallback(ListenableFutureCallback)}. - * @param successCallback the success callback to register - * @param failureCallback the failure callback to register + * Java 8 lambda-friendly alternative with success and failure callbacks. + * @param successCallback the success callback + * @param failureCallback the failure callback * @since 4.1 */ void addCallback(SuccessCallback successCallback, FailureCallback failureCallback); diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java index 268a6496..0f1624aa 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,16 +52,20 @@ public abstract class ListenableFutureAdapter extends FutureAdapter listenableAdaptee.addCallback(new ListenableFutureCallback() { @Override public void onSuccess(S result) { + T adapted; try { - successCallback.onSuccess(adaptInternal(result)); + adapted = adaptInternal(result); } catch (ExecutionException ex) { Throwable cause = ex.getCause(); onFailure(cause != null ? cause : ex); + return; } catch (Throwable ex) { onFailure(ex); + return; } + successCallback.onSuccess(adapted); } @Override public void onFailure(Throwable ex) { diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java index 63379445..4ce858c1 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ package org.springframework.util.concurrent; /** - * Defines the contract for callbacks that accept the result of a + * Callback mechanism for the outcome, success or failure, from a * {@link ListenableFuture}. * * @author Arjen Poutsma diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java index 1c4be730..46eb746b 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,12 +22,14 @@ import java.util.Queue; import org.springframework.util.Assert; /** - * Registry for {@link ListenableFutureCallback} instances. + * Helper class for {@link ListenableFuture} implementations that maintains a + * of success and failure callbacks and helps to notify them. * *

    Inspired by {@code com.google.common.util.concurrent.ExecutionList}. * * @author Arjen Poutsma * @author Sebastien Deleuze + * @author Rossen Stoyanchev * @since 4.0 */ public class ListenableFutureCallbackRegistry { @@ -47,7 +49,6 @@ public class ListenableFutureCallbackRegistry { * Add the given callback to this registry. * @param callback the callback to add */ - @SuppressWarnings("unchecked") public void addCallback(ListenableFutureCallback callback) { Assert.notNull(callback, "'callback' must not be null"); synchronized (this.mutex) { @@ -57,15 +58,34 @@ public class ListenableFutureCallbackRegistry { this.failureCallbacks.add(callback); break; case SUCCESS: - callback.onSuccess((T) this.result); + notifySuccess(callback); break; case FAILURE: - callback.onFailure((Throwable) this.result); + notifyFailure(callback); break; } } } + @SuppressWarnings("unchecked") + private void notifySuccess(SuccessCallback callback) { + try { + callback.onSuccess((T) this.result); + } + catch (Throwable ex) { + // Ignore + } + } + + private void notifyFailure(FailureCallback callback) { + try { + callback.onFailure((Throwable) this.result); + } + catch (Throwable ex) { + // Ignore + } + } + /** * Add the given success callback to this registry. * @param callback the success callback to add @@ -80,7 +100,7 @@ public class ListenableFutureCallbackRegistry { this.successCallbacks.add(callback); break; case SUCCESS: - callback.onSuccess((T) this.result); + notifySuccess(callback); break; } } @@ -99,7 +119,7 @@ public class ListenableFutureCallbackRegistry { this.failureCallbacks.add(callback); break; case FAILURE: - callback.onFailure((Throwable) this.result); + notifyFailure(callback); break; } } @@ -115,7 +135,7 @@ public class ListenableFutureCallbackRegistry { this.state = State.SUCCESS; this.result = result; while (!this.successCallbacks.isEmpty()) { - this.successCallbacks.poll().onSuccess(result); + notifySuccess(this.successCallbacks.poll()); } } } @@ -130,7 +150,7 @@ public class ListenableFutureCallbackRegistry { this.state = State.FAILURE; this.result = ex; while (!this.failureCallbacks.isEmpty()) { - this.failureCallbacks.poll().onFailure(ex); + notifyFailure(this.failureCallbacks.poll()); } } } diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java b/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java index 65f30411..b20bcad1 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,7 @@ package org.springframework.util.concurrent; /** - * Defines the contract for success callbacks that accept the result of a - * {@link ListenableFuture}. + * Success callback for a {@link ListenableFuture}. * * @author Sebastien Deleuze * @since 4.1 @@ -26,7 +25,8 @@ package org.springframework.util.concurrent; public interface SuccessCallback { /** - * Called when the {@link ListenableFuture} successfully completes. + * Called when the {@link ListenableFuture} completes with success. + *

    Note that Exceptions raised by this method are ignored. * @param result the result */ void onSuccess(T result); diff --git a/spring-core/src/main/java/org/springframework/util/xml/XmlValidationModeDetector.java b/spring-core/src/main/java/org/springframework/util/xml/XmlValidationModeDetector.java index d924fd15..227b216c 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/XmlValidationModeDetector.java +++ b/spring-core/src/main/java/org/springframework/util/xml/XmlValidationModeDetector.java @@ -120,7 +120,7 @@ public class XmlValidationModeDetector { /** - * Does the content contain the the DTD DOCTYPE declaration? + * Does the content contain the DTD DOCTYPE declaration? */ private boolean hasDoctype(String content) { return content.contains(DOCTYPE); -- cgit v1.2.3