diff options
author | Emmanuel Bourg <ebourg@apache.org> | 2014-12-03 14:31:16 +0100 |
---|---|---|
committer | Emmanuel Bourg <ebourg@apache.org> | 2014-12-03 14:31:16 +0100 |
commit | c56370beb0a2bfa263e125fce107dceccee89fd3 (patch) | |
tree | 7ee611ceb0acbbdf7f83abcd72adb854b7d77225 /spring-core/src/main/java | |
parent | aa5221b73661fa728dc4e62e1230e9104528c4eb (diff) |
Imported Upstream version 3.2.12
Diffstat (limited to 'spring-core/src/main/java')
295 files changed, 40721 insertions, 0 deletions
diff --git a/spring-core/src/main/java/org/springframework/asm/SpringAsmInfo.java b/spring-core/src/main/java/org/springframework/asm/SpringAsmInfo.java new file mode 100644 index 00000000..409f00c8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/SpringAsmInfo.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2014 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.asm; + +/** + * Utility class exposing constants related to Spring's internal repackaging + * of the ASM bytecode manipulation library (currently based on version 5.0). + * + * <p>See <a href="package-summary.html">package-level javadocs</a> for more + * information on {@code org.springframework.asm}. + * + * @author Chris Beams + * @since 3.2 + */ +public final class SpringAsmInfo { + + /** + * The ASM compatibility version for Spring's ASM visitor implementations: + * currently {@link Opcodes#ASM5}. + */ + public static final int ASM_VERSION = Opcodes.ASM5; + +} diff --git a/spring-core/src/main/java/org/springframework/asm/package-info.java b/spring-core/src/main/java/org/springframework/asm/package-info.java new file mode 100644 index 00000000..9ced53ea --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/package-info.java @@ -0,0 +1,18 @@ + +/** + * Spring's repackaging of + * <a href="http://asm.ow2.org">org.objectweb.asm 5.0</a> + * (for internal use only). + * + * <p>This repackaging technique avoids any potential conflicts with + * dependencies on ASM at the application level or from third-party + * libraries and frameworks. + * + * <p>As this repackaging happens at the class file level, sources + * and javadocs are not available here. See the original ObjectWeb + * <a href="http://asm.ow2.org/asm50/javadoc/user">ASM 5.0 javadocs</a> + * for details when working with these classes. + * + * @since 3.2 + */ +package org.springframework.asm; diff --git a/spring-core/src/main/java/org/springframework/asm/util/TraceClassVisitor.java b/spring-core/src/main/java/org/springframework/asm/util/TraceClassVisitor.java new file mode 100644 index 00000000..bf5900fb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/util/TraceClassVisitor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2012 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.asm.util; + +import java.io.PrintWriter; + +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.SpringAsmInfo; + +/** + * Dummy implementation of missing TraceClassVisitor from cglib-nodep's internally + * repackaged ASM library, added to avoid NoClassDefFoundErrors. + * + * @author Chris Beams + * @since 3.2 + */ +public class TraceClassVisitor extends ClassVisitor { + + public TraceClassVisitor(Object object, PrintWriter pw) { + super(SpringAsmInfo.ASM_VERSION); + } + +} diff --git a/spring-core/src/main/java/org/springframework/asm/util/package-info.java b/spring-core/src/main/java/org/springframework/asm/util/package-info.java new file mode 100644 index 00000000..f03a2e8d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/util/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2002-2012 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. + */ + +/** + * Dummy implementations of asm-util classes (for internal use only). + * + * @since 3.2 + */ +package org.springframework.asm.util; diff --git a/spring-core/src/main/java/org/springframework/cglib/SpringCglibInfo.java b/spring-core/src/main/java/org/springframework/cglib/SpringCglibInfo.java new file mode 100644 index 00000000..0ed64ca5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/SpringCglibInfo.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2012 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.cglib; + +/** + * Empty class used to ensure that the {@code org.springframework.cglib} package is + * processed during Javadoc generation. + * + * <p>See <a href="package-summary.html">package-level Javadoc</a> for more + * information on {@code org.springframework.cglib}. + * + * @author Chris Beams + * @since 3.2 + */ +public final class SpringCglibInfo { + +} diff --git a/spring-core/src/main/java/org/springframework/cglib/core/SpringNamingPolicy.java b/spring-core/src/main/java/org/springframework/cglib/core/SpringNamingPolicy.java new file mode 100644 index 00000000..839f36f8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/core/SpringNamingPolicy.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2014 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.cglib.core; + +/** + * Custom extension of CGLIB's {@link DefaultNamingPolicy}, modifying + * the tag in generated class names from "ByCGLIB" to "BySpringCGLIB". + * + * <p>This is primarily designed to avoid clashes between a regular CGLIB + * version (used by some other library) and Spring's embedded variant, + * in case the same class happens to get proxied for different purposes. + * + * @author Juergen Hoeller + * @since 3.2.8 + */ +public class SpringNamingPolicy extends DefaultNamingPolicy { + + public static final SpringNamingPolicy INSTANCE = new SpringNamingPolicy(); + + @Override + protected String getTag() { + return "BySpringCGLIB"; + } + +} diff --git a/spring-core/src/main/java/org/springframework/cglib/package-info.java b/spring-core/src/main/java/org/springframework/cglib/package-info.java new file mode 100644 index 00000000..a7f25ce2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2012 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. + */ + +/** + * Spring's repackaging of <a href="http://cglib.sourceforge.net">net.sf.cglib 3</a> (for + * internal use only). + * <p>This repackaging technique avoids any potential conflicts with + * dependencies on CGLIB at the application level or from other third-party + * libraries and frameworks. + * <p>As this repackaging happens at the classfile level, sources and Javadoc + * are not available here. See the original + * <a href="http://cglib.sourceforge.net/apidocs">CGLIB 3 Javadoc</a> + * for details when working with these classes. + * + * @since 3.2 + */ +package org.springframework.cglib; diff --git a/spring-core/src/main/java/org/springframework/cglib/transform/impl/MemorySafeUndeclaredThrowableStrategy.java b/spring-core/src/main/java/org/springframework/cglib/transform/impl/MemorySafeUndeclaredThrowableStrategy.java new file mode 100644 index 00000000..28f92352 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/transform/impl/MemorySafeUndeclaredThrowableStrategy.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2014 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.cglib.transform.impl; + +import org.springframework.cglib.core.ClassGenerator; +import org.springframework.cglib.core.DefaultGeneratorStrategy; +import org.springframework.cglib.core.TypeUtils; +import org.springframework.cglib.transform.ClassTransformer; +import org.springframework.cglib.transform.MethodFilter; +import org.springframework.cglib.transform.MethodFilterTransformer; +import org.springframework.cglib.transform.TransformingClassGenerator; + +/** + * Memory-safe variant of {@link UndeclaredThrowableStrategy} ported from CGLIB 3.1, + * introduced for using it in Spring before it was officially released in CGLIB. + * + * @author Phillip Webb + * @since 3.2.4 + */ +public class MemorySafeUndeclaredThrowableStrategy extends DefaultGeneratorStrategy { + + private static final MethodFilter TRANSFORM_FILTER = new MethodFilter() { + public boolean accept(int access, String name, String desc, String signature, String[] exceptions) { + return (!TypeUtils.isPrivate(access) && name.indexOf('$') < 0); + } + }; + + + private final Class<?> wrapper; + + + public MemorySafeUndeclaredThrowableStrategy(Class<?> wrapper) { + this.wrapper = wrapper; + } + + + protected ClassGenerator transform(ClassGenerator cg) throws Exception { + ClassTransformer ct = new UndeclaredThrowableTransformer(this.wrapper); + ct = new MethodFilterTransformer(TRANSFORM_FILTER, ct); + return new TransformingClassGenerator(cg, ct); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/AliasRegistry.java b/spring-core/src/main/java/org/springframework/core/AliasRegistry.java new file mode 100644 index 00000000..0e1e18bd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/AliasRegistry.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2008 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; + +/** + * Common interface for managing aliases. Serves as super-interface for + * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry}. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +public interface AliasRegistry { + + /** + * Given a name, register an alias for it. + * @param name the canonical name + * @param alias the alias to be registered + * @throws IllegalStateException if the alias is already in use + * and may not be overridden + */ + void registerAlias(String name, String alias); + + /** + * Remove the specified alias from this registry. + * @param alias the alias to remove + * @throws IllegalStateException if no such alias was found + */ + void removeAlias(String alias); + + /** + * Determine whether this given name is defines as an alias + * (as opposed to the name of an actually registered component). + * @param beanName the bean name to check + * @return whether the given name is an alias + */ + boolean isAlias(String beanName); + + /** + * Return the aliases for the given name, if defined. + * @param name the name to check for aliases + * @return the aliases, or an empty array if none + */ + String[] getAliases(String name); + +} diff --git a/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java b/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java new file mode 100644 index 00000000..27130bce --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2012 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 defining a generic contract for attaching and accessing metadata + * to/from arbitrary objects. + * + * @author Rob Harrop + * @since 2.0 + */ +public interface AttributeAccessor { + + /** + * Set the attribute defined by {@code name} to the supplied {@code value}. + * If {@code value} is {@code null}, the attribute is {@link #removeAttribute removed}. + * <p>In general, users should take care to prevent overlaps with other + * metadata attributes by using fully-qualified names, perhaps using + * class or package names as prefix. + * @param name the unique attribute key + * @param value the attribute value to be attached + */ + void setAttribute(String name, Object value); + + /** + * Get the value of the attribute identified by {@code name}. + * Return {@code null} if the attribute doesn't exist. + * @param name the unique attribute key + * @return the current value of the attribute, if any + */ + Object getAttribute(String name); + + /** + * Remove the attribute identified by {@code name} and return its value. + * Return {@code null} if no attribute under {@code name} is found. + * @param name the unique attribute key + * @return the last value of the attribute, if any + */ + Object removeAttribute(String name); + + /** + * Return {@code true} if the attribute identified by {@code name} exists. + * Otherwise return {@code false}. + * @param name the unique attribute key + */ + boolean hasAttribute(String name); + + /** + * Return the names of all attributes. + */ + String[] attributeNames(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java b/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java new file mode 100644 index 00000000..cf351f42 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2012 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.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Support class for {@link AttributeAccessor AttributeAccessors}, providing + * a base implementation of all methods. To be extended by subclasses. + * + * <p>{@link Serializable} if subclasses and all attribute values are {@link Serializable}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public abstract class AttributeAccessorSupport implements AttributeAccessor, Serializable { + + /** Map with String keys and Object values */ + private final Map<String, Object> attributes = new LinkedHashMap<String, Object>(0); + + + public void setAttribute(String name, Object value) { + Assert.notNull(name, "Name must not be null"); + if (value != null) { + this.attributes.put(name, value); + } + else { + removeAttribute(name); + } + } + + public Object getAttribute(String name) { + Assert.notNull(name, "Name must not be null"); + return this.attributes.get(name); + } + + public Object removeAttribute(String name) { + Assert.notNull(name, "Name must not be null"); + return this.attributes.remove(name); + } + + public boolean hasAttribute(String name) { + Assert.notNull(name, "Name must not be null"); + return this.attributes.containsKey(name); + } + + public String[] attributeNames() { + return this.attributes.keySet().toArray(new String[this.attributes.size()]); + } + + + /** + * Copy the attributes from the supplied AttributeAccessor to this accessor. + * @param source the AttributeAccessor to copy from + */ + protected void copyAttributesFrom(AttributeAccessor source) { + Assert.notNull(source, "Source must not be null"); + String[] attributeNames = source.attributeNames(); + for (String attributeName : attributeNames) { + setAttribute(attributeName, source.getAttribute(attributeName)); + } + } + + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AttributeAccessorSupport)) { + return false; + } + AttributeAccessorSupport that = (AttributeAccessorSupport) other; + return this.attributes.equals(that.attributes); + } + + @Override + public int hashCode() { + return this.attributes.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java new file mode 100644 index 00000000..1027d4df --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java @@ -0,0 +1,229 @@ +/* + * Copyright 2002-2013 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.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Helper for resolving synthetic {@link Method#isBridge bridge Methods} to the + * {@link Method} being bridged. + * + * <p>Given a synthetic {@link Method#isBridge bridge Method} returns the {@link Method} + * being bridged. A bridge method may be created by the compiler when extending a + * parameterized type whose methods have parameterized arguments. During runtime + * invocation the bridge {@link Method} may be invoked and/or used via reflection. + * When attempting to locate annotations on {@link Method Methods}, it is wise to check + * for bridge {@link Method Methods} as appropriate and find the bridged {@link Method}. + * + * <p>See <a href="http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#15.12.4.5"> + * The Java Language Specification</a> for more details on the use of bridge methods. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class BridgeMethodResolver { + + /** + * Find the original method for the supplied {@link Method bridge Method}. + * <p>It is safe to call this method passing in a non-bridge {@link Method} instance. + * In such a case, the supplied {@link Method} instance is returned directly to the caller. + * Callers are <strong>not</strong> required to check for bridging before calling this method. + * @param bridgeMethod the method to introspect + * @return the original method (either the bridged method or the passed-in method + * if no more specific one could be found) + */ + public static Method findBridgedMethod(Method bridgeMethod) { + if (bridgeMethod == null || !bridgeMethod.isBridge()) { + return bridgeMethod; + } + // Gather all methods with matching name and parameter size. + List<Method> candidateMethods = new ArrayList<Method>(); + Method[] methods = ReflectionUtils.getAllDeclaredMethods(bridgeMethod.getDeclaringClass()); + for (Method candidateMethod : methods) { + if (isBridgedCandidateFor(candidateMethod, bridgeMethod)) { + candidateMethods.add(candidateMethod); + } + } + // Now perform simple quick check. + if (candidateMethods.size() == 1) { + return candidateMethods.get(0); + } + // Search for candidate match. + Method bridgedMethod = searchCandidates(candidateMethods, bridgeMethod); + if (bridgedMethod != null) { + // Bridged method found... + return bridgedMethod; + } + else { + // A bridge method was passed in but we couldn't find the bridged method. + // Let's proceed with the passed-in method and hope for the best... + return bridgeMethod; + } + } + + /** + * Searches for the bridged method in the given candidates. + * @param candidateMethods the List of candidate Methods + * @param bridgeMethod the bridge method + * @return the bridged method, or {@code null} if none found + */ + private static Method searchCandidates(List<Method> candidateMethods, Method bridgeMethod) { + if (candidateMethods.isEmpty()) { + return null; + } + Map<TypeVariable, Type> typeParameterMap = GenericTypeResolver.getTypeVariableMap(bridgeMethod.getDeclaringClass()); + Method previousMethod = null; + boolean sameSig = true; + for (Method candidateMethod : candidateMethods) { + if (isBridgeMethodFor(bridgeMethod, candidateMethod, typeParameterMap)) { + return candidateMethod; + } + else if (previousMethod != null) { + sameSig = sameSig && + Arrays.equals(candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes()); + } + previousMethod = candidateMethod; + } + return (sameSig ? candidateMethods.get(0) : null); + } + + /** + * Returns {@code true} if the supplied '{@code candidateMethod}' can be + * consider a validate candidate for the {@link Method} that is {@link Method#isBridge() bridged} + * by the supplied {@link Method bridge Method}. This method performs inexpensive + * checks and can be used quickly filter for a set of possible matches. + */ + private static boolean isBridgedCandidateFor(Method candidateMethod, Method bridgeMethod) { + return (!candidateMethod.isBridge() && !candidateMethod.equals(bridgeMethod) && + candidateMethod.getName().equals(bridgeMethod.getName()) && + candidateMethod.getParameterTypes().length == bridgeMethod.getParameterTypes().length); + } + + /** + * Determines whether or not the bridge {@link Method} is the bridge for the + * supplied candidate {@link Method}. + */ + static boolean isBridgeMethodFor(Method bridgeMethod, Method candidateMethod, Map<TypeVariable, Type> typeVariableMap) { + if (isResolvedTypeMatch(candidateMethod, bridgeMethod, typeVariableMap)) { + return true; + } + Method method = findGenericDeclaration(bridgeMethod); + return (method != null && isResolvedTypeMatch(method, candidateMethod, typeVariableMap)); + } + + /** + * Searches for the generic {@link Method} declaration whose erased signature + * matches that of the supplied bridge method. + * @throws IllegalStateException if the generic declaration cannot be found + */ + private static Method findGenericDeclaration(Method bridgeMethod) { + // Search parent types for method that has same signature as bridge. + Class superclass = bridgeMethod.getDeclaringClass().getSuperclass(); + while (superclass != null && !Object.class.equals(superclass)) { + Method method = searchForMatch(superclass, bridgeMethod); + if (method != null && !method.isBridge()) { + return method; + } + superclass = superclass.getSuperclass(); + } + + // Search interfaces. + Class[] interfaces = ClassUtils.getAllInterfacesForClass(bridgeMethod.getDeclaringClass()); + for (Class ifc : interfaces) { + Method method = searchForMatch(ifc, bridgeMethod); + if (method != null && !method.isBridge()) { + return method; + } + } + + return null; + } + + /** + * Returns {@code true} if the {@link Type} signature of both the supplied + * {@link Method#getGenericParameterTypes() generic Method} and concrete {@link Method} + * are equal after resolving all {@link TypeVariable TypeVariables} using the supplied + * TypeVariable Map, otherwise returns {@code false}. + */ + private static boolean isResolvedTypeMatch( + Method genericMethod, Method candidateMethod, Map<TypeVariable, Type> typeVariableMap) { + + Type[] genericParameters = genericMethod.getGenericParameterTypes(); + Class[] candidateParameters = candidateMethod.getParameterTypes(); + if (genericParameters.length != candidateParameters.length) { + return false; + } + for (int i = 0; i < genericParameters.length; i++) { + Type genericParameter = genericParameters[i]; + Class candidateParameter = candidateParameters[i]; + if (candidateParameter.isArray()) { + // An array type: compare the component type. + Type rawType = GenericTypeResolver.getRawType(genericParameter, typeVariableMap); + if (rawType instanceof GenericArrayType) { + if (!candidateParameter.getComponentType().equals( + GenericTypeResolver.resolveType(((GenericArrayType) rawType).getGenericComponentType(), typeVariableMap))) { + return false; + } + break; + } + } + // A non-array type: compare the type itself. + Class resolvedParameter = GenericTypeResolver.resolveType(genericParameter, typeVariableMap); + if (!candidateParameter.equals(resolvedParameter)) { + return false; + } + } + return true; + } + + /** + * If the supplied {@link Class} has a declared {@link Method} whose signature matches + * that of the supplied {@link Method}, then this matching {@link Method} is returned, + * otherwise {@code null} is returned. + */ + private static Method searchForMatch(Class type, Method bridgeMethod) { + return ReflectionUtils.findMethod(type, bridgeMethod.getName(), bridgeMethod.getParameterTypes()); + } + + /** + * Compare the signatures of the bridge method and the method which it bridges. If + * the parameter and return types are the same, it is a 'visibility' bridge method + * introduced in Java 6 to fix http://bugs.sun.com/view_bug.do?bug_id=6342411. + * See also http://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html + * @return whether signatures match as described + */ + public static boolean isVisibilityBridgeMethodPair(Method bridgeMethod, Method bridgedMethod) { + if (bridgeMethod == bridgedMethod) { + return true; + } + return Arrays.equals(bridgeMethod.getParameterTypes(), bridgedMethod.getParameterTypes()) && + bridgeMethod.getReturnType().equals(bridgedMethod.getReturnType()); + } + + +} diff --git a/spring-core/src/main/java/org/springframework/core/CollectionFactory.java b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java new file mode 100644 index 00000000..23335434 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java @@ -0,0 +1,346 @@ +/* + * Copyright 2002-2012 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.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.springframework.util.ClassUtils; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Factory for collections, being aware of Java 5 and Java 6 collections. + * Mainly for internal use within the framework. + * + * <p>The goal of this class is to avoid runtime dependencies on a specific + * Java version, while nevertheless using the best collection implementation + * that is available at runtime. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @since 1.1.1 + */ +public abstract class CollectionFactory { + + private static Class navigableSetClass = null; + + private static Class navigableMapClass = null; + + private static final Set<Class> approximableCollectionTypes = new HashSet<Class>(10); + + private static final Set<Class> approximableMapTypes = new HashSet<Class>(6); + + + static { + // Standard collection interfaces + approximableCollectionTypes.add(Collection.class); + approximableCollectionTypes.add(List.class); + approximableCollectionTypes.add(Set.class); + approximableCollectionTypes.add(SortedSet.class); + approximableMapTypes.add(Map.class); + approximableMapTypes.add(SortedMap.class); + + // New Java 6 collection interfaces + ClassLoader cl = CollectionFactory.class.getClassLoader(); + try { + navigableSetClass = ClassUtils.forName("java.util.NavigableSet", cl); + navigableMapClass = ClassUtils.forName("java.util.NavigableMap", cl); + approximableCollectionTypes.add(navigableSetClass); + approximableMapTypes.add(navigableMapClass); + } + catch (ClassNotFoundException ex) { + // not running on Java 6 or above... + } + + // Common concrete collection classes + approximableCollectionTypes.add(ArrayList.class); + approximableCollectionTypes.add(LinkedList.class); + approximableCollectionTypes.add(HashSet.class); + approximableCollectionTypes.add(LinkedHashSet.class); + approximableCollectionTypes.add(TreeSet.class); + approximableMapTypes.add(HashMap.class); + approximableMapTypes.add(LinkedHashMap.class); + approximableMapTypes.add(TreeMap.class); + } + + + /** + * Create a linked Set if possible: This implementation always + * creates a {@link java.util.LinkedHashSet}, since Spring 2.5 + * requires JDK 1.4 anyway. + * @param initialCapacity the initial capacity of the Set + * @return the new Set instance + * @deprecated as of Spring 2.5, for usage on JDK 1.4 or higher + */ + @Deprecated + public static <T> Set<T> createLinkedSetIfPossible(int initialCapacity) { + return new LinkedHashSet<T>(initialCapacity); + } + + /** + * Create a copy-on-write Set (allowing for synchronization-less iteration) if possible: + * This implementation always creates a {@link java.util.concurrent.CopyOnWriteArraySet}, + * since Spring 3 requires JDK 1.5 anyway. + * @return the new Set instance + * @deprecated as of Spring 3.0, for usage on JDK 1.5 or higher + */ + @Deprecated + public static <T> Set<T> createCopyOnWriteSet() { + return new CopyOnWriteArraySet<T>(); + } + + /** + * Create a linked Map if possible: This implementation always + * creates a {@link java.util.LinkedHashMap}, since Spring 2.5 + * requires JDK 1.4 anyway. + * @param initialCapacity the initial capacity of the Map + * @return the new Map instance + * @deprecated as of Spring 2.5, for usage on JDK 1.4 or higher + */ + @Deprecated + public static <K,V> Map<K,V> createLinkedMapIfPossible(int initialCapacity) { + return new LinkedHashMap<K,V>(initialCapacity); + } + + /** + * Create a linked case-insensitive Map if possible: This implementation + * always returns a {@link org.springframework.util.LinkedCaseInsensitiveMap}. + * @param initialCapacity the initial capacity of the Map + * @return the new Map instance + * @deprecated as of Spring 3.0, for usage on JDK 1.5 or higher + */ + @Deprecated + public static Map createLinkedCaseInsensitiveMapIfPossible(int initialCapacity) { + return new LinkedCaseInsensitiveMap(initialCapacity); + } + + /** + * Create an identity Map if possible: This implementation always + * creates a {@link java.util.IdentityHashMap}, since Spring 2.5 + * requires JDK 1.4 anyway. + * @param initialCapacity the initial capacity of the Map + * @return the new Map instance + * @deprecated as of Spring 2.5, for usage on JDK 1.4 or higher + */ + @Deprecated + public static Map createIdentityMapIfPossible(int initialCapacity) { + return new IdentityHashMap(initialCapacity); + } + + /** + * Create a concurrent Map if possible: This implementation always + * creates a {@link java.util.concurrent.ConcurrentHashMap}, since Spring 3.0 + * requires JDK 1.5 anyway. + * @param initialCapacity the initial capacity of the Map + * @return the new Map instance + * @deprecated as of Spring 3.0, for usage on JDK 1.5 or higher + */ + @Deprecated + public static Map createConcurrentMapIfPossible(int initialCapacity) { + return new ConcurrentHashMap(initialCapacity); + } + + /** + * Create a concurrent Map with a dedicated {@link ConcurrentMap} interface: + * This implementation always creates a {@link java.util.concurrent.ConcurrentHashMap}, + * since Spring 3.0 requires JDK 1.5 anyway. + * @param initialCapacity the initial capacity of the Map + * @return the new ConcurrentMap instance + * @deprecated as of Spring 3.0, for usage on JDK 1.5 or higher + */ + @Deprecated + public static ConcurrentMap createConcurrentMap(int initialCapacity) { + return new JdkConcurrentHashMap(initialCapacity); + } + + /** + * Determine whether the given collection type is an approximable type, + * i.e. a type that {@link #createApproximateCollection} can approximate. + * @param collectionType the collection type to check + * @return {@code true} if the type is approximable, + * {@code false} if it is not + */ + public static boolean isApproximableCollectionType(Class<?> collectionType) { + return (collectionType != null && approximableCollectionTypes.contains(collectionType)); + } + + /** + * Create the most approximate collection for the given collection. + * <p>Creates an ArrayList, TreeSet or linked Set for a List, SortedSet + * or Set, respectively. + * @param collection the original Collection object + * @param initialCapacity the initial capacity + * @return the new Collection instance + * @see java.util.ArrayList + * @see java.util.TreeSet + * @see java.util.LinkedHashSet + */ + @SuppressWarnings("unchecked") + public static Collection createApproximateCollection(Object collection, int initialCapacity) { + if (collection instanceof LinkedList) { + return new LinkedList(); + } + else if (collection instanceof List) { + return new ArrayList(initialCapacity); + } + else if (collection instanceof SortedSet) { + return new TreeSet(((SortedSet) collection).comparator()); + } + else { + return new LinkedHashSet(initialCapacity); + } + } + + /** + * Create the most appropriate collection for the given collection type. + * <p>Creates an ArrayList, TreeSet or linked Set for a List, SortedSet + * or Set, respectively. + * @param collectionType the desired type of the target Collection + * @param initialCapacity the initial capacity + * @return the new Collection instance + * @see java.util.ArrayList + * @see java.util.TreeSet + * @see java.util.LinkedHashSet + */ + public static Collection createCollection(Class<?> collectionType, int initialCapacity) { + if (collectionType.isInterface()) { + if (List.class.equals(collectionType)) { + return new ArrayList(initialCapacity); + } + else if (SortedSet.class.equals(collectionType) || collectionType.equals(navigableSetClass)) { + return new TreeSet(); + } + else if (Set.class.equals(collectionType) || Collection.class.equals(collectionType)) { + return new LinkedHashSet(initialCapacity); + } + else { + throw new IllegalArgumentException("Unsupported Collection interface: " + collectionType.getName()); + } + } + else { + if (!Collection.class.isAssignableFrom(collectionType)) { + throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName()); + } + try { + return (Collection) collectionType.newInstance(); + } + catch (Exception ex) { + throw new IllegalArgumentException("Could not instantiate Collection type: " + + collectionType.getName(), ex); + } + } + } + + /** + * Determine whether the given map type is an approximable type, + * i.e. a type that {@link #createApproximateMap} can approximate. + * @param mapType the map type to check + * @return {@code true} if the type is approximable, + * {@code false} if it is not + */ + public static boolean isApproximableMapType(Class<?> mapType) { + return (mapType != null && approximableMapTypes.contains(mapType)); + } + + /** + * Create the most approximate map for the given map. + * <p>Creates a TreeMap or linked Map for a SortedMap or Map, respectively. + * @param map the original Map object + * @param initialCapacity the initial capacity + * @return the new Map instance + * @see java.util.TreeMap + * @see java.util.LinkedHashMap + */ + @SuppressWarnings("unchecked") + public static Map createApproximateMap(Object map, int initialCapacity) { + if (map instanceof SortedMap) { + return new TreeMap(((SortedMap) map).comparator()); + } + else { + return new LinkedHashMap(initialCapacity); + } + } + + /** + * Create the most approximate map for the given map. + * <p>Creates a TreeMap or linked Map for a SortedMap or Map, respectively. + * @param mapType the desired type of the target Map + * @param initialCapacity the initial capacity + * @return the new Map instance + * @see java.util.TreeMap + * @see java.util.LinkedHashMap + */ + public static Map createMap(Class<?> mapType, int initialCapacity) { + if (mapType.isInterface()) { + if (Map.class.equals(mapType)) { + return new LinkedHashMap(initialCapacity); + } + else if (SortedMap.class.equals(mapType) || mapType.equals(navigableMapClass)) { + return new TreeMap(); + } + else if (MultiValueMap.class.equals(mapType)) { + return new LinkedMultiValueMap(); + } + else { + throw new IllegalArgumentException("Unsupported Map interface: " + mapType.getName()); + } + } + else { + if (!Map.class.isAssignableFrom(mapType)) { + throw new IllegalArgumentException("Unsupported Map type: " + mapType.getName()); + } + try { + return (Map) mapType.newInstance(); + } + catch (Exception ex) { + throw new IllegalArgumentException("Could not instantiate Map type: " + + mapType.getName(), ex); + } + } + } + + + /** + * ConcurrentMap adapter for the JDK ConcurrentHashMap class. + */ + @Deprecated + @SuppressWarnings("serial") + private static class JdkConcurrentHashMap extends ConcurrentHashMap implements ConcurrentMap { + + private JdkConcurrentHashMap(int initialCapacity) { + super(initialCapacity); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/ConcurrentMap.java b/spring-core/src/main/java/org/springframework/core/ConcurrentMap.java new file mode 100644 index 00000000..64597193 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ConcurrentMap.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2009 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.util.Map; + +/** + * Common interface for a concurrent Map, as exposed by + * {@link CollectionFactory#createConcurrentMap}. Mirrors + * {@link java.util.concurrent.ConcurrentMap}, allowing to be backed by a + * JDK ConcurrentHashMap as well as a backport-concurrent ConcurrentHashMap. + * + * <p>Check out the {@link java.util.concurrent.ConcurrentMap ConcurrentMap javadoc} + * for details on the interface's methods. + * + * @author Juergen Hoeller + * @since 2.5 + * @deprecated as of Spring 3.0, since standard {@link java.util.concurrent.ConcurrentMap} + * is available on Java 5+ anyway + */ +@Deprecated +public interface ConcurrentMap extends Map { + + Object putIfAbsent(Object key, Object value); + + boolean remove(Object key, Object value); + + boolean replace(Object key, Object oldValue, Object newValue); + + Object replace(Object key, Object value); + +} diff --git a/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java b/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java new file mode 100644 index 00000000..4d48b044 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2012 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.io.IOException; +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; + +/** + * Special ObjectInputStream subclass that resolves class names + * against a specific ClassLoader. Serves as base class for + * {@link org.springframework.remoting.rmi.CodebaseAwareObjectInputStream}. + * + * @author Juergen Hoeller + * @since 2.5.5 + */ +public class ConfigurableObjectInputStream extends ObjectInputStream { + + private final ClassLoader classLoader; + + private final boolean acceptProxyClasses; + + + /** + * Create a new ConfigurableObjectInputStream for the given InputStream and ClassLoader. + * @param in the InputStream to read from + * @param classLoader the ClassLoader to use for loading local classes + * @see java.io.ObjectInputStream#ObjectInputStream(java.io.InputStream) + */ + public ConfigurableObjectInputStream(InputStream in, ClassLoader classLoader) throws IOException { + this(in, classLoader, true); + } + + /** + * Create a new ConfigurableObjectInputStream for the given InputStream and ClassLoader. + * @param in the InputStream to read from + * @param classLoader the ClassLoader to use for loading local classes + * @param acceptProxyClasses whether to accept deserialization of proxy classes + * (may be deactivated as a security measure) + * @see java.io.ObjectInputStream#ObjectInputStream(java.io.InputStream) + */ + public ConfigurableObjectInputStream( + InputStream in, ClassLoader classLoader, boolean acceptProxyClasses) throws IOException { + + super(in); + this.classLoader = classLoader; + this.acceptProxyClasses = acceptProxyClasses; + } + + + @Override + protected Class resolveClass(ObjectStreamClass classDesc) throws IOException, ClassNotFoundException { + try { + if (this.classLoader != null) { + // Use the specified ClassLoader to resolve local classes. + return ClassUtils.forName(classDesc.getName(), this.classLoader); + } + else { + // Use the default ClassLoader... + return super.resolveClass(classDesc); + } + } + catch (ClassNotFoundException ex) { + return resolveFallbackIfPossible(classDesc.getName(), ex); + } + } + + @Override + protected Class resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException { + if (!this.acceptProxyClasses) { + throw new NotSerializableException("Not allowed to accept serialized proxy classes"); + } + if (this.classLoader != null) { + // Use the specified ClassLoader to resolve local proxy classes. + Class[] resolvedInterfaces = new Class[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + try { + resolvedInterfaces[i] = ClassUtils.forName(interfaces[i], this.classLoader); + } + catch (ClassNotFoundException ex) { + resolvedInterfaces[i] = resolveFallbackIfPossible(interfaces[i], ex); + } + } + try { + return Proxy.getProxyClass(this.classLoader, resolvedInterfaces); + } + catch (IllegalArgumentException ex) { + throw new ClassNotFoundException(null, ex); + } + } + else { + // Use ObjectInputStream's default ClassLoader... + try { + return super.resolveProxyClass(interfaces); + } + catch (ClassNotFoundException ex) { + Class[] resolvedInterfaces = new Class[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + resolvedInterfaces[i] = resolveFallbackIfPossible(interfaces[i], ex); + } + return Proxy.getProxyClass(getFallbackClassLoader(), resolvedInterfaces); + } + } + } + + + /** + * Resolve the given class name against a fallback class loader. + * <p>The default implementation simply rethrows the original exception, + * since there is no fallback available. + * @param className the class name to resolve + * @param ex the original exception thrown when attempting to load the class + * @return the newly resolved class (never {@code null}) + */ + protected Class resolveFallbackIfPossible(String className, ClassNotFoundException ex) + throws IOException, ClassNotFoundException{ + + throw ex; + } + + /** + * Return the fallback ClassLoader to use when no ClassLoader was specified + * and ObjectInputStream's own default ClassLoader failed. + * <p>The default implementation simply returns {@code null}. + */ + protected ClassLoader getFallbackClassLoader() throws IOException { + return null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/ConstantException.java b/spring-core/src/main/java/org/springframework/core/ConstantException.java new file mode 100644 index 00000000..451b46b8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ConstantException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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; + +/** + * Exception thrown when the {@link Constants} class is asked for + * an invalid constant name. + * + * @author Rod Johnson + * @since 28.04.2003 + * @see org.springframework.core.Constants + */ +@SuppressWarnings("serial") +public class ConstantException extends IllegalArgumentException { + + /** + * Thrown when an invalid constant name is requested. + * @param className name of the class containing the constant definitions + * @param field invalid constant name + * @param message description of the problem + */ + public ConstantException(String className, String field, String message) { + super("Field '" + field + "' " + message + " in class [" + className + "]"); + } + + /** + * Thrown when an invalid constant value is looked up. + * @param className name of the class containing the constant definitions + * @param namePrefix prefix of the searched constant names + * @param value the looked up constant value + */ + public ConstantException(String className, String namePrefix, Object value) { + super("No '" + namePrefix + "' field with value '" + value + "' found in class [" + className + "]"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/Constants.java b/spring-core/src/main/java/org/springframework/core/Constants.java new file mode 100644 index 00000000..3f8d9737 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/Constants.java @@ -0,0 +1,336 @@ +/* + * Copyright 2002-2012 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.Field; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * This class can be used to parse other classes containing constant definitions + * in public static final members. The {@code asXXXX} methods of this class + * allow these constant values to be accessed via their string names. + * + * <p>Consider class Foo containing {@code public final static int CONSTANT1 = 66;} + * An instance of this class wrapping {@code Foo.class} will return the constant value + * of 66 from its {@code asNumber} method given the argument {@code "CONSTANT1"}. + * + * <p>This class is ideal for use in PropertyEditors, enabling them to + * recognize the same names as the constants themselves, and freeing them + * from maintaining their own mapping. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 16.03.2003 + */ +public class Constants { + + /** The name of the introspected class */ + private final String className; + + /** Map from String field name to object value */ + private final Map<String, Object> fieldCache = new HashMap<String, Object>(); + + + /** + * Create a new Constants converter class wrapping the given class. + * <p>All <b>public</b> static final variables will be exposed, whatever their type. + * @param clazz the class to analyze + * @throws IllegalArgumentException if the supplied {@code clazz} is {@code null} + */ + public Constants(Class<?> clazz) { + Assert.notNull(clazz); + this.className = clazz.getName(); + Field[] fields = clazz.getFields(); + for (Field field : fields) { + if (ReflectionUtils.isPublicStaticFinal(field)) { + String name = field.getName(); + try { + Object value = field.get(null); + this.fieldCache.put(name, value); + } + catch (IllegalAccessException ex) { + // just leave this field and continue + } + } + } + } + + + /** + * Return the name of the analyzed class. + */ + public final String getClassName() { + return this.className; + } + + /** + * Return the number of constants exposed. + */ + public final int getSize() { + return this.fieldCache.size(); + } + + /** + * Exposes the field cache to subclasses: + * a Map from String field name to object value. + */ + protected final Map<String, Object> getFieldCache() { + return this.fieldCache; + } + + + /** + * Return a constant value cast to a Number. + * @param code the name of the field (never {@code null}) + * @return the Number value + * @see #asObject + * @throws ConstantException if the field name wasn't found + * or if the type wasn't compatible with Number + */ + public Number asNumber(String code) throws ConstantException { + Object obj = asObject(code); + if (!(obj instanceof Number)) { + throw new ConstantException(this.className, code, "not a Number"); + } + return (Number) obj; + } + + /** + * Return a constant value as a String. + * @param code the name of the field (never {@code null}) + * @return the String value + * Works even if it's not a string (invokes {@code toString()}). + * @see #asObject + * @throws ConstantException if the field name wasn't found + */ + public String asString(String code) throws ConstantException { + return asObject(code).toString(); + } + + /** + * Parse the given String (upper or lower case accepted) and return + * the appropriate value if it's the name of a constant field in the + * class that we're analysing. + * @param code the name of the field (never {@code null}) + * @return the Object value + * @throws ConstantException if there's no such field + */ + public Object asObject(String code) throws ConstantException { + Assert.notNull(code, "Code must not be null"); + String codeToUse = code.toUpperCase(Locale.ENGLISH); + Object val = this.fieldCache.get(codeToUse); + if (val == null) { + throw new ConstantException(this.className, codeToUse, "not found"); + } + return val; + } + + + /** + * Return all names of the given group of constants. + * <p>Note that this method assumes that constants are named + * in accordance with the standard Java convention for constant + * values (i.e. all uppercase). The supplied {@code namePrefix} + * will be uppercased (in a locale-insensitive fashion) prior to + * the main logic of this method kicking in. + * @param namePrefix prefix of the constant names to search (may be {@code null}) + * @return the set of constant names + */ + public Set<String> getNames(String namePrefix) { + String prefixToUse = (namePrefix != null ? namePrefix.trim().toUpperCase(Locale.ENGLISH) : ""); + Set<String> names = new HashSet<String>(); + for (String code : this.fieldCache.keySet()) { + if (code.startsWith(prefixToUse)) { + names.add(code); + } + } + return names; + } + + /** + * Return all names of the group of constants for the + * given bean property name. + * @param propertyName the name of the bean property + * @return the set of values + * @see #propertyToConstantNamePrefix + */ + public Set<String> getNamesForProperty(String propertyName) { + return getNames(propertyToConstantNamePrefix(propertyName)); + } + + /** + * Return all names of the given group of constants. + * <p>Note that this method assumes that constants are named + * in accordance with the standard Java convention for constant + * values (i.e. all uppercase). The supplied {@code nameSuffix} + * will be uppercased (in a locale-insensitive fashion) prior to + * the main logic of this method kicking in. + * @param nameSuffix suffix of the constant names to search (may be {@code null}) + * @return the set of constant names + */ + public Set<String> getNamesForSuffix(String nameSuffix) { + String suffixToUse = (nameSuffix != null ? nameSuffix.trim().toUpperCase(Locale.ENGLISH) : ""); + Set<String> names = new HashSet<String>(); + for (String code : this.fieldCache.keySet()) { + if (code.endsWith(suffixToUse)) { + names.add(code); + } + } + return names; + } + + + /** + * Return all values of the given group of constants. + * <p>Note that this method assumes that constants are named + * in accordance with the standard Java convention for constant + * values (i.e. all uppercase). The supplied {@code namePrefix} + * will be uppercased (in a locale-insensitive fashion) prior to + * the main logic of this method kicking in. + * @param namePrefix prefix of the constant names to search (may be {@code null}) + * @return the set of values + */ + public Set<Object> getValues(String namePrefix) { + String prefixToUse = (namePrefix != null ? namePrefix.trim().toUpperCase(Locale.ENGLISH) : ""); + Set<Object> values = new HashSet<Object>(); + for (String code : this.fieldCache.keySet()) { + if (code.startsWith(prefixToUse)) { + values.add(this.fieldCache.get(code)); + } + } + return values; + } + + /** + * Return all values of the group of constants for the + * given bean property name. + * @param propertyName the name of the bean property + * @return the set of values + * @see #propertyToConstantNamePrefix + */ + public Set<Object> getValuesForProperty(String propertyName) { + return getValues(propertyToConstantNamePrefix(propertyName)); + } + + /** + * Return all values of the given group of constants. + * <p>Note that this method assumes that constants are named + * in accordance with the standard Java convention for constant + * values (i.e. all uppercase). The supplied {@code nameSuffix} + * will be uppercased (in a locale-insensitive fashion) prior to + * the main logic of this method kicking in. + * @param nameSuffix suffix of the constant names to search (may be {@code null}) + * @return the set of values + */ + public Set<Object> getValuesForSuffix(String nameSuffix) { + String suffixToUse = (nameSuffix != null ? nameSuffix.trim().toUpperCase(Locale.ENGLISH) : ""); + Set<Object> values = new HashSet<Object>(); + for (String code : this.fieldCache.keySet()) { + if (code.endsWith(suffixToUse)) { + values.add(this.fieldCache.get(code)); + } + } + return values; + } + + + /** + * Look up the given value within the given group of constants. + * <p>Will return the first match. + * @param value constant value to look up + * @param namePrefix prefix of the constant names to search (may be {@code null}) + * @return the name of the constant field + * @throws ConstantException if the value wasn't found + */ + public String toCode(Object value, String namePrefix) throws ConstantException { + String prefixToUse = (namePrefix != null ? namePrefix.trim().toUpperCase(Locale.ENGLISH) : ""); + for (Map.Entry<String, Object> entry : this.fieldCache.entrySet()) { + if (entry.getKey().startsWith(prefixToUse) && entry.getValue().equals(value)) { + return entry.getKey(); + } + } + throw new ConstantException(this.className, prefixToUse, value); + } + + /** + * Look up the given value within the group of constants for + * the given bean property name. Will return the first match. + * @param value constant value to look up + * @param propertyName the name of the bean property + * @return the name of the constant field + * @throws ConstantException if the value wasn't found + * @see #propertyToConstantNamePrefix + */ + public String toCodeForProperty(Object value, String propertyName) throws ConstantException { + return toCode(value, propertyToConstantNamePrefix(propertyName)); + } + + /** + * Look up the given value within the given group of constants. + * <p>Will return the first match. + * @param value constant value to look up + * @param nameSuffix suffix of the constant names to search (may be {@code null}) + * @return the name of the constant field + * @throws ConstantException if the value wasn't found + */ + public String toCodeForSuffix(Object value, String nameSuffix) throws ConstantException { + String suffixToUse = (nameSuffix != null ? nameSuffix.trim().toUpperCase(Locale.ENGLISH) : ""); + for (Map.Entry<String, Object> entry : this.fieldCache.entrySet()) { + if (entry.getKey().endsWith(suffixToUse) && entry.getValue().equals(value)) { + return entry.getKey(); + } + } + throw new ConstantException(this.className, suffixToUse, value); + } + + + /** + * Convert the given bean property name to a constant name prefix. + * <p>Uses a common naming idiom: turning all lower case characters to + * upper case, and prepending upper case characters with an underscore. + * <p>Example: "imageSize" -> "IMAGE_SIZE"<br> + * Example: "imagesize" -> "IMAGESIZE".<br> + * Example: "ImageSize" -> "_IMAGE_SIZE".<br> + * Example: "IMAGESIZE" -> "_I_M_A_G_E_S_I_Z_E" + * @param propertyName the name of the bean property + * @return the corresponding constant name prefix + * @see #getValuesForProperty + * @see #toCodeForProperty + */ + public String propertyToConstantNamePrefix(String propertyName) { + StringBuilder parsedPrefix = new StringBuilder(); + for (int i = 0; i < propertyName.length(); i++) { + char c = propertyName.charAt(i); + if (Character.isUpperCase(c)) { + parsedPrefix.append("_"); + parsedPrefix.append(c); + } + else { + parsedPrefix.append(Character.toUpperCase(c)); + } + } + return parsedPrefix.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/ControlFlow.java b/spring-core/src/main/java/org/springframework/core/ControlFlow.java new file mode 100644 index 00000000..bc754211 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ControlFlow.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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 objects that can return information about + * the current call stack. Useful in AOP (as in AspectJ cflow concept) + * but not AOP-specific. + * + * @author Rod Johnson + * @since 02.02.2004 + */ +public interface ControlFlow { + + /** + * Detect whether we're under the given class, + * according to the current stack trace. + * @param clazz the clazz to look for + */ + boolean under(Class clazz); + + /** + * Detect whether we're under the given class and method, + * according to the current stack trace. + * @param clazz the clazz to look for + * @param methodName the name of the method to look for + */ + boolean under(Class clazz, String methodName); + + /** + * Detect whether the current stack trace contains the given token. + * @param token the token to look for + */ + boolean underToken(String token); + +} diff --git a/spring-core/src/main/java/org/springframework/core/ControlFlowFactory.java b/spring-core/src/main/java/org/springframework/core/ControlFlowFactory.java new file mode 100644 index 00000000..6430ff7f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ControlFlowFactory.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2008 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.io.PrintWriter; +import java.io.StringWriter; + +import org.springframework.util.Assert; + +/** + * Static factory to conceal the automatic choice of the ControlFlow + * implementation class. + * + * <p>This implementation always uses the efficient Java 1.4 StackTraceElement + * mechanism for analyzing control flows. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 02.02.2004 + */ +public abstract class ControlFlowFactory { + + /** + * Return an appropriate {@link ControlFlow} instance. + */ + public static ControlFlow createControlFlow() { + return new Jdk14ControlFlow(); + } + + + /** + * Utilities for cflow-style pointcuts. Note that such pointcuts are + * 5-10 times more expensive to evaluate than other pointcuts, as they require + * analysis of the stack trace (through constructing a new throwable). + * However, they are useful in some cases. + * <p>This implementation uses the StackTraceElement class introduced in Java 1.4. + * @see java.lang.StackTraceElement + */ + static class Jdk14ControlFlow implements ControlFlow { + + private StackTraceElement[] stack; + + public Jdk14ControlFlow() { + this.stack = new Throwable().getStackTrace(); + } + + /** + * Searches for class name match in a StackTraceElement. + */ + public boolean under(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + String className = clazz.getName(); + for (int i = 0; i < stack.length; i++) { + if (this.stack[i].getClassName().equals(className)) { + return true; + } + } + return false; + } + + /** + * Searches for class name match plus method name match + * in a StackTraceElement. + */ + public boolean under(Class clazz, String methodName) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + String className = clazz.getName(); + for (int i = 0; i < this.stack.length; i++) { + if (this.stack[i].getClassName().equals(className) && + this.stack[i].getMethodName().equals(methodName)) { + return true; + } + } + return false; + } + + /** + * Leave it up to the caller to decide what matches. + * Caller must understand stack trace format, so there's less abstraction. + */ + public boolean underToken(String token) { + if (token == null) { + return false; + } + StringWriter sw = new StringWriter(); + new Throwable().printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + return stackTrace.indexOf(token) != -1; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Jdk14ControlFlow: "); + for (int i = 0; i < this.stack.length; i++) { + if (i > 0) { + sb.append("\n\t@"); + } + sb.append(this.stack[i]); + } + return sb.toString(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/Conventions.java b/spring-core/src/main/java/org/springframework/core/Conventions.java new file mode 100644 index 00000000..7ea7c825 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/Conventions.java @@ -0,0 +1,302 @@ +/* + * Copyright 2002-2012 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.io.Externalizable; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Provides methods to support various naming and other conventions used + * throughout the framework. Mainly for internal use within the framework. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class Conventions { + + /** + * Suffix added to names when using arrays. + */ + private static final String PLURAL_SUFFIX = "List"; + + + /** + * Set of interfaces that are supposed to be ignored + * when searching for the 'primary' interface of a proxy. + */ + private static final Set<Class> ignoredInterfaces = new HashSet<Class>(); + + static { + ignoredInterfaces.add(Serializable.class); + ignoredInterfaces.add(Externalizable.class); + ignoredInterfaces.add(Cloneable.class); + ignoredInterfaces.add(Comparable.class); + } + + + /** + * Determine the conventional variable name for the supplied + * {@code Object} based on its concrete type. The convention + * used is to return the uncapitalized short name of the {@code Class}, + * according to JavaBeans property naming rules: So, + * {@code com.myapp.Product} becomes {@code product}; + * {@code com.myapp.MyProduct} becomes {@code myProduct}; + * {@code com.myapp.UKProduct} becomes {@code UKProduct}. + * <p>For arrays, we use the pluralized version of the array component type. + * For {@code Collection}s we attempt to 'peek ahead' in the + * {@code Collection} to determine the component type and + * return the pluralized version of that component type. + * @param value the value to generate a variable name for + * @return the generated variable name + */ + public static String getVariableName(Object value) { + Assert.notNull(value, "Value must not be null"); + Class valueClass; + boolean pluralize = false; + + if (value.getClass().isArray()) { + valueClass = value.getClass().getComponentType(); + pluralize = true; + } + else if (value instanceof Collection) { + Collection collection = (Collection) value; + if (collection.isEmpty()) { + throw new IllegalArgumentException("Cannot generate variable name for an empty Collection"); + } + Object valueToCheck = peekAhead(collection); + valueClass = getClassForValue(valueToCheck); + pluralize = true; + } + else { + valueClass = getClassForValue(value); + } + + String name = ClassUtils.getShortNameAsProperty(valueClass); + return (pluralize ? pluralize(name) : name); + } + + /** + * Determine the conventional variable name for the supplied parameter, + * taking the generic collection type (if any) into account. + * @param parameter the method or constructor parameter to generate a variable name for + * @return the generated variable name + */ + public static String getVariableNameForParameter(MethodParameter parameter) { + Assert.notNull(parameter, "MethodParameter must not be null"); + Class valueClass; + boolean pluralize = false; + + if (parameter.getParameterType().isArray()) { + valueClass = parameter.getParameterType().getComponentType(); + pluralize = true; + } + else if (Collection.class.isAssignableFrom(parameter.getParameterType())) { + valueClass = GenericCollectionTypeResolver.getCollectionParameterType(parameter); + if (valueClass == null) { + throw new IllegalArgumentException( + "Cannot generate variable name for non-typed Collection parameter type"); + } + pluralize = true; + } + else { + valueClass = parameter.getParameterType(); + } + + String name = ClassUtils.getShortNameAsProperty(valueClass); + return (pluralize ? pluralize(name) : name); + } + + /** + * Determine the conventional variable name for the return type of the supplied method, + * taking the generic collection type (if any) into account. + * @param method the method to generate a variable name for + * @return the generated variable name + */ + public static String getVariableNameForReturnType(Method method) { + return getVariableNameForReturnType(method, method.getReturnType(), null); + } + + /** + * Determine the conventional variable name for the return type of the supplied method, + * taking the generic collection type (if any) into account, falling back to the + * given return value if the method declaration is not specific enough (i.e. in case of + * the return type being declared as {@code Object} or as untyped collection). + * @param method the method to generate a variable name for + * @param value the return value (may be {@code null} if not available) + * @return the generated variable name + */ + public static String getVariableNameForReturnType(Method method, Object value) { + return getVariableNameForReturnType(method, method.getReturnType(), value); + } + + /** + * Determine the conventional variable name for the return type of the supplied method, + * taking the generic collection type (if any) into account, falling back to the + * given return value if the method declaration is not specific enough (i.e. in case of + * the return type being declared as {@code Object} or as untyped collection). + * @param method the method to generate a variable name for + * @param resolvedType the resolved return type of the method + * @param value the return value (may be {@code null} if not available) + * @return the generated variable name + */ + public static String getVariableNameForReturnType(Method method, Class resolvedType, Object value) { + Assert.notNull(method, "Method must not be null"); + + if (Object.class.equals(resolvedType)) { + if (value == null) { + throw new IllegalArgumentException("Cannot generate variable name for an Object return type with null value"); + } + return getVariableName(value); + } + + Class valueClass; + boolean pluralize = false; + + if (resolvedType.isArray()) { + valueClass = resolvedType.getComponentType(); + pluralize = true; + } + else if (Collection.class.isAssignableFrom(resolvedType)) { + valueClass = GenericCollectionTypeResolver.getCollectionReturnType(method); + if (valueClass == null) { + if (!(value instanceof Collection)) { + throw new IllegalArgumentException( + "Cannot generate variable name for non-typed Collection return type and a non-Collection value"); + } + Collection collection = (Collection) value; + if (collection.isEmpty()) { + throw new IllegalArgumentException( + "Cannot generate variable name for non-typed Collection return type and an empty Collection value"); + } + Object valueToCheck = peekAhead(collection); + valueClass = getClassForValue(valueToCheck); + } + pluralize = true; + } + else { + valueClass = resolvedType; + } + + String name = ClassUtils.getShortNameAsProperty(valueClass); + return (pluralize ? pluralize(name) : name); + } + + /** + * Convert {@code String}s in attribute name format (lowercase, hyphens separating words) + * into property name format (camel-cased). For example, {@code transaction-manager} is + * converted into {@code transactionManager}. + */ + public static String attributeNameToPropertyName(String attributeName) { + Assert.notNull(attributeName, "'attributeName' must not be null"); + if (!attributeName.contains("-")) { + return attributeName; + } + char[] chars = attributeName.toCharArray(); + char[] result = new char[chars.length -1]; // not completely accurate but good guess + int currPos = 0; + boolean upperCaseNext = false; + for (char c : chars) { + if (c == '-') { + upperCaseNext = true; + } + else if (upperCaseNext) { + result[currPos++] = Character.toUpperCase(c); + upperCaseNext = false; + } + else { + result[currPos++] = c; + } + } + return new String(result, 0, currPos); + } + + /** + * Return an attribute name qualified by the supplied enclosing {@link Class}. For example, + * the attribute name '{@code foo}' qualified by {@link Class} '{@code com.myapp.SomeClass}' + * would be '{@code com.myapp.SomeClass.foo}' + */ + public static String getQualifiedAttributeName(Class enclosingClass, String attributeName) { + Assert.notNull(enclosingClass, "'enclosingClass' must not be null"); + Assert.notNull(attributeName, "'attributeName' must not be null"); + return enclosingClass.getName() + "." + attributeName; + } + + + /** + * Determines the class to use for naming a variable that contains + * the given value. + * <p>Will return the class of the given value, except when + * encountering a JDK proxy, in which case it will determine + * the 'primary' interface implemented by that proxy. + * @param value the value to check + * @return the class to use for naming a variable + */ + private static Class getClassForValue(Object value) { + Class valueClass = value.getClass(); + if (Proxy.isProxyClass(valueClass)) { + Class[] ifcs = valueClass.getInterfaces(); + for (Class ifc : ifcs) { + if (!ignoredInterfaces.contains(ifc)) { + return ifc; + } + } + } + else if (valueClass.getName().lastIndexOf('$') != -1 && valueClass.getDeclaringClass() == null) { + // '$' in the class name but no inner class - + // assuming it's a special subclass (e.g. by OpenJPA) + valueClass = valueClass.getSuperclass(); + } + return valueClass; + } + + /** + * Pluralize the given name. + */ + private static String pluralize(String name) { + return name + PLURAL_SUFFIX; + } + + /** + * Retrieves the {@code Class} of an element in the {@code Collection}. + * The exact element for which the {@code Class} is retreived will depend + * on the concrete {@code Collection} implementation. + */ + private static Object peekAhead(Collection collection) { + Iterator it = collection.iterator(); + if (!it.hasNext()) { + throw new IllegalStateException( + "Unable to peek ahead in non-empty collection - no element found"); + } + Object value = it.next(); + if (value == null) { + throw new IllegalStateException( + "Unable to peek ahead in non-empty collection - only null element found"); + } + return value; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java b/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java new file mode 100644 index 00000000..0e677141 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2008 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.util.HashSet; +import java.util.Set; + +import org.springframework.util.Assert; + +/** + * Base class for decorating ClassLoaders such as {@link OverridingClassLoader} + * and {@link org.springframework.instrument.classloading.ShadowingClassLoader}, + * providing common handling of excluded packages and classes. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 2.5.2 + */ +public abstract class DecoratingClassLoader extends ClassLoader { + + private final Set<String> excludedPackages = new HashSet<String>(); + + private final Set<String> excludedClasses = new HashSet<String>(); + + private final Object exclusionMonitor = new Object(); + + + /** + * Create a new DecoratingClassLoader with no parent ClassLoader. + */ + public DecoratingClassLoader() { + } + + /** + * Create a new DecoratingClassLoader using the given parent ClassLoader + * for delegation. + */ + public DecoratingClassLoader(ClassLoader parent) { + super(parent); + } + + + /** + * Add a package name to exclude from decoration (e.g. overriding). + * <p>Any class whose fully-qualified name starts with the name registered + * here will be handled by the parent ClassLoader in the usual fashion. + * @param packageName the package name to exclude + */ + public void excludePackage(String packageName) { + Assert.notNull(packageName, "Package name must not be null"); + synchronized (this.exclusionMonitor) { + this.excludedPackages.add(packageName); + } + } + + /** + * Add a class name to exclude from decoration (e.g. overriding). + * <p>Any class name registered here will be handled by the parent + * ClassLoader in the usual fashion. + * @param className the class name to exclude + */ + public void excludeClass(String className) { + Assert.notNull(className, "Class name must not be null"); + synchronized (this.exclusionMonitor) { + this.excludedClasses.add(className); + } + } + + /** + * Determine whether the specified class is excluded from decoration + * by this class loader. + * <p>The default implementation checks against excluded packages and classes. + * @param className the class name to check + * @return whether the specified class is eligible + * @see #excludePackage + * @see #excludeClass + */ + protected boolean isExcluded(String className) { + synchronized (this.exclusionMonitor) { + if (this.excludedClasses.contains(className)) { + 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/ErrorCoded.java b/spring-core/src/main/java/org/springframework/core/ErrorCoded.java new file mode 100644 index 00000000..1425fe84 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ErrorCoded.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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 that can be implemented by exceptions etc that are error coded. + * The error code is a String, rather than a number, so it can be given + * user-readable values, such as "object.failureDescription". + * + * <p>An error code can be resolved by a MessageSource, for example. + * + * @author Rod Johnson + * @see org.springframework.context.MessageSource + */ +public interface ErrorCoded { + + /** + * Return the error code associated with this failure. + * The GUI can render this any way it pleases, allowing for localization etc. + * @return a String error code associated with this failure, + * or {@code null} if not error-coded + */ + String getErrorCode(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/ExceptionDepthComparator.java b/spring-core/src/main/java/org/springframework/core/ExceptionDepthComparator.java new file mode 100644 index 00000000..53e3425f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ExceptionDepthComparator.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2011 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.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Comparator capable of sorting exceptions based on their depth from the thrown exception type. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @since 3.0.3 + */ +public class ExceptionDepthComparator implements Comparator<Class<? extends Throwable>> { + + private final Class<? extends Throwable> targetException; + + + /** + * Create a new ExceptionDepthComparator for the given exception. + * @param exception the target exception to compare to when sorting by depth + */ + public ExceptionDepthComparator(Throwable exception) { + Assert.notNull(exception, "Target exception must not be null"); + this.targetException = exception.getClass(); + } + + /** + * Create a new ExceptionDepthComparator for the given exception type. + * @param exceptionType the target exception type to compare to when sorting by depth + */ + public ExceptionDepthComparator(Class<? extends Throwable> exceptionType) { + Assert.notNull(exceptionType, "Target exception type must not be null"); + this.targetException = exceptionType; + } + + + public int compare(Class<? extends Throwable> o1, Class<? extends Throwable> o2) { + int depth1 = getDepth(o1, this.targetException, 0); + int depth2 = getDepth(o2, this.targetException, 0); + return (depth1 - depth2); + } + + private int getDepth(Class declaredException, Class exceptionToMatch, int depth) { + if (declaredException.equals(exceptionToMatch)) { + // Found it! + return depth; + } + // If we've gone as far as we can go and haven't found it... + if (Throwable.class.equals(exceptionToMatch)) { + return Integer.MAX_VALUE; + } + return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1); + } + + + /** + * Obtain the closest match from the given exception types for the given target exception. + * @param exceptionTypes the collection of exception types + * @param targetException the target exception to find a match for + * @return the closest matching exception type from the given collection + */ + public static Class<? extends Throwable> findClosestMatch( + Collection<Class<? extends Throwable>> exceptionTypes, Throwable targetException) { + + Assert.notEmpty(exceptionTypes, "Exception types must not be empty"); + if (exceptionTypes.size() == 1) { + return exceptionTypes.iterator().next(); + } + List<Class<? extends Throwable>> handledExceptions = + new ArrayList<Class<? extends Throwable>>(exceptionTypes); + Collections.sort(handledExceptions, new ExceptionDepthComparator(targetException)); + return handledExceptions.get(0); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/GenericCollectionTypeResolver.java b/spring-core/src/main/java/org/springframework/core/GenericCollectionTypeResolver.java new file mode 100644 index 00000000..8b4c280a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/GenericCollectionTypeResolver.java @@ -0,0 +1,483 @@ +/* + * Copyright 2002-2013 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.Array; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.MalformedParameterizedTypeException; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Collection; +import java.util.Map; + +/** + * Helper class for determining element types of collections and maps. + * + * <p>Mainly intended for usage within the framework, determining the + * target type of values to be added to a collection or map + * (to be able to attempt type conversion if appropriate). + * + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class GenericCollectionTypeResolver { + + /** + * Determine the generic element type of the given Collection class + * (if it declares one through a generic superclass or generic interface). + * @param collectionClass the collection class to introspect + * @return the generic type, or {@code null} if none + */ + public static Class<?> getCollectionType(Class<? extends Collection> collectionClass) { + return extractTypeFromClass(collectionClass, Collection.class, 0); + } + + /** + * Determine the generic key type of the given Map class + * (if it declares one through a generic superclass or generic interface). + * @param mapClass the map class to introspect + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapKeyType(Class<? extends Map> mapClass) { + return extractTypeFromClass(mapClass, Map.class, 0); + } + + /** + * Determine the generic value type of the given Map class + * (if it declares one through a generic superclass or generic interface). + * @param mapClass the map class to introspect + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapValueType(Class<? extends Map> mapClass) { + return extractTypeFromClass(mapClass, Map.class, 1); + } + + /** + * Determine the generic element type of the given Collection field. + * @param collectionField the collection field to introspect + * @return the generic type, or {@code null} if none + */ + public static Class<?> getCollectionFieldType(Field collectionField) { + return getGenericFieldType(collectionField, Collection.class, 0, null, 1); + } + + /** + * Determine the generic element type of the given Collection field. + * @param collectionField the collection field to introspect + * @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) + * @return the generic type, or {@code null} if none + */ + public static Class<?> getCollectionFieldType(Field collectionField, int nestingLevel) { + return getGenericFieldType(collectionField, Collection.class, 0, null, nestingLevel); + } + + /** + * Determine the generic element type of the given Collection field. + * @param collectionField the collection field to introspect + * @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) + * @param typeIndexesPerLevel Map keyed by nesting level, with each value + * expressing the type index for traversal at that level + * @return the generic type, or {@code null} if none + */ + public static Class<?> getCollectionFieldType(Field collectionField, int nestingLevel, Map<Integer, Integer> typeIndexesPerLevel) { + return getGenericFieldType(collectionField, Collection.class, 0, typeIndexesPerLevel, nestingLevel); + } + + /** + * Determine the generic key type of the given Map field. + * @param mapField the map field to introspect + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapKeyFieldType(Field mapField) { + return getGenericFieldType(mapField, Map.class, 0, null, 1); + } + + /** + * Determine the generic key type of the given Map field. + * @param mapField the map field to introspect + * @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) + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapKeyFieldType(Field mapField, int nestingLevel) { + return getGenericFieldType(mapField, Map.class, 0, null, nestingLevel); + } + + /** + * Determine the generic key type of the given Map field. + * @param mapField the map field to introspect + * @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) + * @param typeIndexesPerLevel Map keyed by nesting level, with each value + * expressing the type index for traversal at that level + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapKeyFieldType(Field mapField, int nestingLevel, Map<Integer, Integer> typeIndexesPerLevel) { + return getGenericFieldType(mapField, Map.class, 0, typeIndexesPerLevel, nestingLevel); + } + + /** + * Determine the generic value type of the given Map field. + * @param mapField the map field to introspect + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapValueFieldType(Field mapField) { + return getGenericFieldType(mapField, Map.class, 1, null, 1); + } + + /** + * Determine the generic value type of the given Map field. + * @param mapField the map field to introspect + * @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) + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapValueFieldType(Field mapField, int nestingLevel) { + return getGenericFieldType(mapField, Map.class, 1, null, nestingLevel); + } + + /** + * Determine the generic value type of the given Map field. + * @param mapField the map field to introspect + * @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) + * @param typeIndexesPerLevel Map keyed by nesting level, with each value + * expressing the type index for traversal at that level + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapValueFieldType(Field mapField, int nestingLevel, Map<Integer, Integer> typeIndexesPerLevel) { + return getGenericFieldType(mapField, Map.class, 1, typeIndexesPerLevel, nestingLevel); + } + + /** + * Determine the generic element type of the given Collection parameter. + * @param methodParam the method parameter specification + * @return the generic type, or {@code null} if none + */ + public static Class<?> getCollectionParameterType(MethodParameter methodParam) { + return getGenericParameterType(methodParam, Collection.class, 0); + } + + /** + * Determine the generic key type of the given Map parameter. + * @param methodParam the method parameter specification + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapKeyParameterType(MethodParameter methodParam) { + return getGenericParameterType(methodParam, Map.class, 0); + } + + /** + * Determine the generic value type of the given Map parameter. + * @param methodParam the method parameter specification + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapValueParameterType(MethodParameter methodParam) { + return getGenericParameterType(methodParam, Map.class, 1); + } + + /** + * Determine the generic element type of the given Collection return type. + * @param method the method to check the return type for + * @return the generic type, or {@code null} if none + */ + public static Class<?> getCollectionReturnType(Method method) { + return getGenericReturnType(method, Collection.class, 0, 1); + } + + /** + * Determine the generic element type of the given Collection return type. + * <p>If the specified nesting level is higher than 1, the element type of + * a nested Collection/Map will be analyzed. + * @param method the method to check the return type for + * @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) + * @return the generic type, or {@code null} if none + */ + public static Class<?> getCollectionReturnType(Method method, int nestingLevel) { + return getGenericReturnType(method, Collection.class, 0, nestingLevel); + } + + /** + * Determine the generic key type of the given Map return type. + * @param method the method to check the return type for + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapKeyReturnType(Method method) { + return getGenericReturnType(method, Map.class, 0, 1); + } + + /** + * Determine the generic key type of the given Map return type. + * @param method the method to check the return type for + * @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) + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapKeyReturnType(Method method, int nestingLevel) { + return getGenericReturnType(method, Map.class, 0, nestingLevel); + } + + /** + * Determine the generic value type of the given Map return type. + * @param method the method to check the return type for + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapValueReturnType(Method method) { + return getGenericReturnType(method, Map.class, 1, 1); + } + + /** + * Determine the generic value type of the given Map return type. + * @param method the method to check the return type for + * @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) + * @return the generic type, or {@code null} if none + */ + public static Class<?> getMapValueReturnType(Method method, int nestingLevel) { + return getGenericReturnType(method, Map.class, 1, nestingLevel); + } + + + /** + * Extract the generic parameter type from the given method or constructor. + * @param methodParam the method parameter specification + * @param source the source class/interface defining the generic parameter types + * @param typeIndex the index of the type (e.g. 0 for Collections, + * 0 for Map keys, 1 for Map values) + * @return the generic type, or {@code null} if none + */ + private static Class<?> getGenericParameterType(MethodParameter methodParam, Class<?> source, int typeIndex) { + return extractType(GenericTypeResolver.getTargetType(methodParam), source, typeIndex, + methodParam.typeVariableMap, methodParam.typeIndexesPerLevel, methodParam.getNestingLevel(), 1); + } + + /** + * Extract the generic type from the given field. + * @param field the field to check the type for + * @param source the source class/interface defining the generic parameter types + * @param typeIndex the index of the type (e.g. 0 for Collections, + * 0 for Map keys, 1 for Map values) + * @param nestingLevel the nesting level of the target type + * @return the generic type, or {@code null} if none + */ + private static Class<?> getGenericFieldType(Field field, Class<?> source, int typeIndex, + Map<Integer, Integer> typeIndexesPerLevel, int nestingLevel) { + return extractType(field.getGenericType(), source, typeIndex, null, typeIndexesPerLevel, nestingLevel, 1); + } + + /** + * Extract the generic return type from the given method. + * @param method the method to check the return type for + * @param source the source class/interface defining the generic parameter types + * @param typeIndex the index of the type (e.g. 0 for Collections, + * 0 for Map keys, 1 for Map values) + * @param nestingLevel the nesting level of the target type + * @return the generic type, or {@code null} if none + */ + private static Class<?> getGenericReturnType(Method method, Class<?> source, int typeIndex, int nestingLevel) { + return extractType(method.getGenericReturnType(), source, typeIndex, null, null, nestingLevel, 1); + } + + /** + * Extract the generic type from the given Type object. + * @param type the Type to check + * @param source the source collection/map Class that we check + * @param typeIndex the index of the actual type argument + * @param nestingLevel the nesting level of the target type + * @param currentLevel the current nested level + * @return the generic type as Class, or {@code null} if none + */ + private static Class<?> extractType(Type type, Class<?> source, int typeIndex, + Map<TypeVariable, Type> typeVariableMap, Map<Integer, Integer> typeIndexesPerLevel, + int nestingLevel, int currentLevel) { + + Type resolvedType = type; + if (type instanceof TypeVariable && typeVariableMap != null) { + Type mappedType = typeVariableMap.get(type); + if (mappedType != null) { + resolvedType = mappedType; + } + } + if (resolvedType instanceof ParameterizedType) { + return extractTypeFromParameterizedType((ParameterizedType) resolvedType, source, typeIndex, typeVariableMap, typeIndexesPerLevel, + nestingLevel, currentLevel); + } + else if (resolvedType instanceof Class) { + return extractTypeFromClass((Class) resolvedType, source, typeIndex, typeVariableMap, typeIndexesPerLevel, + nestingLevel, currentLevel); + } + else if (resolvedType instanceof GenericArrayType) { + Type compType = ((GenericArrayType) resolvedType).getGenericComponentType(); + return extractType(compType, source, typeIndex, typeVariableMap, typeIndexesPerLevel, nestingLevel, currentLevel + 1); + } + else { + return null; + } + } + + /** + * Extract the generic type from the given ParameterizedType object. + * @param ptype the ParameterizedType to check + * @param source the expected raw source type (can be {@code null}) + * @param typeIndex the index of the actual type argument + * @param nestingLevel the nesting level of the target type + * @param currentLevel the current nested level + * @return the generic type as Class, or {@code null} if none + */ + private static Class<?> extractTypeFromParameterizedType(ParameterizedType ptype, Class<?> source, int typeIndex, + Map<TypeVariable, Type> typeVariableMap, Map<Integer, Integer> typeIndexesPerLevel, + int nestingLevel, int currentLevel) { + + if (!(ptype.getRawType() instanceof Class)) { + return null; + } + Class rawType = (Class) ptype.getRawType(); + Type[] paramTypes = ptype.getActualTypeArguments(); + if (nestingLevel - currentLevel > 0) { + int nextLevel = currentLevel + 1; + Integer currentTypeIndex = (typeIndexesPerLevel != null ? typeIndexesPerLevel.get(nextLevel) : null); + // Default is last parameter type: Collection element or Map value. + int indexToUse = (currentTypeIndex != null ? currentTypeIndex : paramTypes.length - 1); + Type paramType = paramTypes[indexToUse]; + return extractType(paramType, source, typeIndex, typeVariableMap, typeIndexesPerLevel, nestingLevel, nextLevel); + } + if (source != null && !source.isAssignableFrom(rawType)) { + return null; + } + Class fromSuperclassOrInterface = extractTypeFromClass(rawType, source, typeIndex, typeVariableMap, typeIndexesPerLevel, + nestingLevel, currentLevel); + if (fromSuperclassOrInterface != null) { + return fromSuperclassOrInterface; + } + if (paramTypes == null || typeIndex >= paramTypes.length) { + return null; + } + Type paramType = paramTypes[typeIndex]; + if (paramType instanceof TypeVariable && typeVariableMap != null) { + Type mappedType = typeVariableMap.get(paramType); + if (mappedType != null) { + paramType = mappedType; + } + } + if (paramType instanceof WildcardType) { + WildcardType wildcardType = (WildcardType) paramType; + Type[] upperBounds = wildcardType.getUpperBounds(); + if (upperBounds != null && upperBounds.length > 0 && !Object.class.equals(upperBounds[0])) { + paramType = upperBounds[0]; + } + else { + Type[] lowerBounds = wildcardType.getLowerBounds(); + if (lowerBounds != null && lowerBounds.length > 0 && !Object.class.equals(lowerBounds[0])) { + paramType = lowerBounds[0]; + } + } + } + if (paramType instanceof ParameterizedType) { + paramType = ((ParameterizedType) paramType).getRawType(); + } + if (paramType instanceof GenericArrayType) { + // A generic array type... Let's turn it into a straight array type if possible. + Type compType = ((GenericArrayType) paramType).getGenericComponentType(); + if (compType instanceof Class) { + return Array.newInstance((Class) compType, 0).getClass(); + } + } + else if (paramType instanceof Class) { + // We finally got a straight Class... + return (Class) paramType; + } + return null; + } + + /** + * Extract the generic type from the given Class object. + * @param clazz the Class to check + * @param source the expected raw source type (can be {@code null}) + * @param typeIndex the index of the actual type argument + * @return the generic type as Class, or {@code null} if none + */ + private static Class<?> extractTypeFromClass(Class<?> clazz, Class<?> source, int typeIndex) { + return extractTypeFromClass(clazz, source, typeIndex, null, null, 1, 1); + } + + /** + * Extract the generic type from the given Class object. + * @param clazz the Class to check + * @param source the expected raw source type (can be {@code null}) + * @param typeIndex the index of the actual type argument + * @param nestingLevel the nesting level of the target type + * @param currentLevel the current nested level + * @return the generic type as Class, or {@code null} if none + */ + private static Class<?> extractTypeFromClass(Class<?> clazz, Class<?> source, int typeIndex, + Map<TypeVariable, Type> typeVariableMap, Map<Integer, Integer> typeIndexesPerLevel, + int nestingLevel, int currentLevel) { + + if (clazz.getName().startsWith("java.util.")) { + return null; + } + if (clazz.getSuperclass() != null && isIntrospectionCandidate(clazz.getSuperclass())) { + try { + return extractType(clazz.getGenericSuperclass(), source, typeIndex, typeVariableMap, + typeIndexesPerLevel, nestingLevel, currentLevel); + } + catch (MalformedParameterizedTypeException ex) { + // from getGenericSuperclass() - ignore and continue with interface introspection + } + } + Type[] ifcs = clazz.getGenericInterfaces(); + if (ifcs != null) { + for (Type ifc : ifcs) { + Type rawType = ifc; + if (ifc instanceof ParameterizedType) { + rawType = ((ParameterizedType) ifc).getRawType(); + } + if (rawType instanceof Class && isIntrospectionCandidate((Class) rawType)) { + return extractType(ifc, source, typeIndex, typeVariableMap, typeIndexesPerLevel, nestingLevel, currentLevel); + } + } + } + return null; + } + + /** + * Determine whether the given class is a potential candidate + * that defines generic collection or map types. + * @param clazz the class to check + * @return whether the given class is assignable to Collection or Map + */ + private static boolean isIntrospectionCandidate(Class clazz) { + return (Collection.class.isAssignableFrom(clazz) || Map.class.isAssignableFrom(clazz)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java new file mode 100644 index 00000000..b860aaaf --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java @@ -0,0 +1,512 @@ +/* + * Copyright 2002-2013 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.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.MalformedParameterizedTypeException; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Helper class for resolving generic types against type variables. + * + * <p>Mainly intended for usage within the framework, resolving method + * parameter types even when they are declared generically. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Sam Brannen + * @since 2.5.2 + * @see GenericCollectionTypeResolver + */ +public abstract class GenericTypeResolver { + + /** Cache from Class to TypeVariable Map */ + private static final Map<Class, Map<TypeVariable, Type>> typeVariableCache = + new ConcurrentReferenceHashMap<Class, Map<TypeVariable,Type>>(); + + + /** + * Determine the target type for the given parameter specification. + * @param methodParam the method parameter specification + * @return the corresponding generic parameter type + */ + public static Type getTargetType(MethodParameter methodParam) { + Assert.notNull(methodParam, "MethodParameter must not be null"); + if (methodParam.getConstructor() != null) { + return methodParam.getConstructor().getGenericParameterTypes()[methodParam.getParameterIndex()]; + } + else { + if (methodParam.getParameterIndex() >= 0) { + return methodParam.getMethod().getGenericParameterTypes()[methodParam.getParameterIndex()]; + } + else { + return methodParam.getMethod().getGenericReturnType(); + } + } + } + + /** + * Determine the target type for the given generic parameter type. + * @param methodParam the method parameter specification + * @param clazz the class to resolve type variables against + * @return the corresponding generic parameter or return type + */ + public static Class<?> resolveParameterType(MethodParameter methodParam, Class<?> clazz) { + Type genericType = getTargetType(methodParam); + Assert.notNull(clazz, "Class must not be null"); + Map<TypeVariable, Type> typeVariableMap = getTypeVariableMap(clazz); + Type rawType = getRawType(genericType, typeVariableMap); + Class<?> result = (rawType instanceof Class ? (Class) rawType : methodParam.getParameterType()); + methodParam.setParameterType(result); + methodParam.typeVariableMap = typeVariableMap; + return result; + } + + /** + * Determine the target type for the generic return type of the given method, + * where formal type variables are declared on the given class. + * @param method the method to introspect + * @param clazz the class to resolve type variables against + * @return the corresponding generic parameter or return type + * @see #resolveReturnTypeForGenericMethod + */ + public static Class<?> resolveReturnType(Method method, Class<?> clazz) { + Assert.notNull(method, "Method must not be null"); + Type genericType = method.getGenericReturnType(); + Assert.notNull(clazz, "Class must not be null"); + Map<TypeVariable, Type> typeVariableMap = getTypeVariableMap(clazz); + Type rawType = getRawType(genericType, typeVariableMap); + return (rawType instanceof Class ? (Class<?>) rawType : method.getReturnType()); + } + + /** + * Determine the target type for the generic return type of the given + * <em>generic method</em>, where formal type variables are declared on + * the given method itself. + * <p>For example, given a factory method with the following signature, + * if {@code resolveReturnTypeForGenericMethod()} is invoked with the reflected + * method for {@code creatProxy()} and an {@code Object[]} array containing + * {@code MyService.class}, {@code resolveReturnTypeForGenericMethod()} will + * infer that the target return type is {@code MyService}. + * <pre>{@code public static <T> T createProxy(Class<T> clazz)}</pre> + * <h4>Possible Return Values</h4> + * <ul> + * <li>the target return type, if it can be inferred</li> + * <li>the {@linkplain Method#getReturnType() standard return type}, if + * the given {@code method} does not declare any {@linkplain + * Method#getTypeParameters() formal type variables}</li> + * <li>the {@linkplain Method#getReturnType() standard return type}, if the + * target return type cannot be inferred (e.g., due to type erasure)</li> + * <li>{@code null}, if the length of the given arguments array is shorter + * than the length of the {@linkplain + * Method#getGenericParameterTypes() formal argument list} for the given + * method</li> + * </ul> + * @param method the method to introspect, never {@code null} + * @param args the arguments that will be supplied to the method when it is + * invoked, never {@code null} + * @return the resolved target return type, the standard return type, or {@code null} + * @since 3.2 + * @deprecated in favor of resolveReturnTypeForFactoryMethod in the internal + * AutowireUtils class in the beans module; we do not expect other use of it! + */ + @Deprecated + public static Class<?> resolveReturnTypeForGenericMethod(Method method, Object[] args) { + Assert.notNull(method, "Method must not be null"); + Assert.notNull(args, "Argument array must not be null"); + + TypeVariable<Method>[] declaredTypeVariables = method.getTypeParameters(); + Type genericReturnType = method.getGenericReturnType(); + Type[] methodArgumentTypes = method.getGenericParameterTypes(); + + // No declared type variables to inspect, so just return the standard return type. + if (declaredTypeVariables.length == 0) { + return method.getReturnType(); + } + + // The supplied argument list is too short for the method's signature, so + // return null, since such a method invocation would fail. + if (args.length < methodArgumentTypes.length) { + return null; + } + + // Ensure that the type variable (e.g., T) is declared directly on the method + // itself (e.g., via <T>), not on the enclosing class or interface. + boolean locallyDeclaredTypeVariableMatchesReturnType = false; + for (TypeVariable<Method> currentTypeVariable : declaredTypeVariables) { + if (currentTypeVariable.equals(genericReturnType)) { + locallyDeclaredTypeVariableMatchesReturnType = true; + break; + } + } + + if (locallyDeclaredTypeVariableMatchesReturnType) { + for (int i = 0; i < methodArgumentTypes.length; i++) { + Type currentMethodArgumentType = methodArgumentTypes[i]; + if (currentMethodArgumentType.equals(genericReturnType)) { + return args[i].getClass(); + } + if (currentMethodArgumentType instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) currentMethodArgumentType; + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + for (Type typeArg : actualTypeArguments) { + if (typeArg.equals(genericReturnType)) { + if (args[i] instanceof Class) { + return (Class<?>) args[i]; + } + else { + // Consider adding logic to determine the class of the typeArg, if possible. + // For now, just fall back... + return method.getReturnType(); + } + } + } + } + } + } + + // Fall back... + return method.getReturnType(); + } + + /** + * Resolve the single type argument of the given generic interface against the given + * target method which is assumed to return the given interface or an implementation + * of it. + * @param method the target method to check the return type of + * @param genericIfc the generic interface or superclass to resolve the type argument from + * @return the resolved parameter type of the method return type, or {@code null} + * if not resolvable or if the single argument is of type {@link WildcardType}. + */ + public static Class<?> resolveReturnTypeArgument(Method method, Class<?> genericIfc) { + Assert.notNull(method, "method must not be null"); + Type returnType = method.getReturnType(); + Type genericReturnType = method.getGenericReturnType(); + if (returnType.equals(genericIfc)) { + if (genericReturnType instanceof ParameterizedType) { + ParameterizedType targetType = (ParameterizedType) genericReturnType; + Type[] actualTypeArguments = targetType.getActualTypeArguments(); + Type typeArg = actualTypeArguments[0]; + if (!(typeArg instanceof WildcardType)) { + return (Class<?>) typeArg; + } + } + else { + return null; + } + } + return resolveTypeArgument((Class<?>) returnType, genericIfc); + } + + /** + * Resolve the single type argument of the given generic interface against + * the given target class which is assumed to implement the generic interface + * and possibly declare a concrete type for its type variable. + * @param clazz the target class to check against + * @param genericIfc the generic interface or superclass to resolve the type argument from + * @return the resolved type of the argument, or {@code null} if not resolvable + */ + public static Class<?> resolveTypeArgument(Class<?> clazz, Class<?> genericIfc) { + Class<?>[] typeArgs = resolveTypeArguments(clazz, genericIfc); + if (typeArgs == null) { + return null; + } + if (typeArgs.length != 1) { + throw new IllegalArgumentException("Expected 1 type argument on generic interface [" + + genericIfc.getName() + "] but found " + typeArgs.length); + } + return typeArgs[0]; + } + + /** + * Resolve the type arguments of the given generic interface against the given + * target class which is assumed to implement the generic interface and possibly + * declare concrete types for its type variables. + * <p>Note: In Spring 3.2, this method doesn't return {@code null} in all scenarios + * where it should. To be fixed in Spring 4.0; for client code, this just means it + * might see {@code null} in a few more cases then where it now sees an array with + * a single {@link Object} type. + * @param clazz the target class to check against + * @param genericIfc the generic interface or superclass to resolve the type argument from + * @return the resolved type of each argument, with the array size matching the + * number of actual type arguments, or {@code null} if not resolvable + */ + public static Class<?>[] resolveTypeArguments(Class<?> clazz, Class<?> genericIfc) { + return doResolveTypeArguments(clazz, clazz, genericIfc); + } + + private static Class<?>[] doResolveTypeArguments(Class<?> ownerClass, Class<?> classToIntrospect, Class<?> genericIfc) { + while (classToIntrospect != null) { + if (genericIfc.isInterface()) { + Type[] ifcs = classToIntrospect.getGenericInterfaces(); + for (Type ifc : ifcs) { + Class<?>[] result = doResolveTypeArguments(ownerClass, ifc, genericIfc); + if (result != null) { + return result; + } + } + } + else { + try { + Class<?>[] result = doResolveTypeArguments(ownerClass, classToIntrospect.getGenericSuperclass(), genericIfc); + if (result != null) { + return result; + } + } + catch (MalformedParameterizedTypeException ex) { + // from getGenericSuperclass() - return null to skip further superclass traversal + return null; + } + } + classToIntrospect = classToIntrospect.getSuperclass(); + } + return null; + } + + private static Class<?>[] doResolveTypeArguments(Class<?> ownerClass, Type ifc, Class<?> genericIfc) { + if (ifc instanceof ParameterizedType) { + ParameterizedType paramIfc = (ParameterizedType) ifc; + Type rawType = paramIfc.getRawType(); + if (genericIfc.equals(rawType)) { + Type[] typeArgs = paramIfc.getActualTypeArguments(); + Class<?>[] result = new Class[typeArgs.length]; + for (int i = 0; i < typeArgs.length; i++) { + Type arg = typeArgs[i]; + result[i] = extractClass(ownerClass, arg); + } + return result; + } + else if (genericIfc.isAssignableFrom((Class) rawType)) { + return doResolveTypeArguments(ownerClass, (Class) rawType, genericIfc); + } + } + else if (ifc != null && genericIfc.isAssignableFrom((Class) ifc)) { + return doResolveTypeArguments(ownerClass, (Class) ifc, genericIfc); + } + return null; + } + + /** + * Extract a Class from the given Type. + */ + private static Class<?> extractClass(Class<?> ownerClass, Type arg) { + if (arg instanceof ParameterizedType) { + return extractClass(ownerClass, ((ParameterizedType) arg).getRawType()); + } + else if (arg instanceof GenericArrayType) { + GenericArrayType gat = (GenericArrayType) arg; + Type gt = gat.getGenericComponentType(); + Class<?> componentClass = extractClass(ownerClass, gt); + return Array.newInstance(componentClass, 0).getClass(); + } + else if (arg instanceof TypeVariable) { + TypeVariable tv = (TypeVariable) arg; + arg = getTypeVariableMap(ownerClass).get(tv); + if (arg == null) { + arg = extractBoundForTypeVariable(tv); + if (arg instanceof ParameterizedType) { + return extractClass(ownerClass, ((ParameterizedType) arg).getRawType()); + } + } + else { + return extractClass(ownerClass, arg); + } + } + return (arg instanceof Class ? (Class) arg : Object.class); + } + + /** + * Resolve the specified generic type against the given TypeVariable map. + * @param genericType the generic type to resolve + * @param typeVariableMap the TypeVariable Map to resolved against + * @return the type if it resolves to a Class, or {@code Object.class} otherwise + */ + public static Class<?> resolveType(Type genericType, Map<TypeVariable, Type> typeVariableMap) { + Type resolvedType = getRawType(genericType, typeVariableMap); + if (resolvedType instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) resolvedType).getGenericComponentType(); + Class<?> componentClass = resolveType(componentType, typeVariableMap); + resolvedType = Array.newInstance(componentClass, 0).getClass(); + } + return (resolvedType instanceof Class ? (Class) resolvedType : Object.class); + } + + /** + * Determine the raw type for the given generic parameter type. + * @param genericType the generic type to resolve + * @param typeVariableMap the TypeVariable Map to resolved against + * @return the resolved raw type + */ + static Type getRawType(Type genericType, Map<TypeVariable, Type> typeVariableMap) { + Type resolvedType = genericType; + if (genericType instanceof TypeVariable) { + TypeVariable tv = (TypeVariable) genericType; + resolvedType = typeVariableMap.get(tv); + if (resolvedType == null) { + resolvedType = extractBoundForTypeVariable(tv); + } + } + if (resolvedType instanceof ParameterizedType) { + return ((ParameterizedType) resolvedType).getRawType(); + } + else { + return resolvedType; + } + } + + /** + * Build a mapping of {@link TypeVariable#getName TypeVariable names} to + * {@link Class concrete classes} for the specified {@link Class}. Searches + * all super types, enclosing types and interfaces. + */ + public static Map<TypeVariable, Type> getTypeVariableMap(Class<?> clazz) { + Map<TypeVariable, Type> typeVariableMap = typeVariableCache.get(clazz); + + if (typeVariableMap == null) { + typeVariableMap = new HashMap<TypeVariable, Type>(); + + // interfaces + extractTypeVariablesFromGenericInterfaces(clazz.getGenericInterfaces(), typeVariableMap); + + try { + // super class + Class<?> type = clazz; + while (type.getSuperclass() != null && !Object.class.equals(type.getSuperclass())) { + Type genericType = type.getGenericSuperclass(); + if (genericType instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) genericType; + populateTypeMapFromParameterizedType(pt, typeVariableMap); + } + extractTypeVariablesFromGenericInterfaces(type.getSuperclass().getGenericInterfaces(), typeVariableMap); + type = type.getSuperclass(); + } + } + catch (MalformedParameterizedTypeException ex) { + // from getGenericSuperclass() - ignore and continue with member class check + } + + try { + // enclosing class + Class<?> type = clazz; + while (type.isMemberClass()) { + Type genericType = type.getGenericSuperclass(); + if (genericType instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) genericType; + populateTypeMapFromParameterizedType(pt, typeVariableMap); + } + type = type.getEnclosingClass(); + } + } + catch (MalformedParameterizedTypeException ex) { + // from getGenericSuperclass() - ignore and preserve previously accumulated type variables + } + + typeVariableCache.put(clazz, typeVariableMap); + } + + return typeVariableMap; + } + + /** + * Extracts the bound {@code Type} for a given {@link TypeVariable}. + */ + static Type extractBoundForTypeVariable(TypeVariable typeVariable) { + Type[] bounds = typeVariable.getBounds(); + if (bounds.length == 0) { + return Object.class; + } + Type bound = bounds[0]; + if (bound instanceof TypeVariable) { + bound = extractBoundForTypeVariable((TypeVariable) bound); + } + return bound; + } + + private static void extractTypeVariablesFromGenericInterfaces(Type[] genericInterfaces, Map<TypeVariable, Type> typeVariableMap) { + for (Type genericInterface : genericInterfaces) { + if (genericInterface instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) genericInterface; + populateTypeMapFromParameterizedType(pt, typeVariableMap); + if (pt.getRawType() instanceof Class) { + extractTypeVariablesFromGenericInterfaces( + ((Class) pt.getRawType()).getGenericInterfaces(), typeVariableMap); + } + } + else if (genericInterface instanceof Class) { + extractTypeVariablesFromGenericInterfaces( + ((Class) genericInterface).getGenericInterfaces(), typeVariableMap); + } + } + } + + /** + * Read the {@link TypeVariable TypeVariables} from the supplied {@link ParameterizedType} + * and add mappings corresponding to the {@link TypeVariable#getName TypeVariable name} -> + * concrete type to the supplied {@link Map}. + * <p>Consider this case: + * <pre class="code> + * public interface Foo<S, T> { + * .. + * } + * + * public class FooImpl implements Foo<String, Integer> { + * .. + * }</pre> + * For '{@code FooImpl}' the following mappings would be added to the {@link Map}: + * {S=java.lang.String, T=java.lang.Integer}. + */ + private static void populateTypeMapFromParameterizedType(ParameterizedType type, Map<TypeVariable, Type> typeVariableMap) { + if (type.getRawType() instanceof Class) { + Type[] actualTypeArguments = type.getActualTypeArguments(); + TypeVariable[] typeVariables = ((Class) type.getRawType()).getTypeParameters(); + for (int i = 0; i < actualTypeArguments.length; i++) { + Type actualTypeArgument = actualTypeArguments[i]; + TypeVariable variable = typeVariables[i]; + if (actualTypeArgument instanceof Class) { + typeVariableMap.put(variable, actualTypeArgument); + } + else if (actualTypeArgument instanceof GenericArrayType) { + typeVariableMap.put(variable, actualTypeArgument); + } + else if (actualTypeArgument instanceof ParameterizedType) { + typeVariableMap.put(variable, actualTypeArgument); + } + else if (actualTypeArgument instanceof TypeVariable) { + // We have a type that is parameterized at instantiation time + // the nearest match on the bridge method will be the bounded type. + TypeVariable typeVariableArgument = (TypeVariable) actualTypeArgument; + Type resolvedType = typeVariableMap.get(typeVariableArgument); + if (resolvedType == null) { + resolvedType = extractBoundForTypeVariable(typeVariableArgument); + } + typeVariableMap.put(variable, resolvedType); + } + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/InfrastructureProxy.java b/spring-core/src/main/java/org/springframework/core/InfrastructureProxy.java new file mode 100644 index 00000000..18f90286 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/InfrastructureProxy.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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 transparent resource proxies that need to be + * considered as equal to the underlying resource, for example for consistent + * lookup key comparisons. Note that this interface does imply such special + * semantics and does not constitute a general-purpose mixin! + * + * <p>Such wrappers will automatically be unwrapped for key comparisons in + * {@link org.springframework.transaction.support.TransactionSynchronizationManager}. + * + * <p>Only fully transparent proxies, e.g. for redirection or service lookups, + * are supposed to implement this interface. Proxies that decorate the target + * object with new behavior, such as AOP proxies, do <i>not</i> qualify here! + * + * @author Juergen Hoeller + * @since 2.5.4 + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public interface InfrastructureProxy { + + /** + * Return the underlying resource (never {@code null}). + */ + Object getWrappedObject(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/JdkVersion.java b/spring-core/src/main/java/org/springframework/core/JdkVersion.java new file mode 100644 index 00000000..a77f75ff --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/JdkVersion.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2013 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; + +/** + * Internal helper class used to find the Java/JVM version + * that Spring is operating on, to allow for automatically + * adapting to the present platform's capabilities. + * + * <p>Note that Spring requires JVM 1.5 or higher, as of Spring 3.0. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rick Evans + */ +public abstract class JdkVersion { + + /** + * Constant identifying the 1.3.x JVM (JDK 1.3). + */ + public static final int JAVA_13 = 0; + + /** + * Constant identifying the 1.4.x JVM (J2SE 1.4). + */ + public static final int JAVA_14 = 1; + + /** + * Constant identifying the 1.5 JVM (Java 5). + */ + public static final int JAVA_15 = 2; + + /** + * Constant identifying the 1.6 JVM (Java 6). + */ + public static final int JAVA_16 = 3; + + /** + * Constant identifying the 1.7 JVM (Java 7). + */ + public static final int JAVA_17 = 4; + + /** + * Constant identifying the 1.8 JVM (Java 8). + */ + public static final int JAVA_18 = 5; + + + private static final String javaVersion; + + private static final int majorJavaVersion; + + static { + javaVersion = System.getProperty("java.version"); + // version String should look like "1.4.2_10" + if (javaVersion.contains("1.8.")) { + majorJavaVersion = JAVA_18; + } + else if (javaVersion.contains("1.7.")) { + majorJavaVersion = JAVA_17; + } + else if (javaVersion.contains("1.6.")) { + majorJavaVersion = JAVA_16; + } + else { + // else leave 1.5 as default (it's either 1.5 or unknown) + majorJavaVersion = JAVA_15; + } + } + + + /** + * Return the full Java version string, as returned by + * {@code System.getProperty("java.version")}. + * @return the full Java version string + * @see System#getProperty(String) + */ + public static String getJavaVersion() { + return javaVersion; + } + + /** + * Get the major version code. This means we can do things like + * {@code if (getMajorJavaVersion() >= JAVA_17)}. + * @return a code comparable to the JAVA_XX codes in this class + * @see #JAVA_13 + * @see #JAVA_14 + * @see #JAVA_15 + * @see #JAVA_16 + * @see #JAVA_17 + */ + public static int getMajorJavaVersion() { + return majorJavaVersion; + } + + + /** + * Convenience method to determine if the current JVM is at least Java 1.4. + * @return {@code true} if the current JVM is at least Java 1.4 + * @deprecated as of Spring 3.0 which requires Java 1.5+ + * @see #getMajorJavaVersion() + * @see #JAVA_14 + * @see #JAVA_15 + * @see #JAVA_16 + * @see #JAVA_17 + */ + @Deprecated + public static boolean isAtLeastJava14() { + return true; + } + + /** + * Convenience method to determine if the current JVM is at least + * Java 1.5 (Java 5). + * @return {@code true} if the current JVM is at least Java 1.5 + * @deprecated as of Spring 3.0 which requires Java 1.5+ + * @see #getMajorJavaVersion() + * @see #JAVA_15 + * @see #JAVA_16 + * @see #JAVA_17 + */ + @Deprecated + public static boolean isAtLeastJava15() { + return true; + } + + /** + * Convenience method to determine if the current JVM is at least + * Java 1.6 (Java 6). + * @return {@code true} if the current JVM is at least Java 1.6 + * @deprecated as of Spring 3.0, in favor of reflective checks for + * the specific Java 1.6 classes of interest + * @see #getMajorJavaVersion() + * @see #JAVA_16 + * @see #JAVA_17 + */ + @Deprecated + public static boolean isAtLeastJava16() { + return (majorJavaVersion >= JAVA_16); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java new file mode 100644 index 00000000..151dcb30 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java @@ -0,0 +1,272 @@ +/* + * Copyright 2002-2014 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.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.asm.ClassReader; +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.asm.Type; +import org.springframework.util.ClassUtils; + +/** + * Implementation of {@link ParameterNameDiscoverer} that uses the LocalVariableTable + * information in the method attributes to discover parameter names. Returns + * {@code null} if the class file was compiled without debug information. + * + * <p>Uses ObjectWeb's ASM library for analyzing class files. Each discoverer instance + * caches the ASM discovered information for each introspected Class, in a thread-safe + * manner. It is recommended to reuse ParameterNameDiscoverer instances as far as possible. + * + * @author Adrian Colyer + * @author Costin Leau + * @author Juergen Hoeller + * @author Chris Beams + * @since 2.0 + */ +public class LocalVariableTableParameterNameDiscoverer implements ParameterNameDiscoverer { + + private static final Log logger = LogFactory.getLog(LocalVariableTableParameterNameDiscoverer.class); + + // marker object for classes that do not have any debug info + private static final Map<Member, String[]> NO_DEBUG_INFO_MAP = Collections.emptyMap(); + + // the cache uses a nested index (value is a map) to keep the top level cache relatively small in size + private final Map<Class<?>, Map<Member, String[]>> parameterNamesCache = + new ConcurrentHashMap<Class<?>, Map<Member, String[]>>(32); + + + public String[] getParameterNames(Method method) { + Method originalMethod = BridgeMethodResolver.findBridgedMethod(method); + Class<?> declaringClass = originalMethod.getDeclaringClass(); + Map<Member, String[]> map = this.parameterNamesCache.get(declaringClass); + if (map == null) { + map = inspectClass(declaringClass); + this.parameterNamesCache.put(declaringClass, map); + } + if (map != NO_DEBUG_INFO_MAP) { + return map.get(originalMethod); + } + return null; + } + + public String[] getParameterNames(Constructor<?> ctor) { + Class<?> declaringClass = ctor.getDeclaringClass(); + Map<Member, String[]> map = this.parameterNamesCache.get(declaringClass); + if (map == null) { + map = inspectClass(declaringClass); + this.parameterNamesCache.put(declaringClass, map); + } + if (map != NO_DEBUG_INFO_MAP) { + return map.get(ctor); + } + return null; + } + + /** + * Inspects the target class. Exceptions will be logged and a maker map returned + * to indicate the lack of debug information. + */ + private Map<Member, String[]> inspectClass(Class<?> clazz) { + InputStream is = clazz.getResourceAsStream(ClassUtils.getClassFileName(clazz)); + if (is == null) { + // We couldn't load the class file, which is not fatal as it + // simply means this method of discovering parameter names won't work. + if (logger.isDebugEnabled()) { + logger.debug("Cannot find '.class' file for class [" + clazz + + "] - unable to determine constructor/method parameter names"); + } + return NO_DEBUG_INFO_MAP; + } + try { + ClassReader classReader = new ClassReader(is); + Map<Member, String[]> map = new ConcurrentHashMap<Member, String[]>(32); + classReader.accept(new ParameterNameDiscoveringVisitor(clazz, map), 0); + return map; + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Exception thrown while reading '.class' file for class [" + clazz + + "] - unable to determine constructor/method parameter names", ex); + } + } + catch (IllegalArgumentException ex) { + if (logger.isDebugEnabled()) { + logger.debug("ASM ClassReader failed to parse class file [" + clazz + + "], probably due to a new Java class file version that isn't supported yet " + + "- unable to determine constructor/method parameter names", ex); + } + } + finally { + try { + is.close(); + } + catch (IOException ex) { + // ignore + } + } + return NO_DEBUG_INFO_MAP; + } + + + /** + * Helper class that inspects all methods (constructor included) and then + * attempts to find the parameter names for that member. + */ + private static class ParameterNameDiscoveringVisitor extends ClassVisitor { + + private static final String STATIC_CLASS_INIT = "<clinit>"; + + private final Class<?> clazz; + + private final Map<Member, String[]> memberMap; + + public ParameterNameDiscoveringVisitor(Class<?> clazz, Map<Member, String[]> memberMap) { + super(SpringAsmInfo.ASM_VERSION); + this.clazz = clazz; + this.memberMap = memberMap; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + // exclude synthetic + bridged && static class initialization + if (!isSyntheticOrBridged(access) && !STATIC_CLASS_INIT.equals(name)) { + return new LocalVariableTableVisitor(clazz, memberMap, name, desc, isStatic(access)); + } + return null; + } + + private static boolean isSyntheticOrBridged(int access) { + return (((access & Opcodes.ACC_SYNTHETIC) | (access & Opcodes.ACC_BRIDGE)) > 0); + } + + private static boolean isStatic(int access) { + return ((access & Opcodes.ACC_STATIC) > 0); + } + } + + + private static class LocalVariableTableVisitor extends MethodVisitor { + + private static final String CONSTRUCTOR = "<init>"; + + private final Class<?> clazz; + + private final Map<Member, String[]> memberMap; + + private final String name; + + private final Type[] args; + + private final String[] parameterNames; + + private final boolean isStatic; + + private boolean hasLvtInfo = false; + + /* + * The nth entry contains the slot index of the LVT table entry holding the + * argument name for the nth parameter. + */ + private final int[] lvtSlotIndex; + + public LocalVariableTableVisitor(Class<?> clazz, Map<Member, String[]> map, String name, String desc, boolean isStatic) { + super(SpringAsmInfo.ASM_VERSION); + this.clazz = clazz; + this.memberMap = map; + this.name = name; + this.args = Type.getArgumentTypes(desc); + this.parameterNames = new String[this.args.length]; + this.isStatic = isStatic; + this.lvtSlotIndex = computeLvtSlotIndices(isStatic, this.args); + } + + @Override + public void visitLocalVariable(String name, String description, String signature, Label start, Label end, int index) { + this.hasLvtInfo = true; + for (int i = 0; i < this.lvtSlotIndex.length; i++) { + if (this.lvtSlotIndex[i] == index) { + this.parameterNames[i] = name; + } + } + } + + @Override + public void visitEnd() { + if (this.hasLvtInfo || (this.isStatic && this.parameterNames.length == 0)) { + // visitLocalVariable will never be called for static no args methods + // which doesn't use any local variables. + // This means that hasLvtInfo could be false for that kind of methods + // even if the class has local variable info. + this.memberMap.put(resolveMember(), this.parameterNames); + } + } + + private Member resolveMember() { + ClassLoader loader = this.clazz.getClassLoader(); + Class<?>[] argTypes = new Class<?>[this.args.length]; + for (int i = 0; i < this.args.length; i++) { + argTypes[i] = ClassUtils.resolveClassName(this.args[i].getClassName(), loader); + } + try { + if (CONSTRUCTOR.equals(this.name)) { + return this.clazz.getDeclaredConstructor(argTypes); + } + return this.clazz.getDeclaredMethod(this.name, argTypes); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("Method [" + this.name + + "] was discovered in the .class file but cannot be resolved in the class object", ex); + } + } + + private static int[] computeLvtSlotIndices(boolean isStatic, Type[] paramTypes) { + int[] lvtIndex = new int[paramTypes.length]; + int nextIndex = (isStatic ? 0 : 1); + for (int i = 0; i < paramTypes.length; i++) { + lvtIndex[i] = nextIndex; + if (isWideType(paramTypes[i])) { + nextIndex += 2; + } + else { + nextIndex++; + } + } + return lvtIndex; + } + + private static boolean isWideType(Type aType) { + // float is not a wide type + return (aType == Type.LONG_TYPE || aType == Type.DOUBLE_TYPE); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameter.java b/spring-core/src/main/java/org/springframework/core/MethodParameter.java new file mode 100644 index 00000000..1325db2a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -0,0 +1,455 @@ +/* + * Copyright 2002-2014 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.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Helper class that encapsulates the specification of a method parameter, i.e. + * a Method or Constructor plus a parameter index and a nested type index for + * a declared generic type. Useful as a specification object to pass along. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Andy Clement + * @since 2.0 + * @see GenericCollectionTypeResolver + */ +public class MethodParameter { + + private final Method method; + + private final Constructor<?> constructor; + + private final int parameterIndex; + + private Class<?> parameterType; + + private Type genericParameterType; + + private Annotation[] parameterAnnotations; + + private ParameterNameDiscoverer parameterNameDiscoverer; + + private String parameterName; + + private int nestingLevel = 1; + + /** Map from Integer level to Integer type index */ + Map<Integer, Integer> typeIndexesPerLevel; + + Map<TypeVariable, Type> typeVariableMap; + + + /** + * Create a new MethodParameter for the given method, with nesting level 1. + * @param method the Method to specify a parameter for + * @param parameterIndex the index of the parameter + */ + public MethodParameter(Method method, int parameterIndex) { + this(method, parameterIndex, 1); + } + + /** + * Create a new MethodParameter 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 MethodParameter(Method method, int parameterIndex, int nestingLevel) { + Assert.notNull(method, "Method must not be null"); + this.method = method; + this.parameterIndex = parameterIndex; + this.nestingLevel = nestingLevel; + this.constructor = null; + } + + /** + * Create a new MethodParameter 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 MethodParameter(Constructor<?> constructor, int parameterIndex) { + this(constructor, parameterIndex, 1); + } + + /** + * Create a new MethodParameter 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 MethodParameter(Constructor<?> constructor, int parameterIndex, int nestingLevel) { + Assert.notNull(constructor, "Constructor must not be null"); + this.constructor = constructor; + this.parameterIndex = parameterIndex; + this.nestingLevel = nestingLevel; + this.method = null; + } + + /** + * Copy constructor, resulting in an independent MethodParameter object + * based on the same metadata and cache state that the original object was in. + * @param original the original MethodParameter object to copy from + */ + public MethodParameter(MethodParameter original) { + Assert.notNull(original, "Original must not be null"); + this.method = original.method; + this.constructor = original.constructor; + this.parameterIndex = original.parameterIndex; + this.parameterType = original.parameterType; + this.genericParameterType = original.genericParameterType; + this.parameterAnnotations = original.parameterAnnotations; + this.parameterNameDiscoverer = original.parameterNameDiscoverer; + this.parameterName = original.parameterName; + this.nestingLevel = original.nestingLevel; + this.typeIndexesPerLevel = original.typeIndexesPerLevel; + this.typeVariableMap = original.typeVariableMap; + } + + + /** + * Return the wrapped Method, if any. + * <p>Note: Either Method or Constructor is available. + * @return the Method, or {@code null} if none + */ + public Method getMethod() { + return this.method; + } + + /** + * Return the wrapped Constructor, if any. + * <p>Note: Either Method or Constructor is available. + * @return the Constructor, or {@code null} if none + */ + public Constructor<?> getConstructor() { + return this.constructor; + } + + /** + * Returns the wrapped member. + * @return the Method or Constructor as Member + */ + private Member getMember() { + return (this.method != null ? this.method : this.constructor); + } + + /** + * Returns the wrapped annotated element. + * @return the Method or Constructor as AnnotatedElement + */ + private AnnotatedElement getAnnotatedElement() { + return (this.method != null ? this.method : this.constructor); + } + + /** + * Return the class that declares the underlying Method or Constructor. + */ + public Class<?> getDeclaringClass() { + return getMember().getDeclaringClass(); + } + + /** + * Return the index of the method/constructor parameter. + * @return the parameter index (never negative) + */ + public int getParameterIndex() { + return this.parameterIndex; + } + + /** + * Set a resolved (generic) parameter type. + */ + void setParameterType(Class<?> parameterType) { + this.parameterType = parameterType; + } + + /** + * Return the type of the method/constructor parameter. + * @return the parameter type (never {@code null}) + */ + public Class<?> getParameterType() { + if (this.parameterType == null) { + if (this.parameterIndex < 0) { + this.parameterType = (this.method != null ? this.method.getReturnType() : null); + } + else { + this.parameterType = (this.method != null ? + this.method.getParameterTypes()[this.parameterIndex] : + this.constructor.getParameterTypes()[this.parameterIndex]); + } + } + return this.parameterType; + } + + /** + * Return the generic type of the method/constructor parameter. + * @return the parameter type (never {@code null}) + */ + public Type getGenericParameterType() { + if (this.genericParameterType == null) { + if (this.parameterIndex < 0) { + this.genericParameterType = (this.method != null ? this.method.getGenericReturnType() : null); + } + else { + this.genericParameterType = (this.method != null ? + this.method.getGenericParameterTypes()[this.parameterIndex] : + this.constructor.getGenericParameterTypes()[this.parameterIndex]); + } + } + return this.genericParameterType; + } + + public Class<?> getNestedParameterType() { + if (this.nestingLevel > 1) { + Type type = getGenericParameterType(); + if (type instanceof ParameterizedType) { + Integer index = getTypeIndexForCurrentLevel(); + Type arg = ((ParameterizedType) type).getActualTypeArguments()[index != null ? index : 0]; + if (arg instanceof Class) { + return (Class<?>) arg; + } + else if (arg instanceof ParameterizedType) { + arg = ((ParameterizedType) arg).getRawType(); + if (arg instanceof Class) { + return (Class<?>) arg; + } + } + } + return Object.class; + } + else { + return getParameterType(); + } + } + + /** + * Return the annotations associated with the target method/constructor itself. + */ + public Annotation[] getMethodAnnotations() { + return getAnnotatedElement().getAnnotations(); + } + + /** + * Return the method/constructor annotation of the given type, if available. + * @param annotationType the annotation type to look for + * @return the annotation object, or {@code null} if not found + */ + public <T extends Annotation> T getMethodAnnotation(Class<T> annotationType) { + return getAnnotatedElement().getAnnotation(annotationType); + } + + /** + * Return the annotations associated with the specific method/constructor parameter. + */ + public Annotation[] getParameterAnnotations() { + if (this.parameterAnnotations == null) { + Annotation[][] annotationArray = (this.method != null ? + this.method.getParameterAnnotations() : this.constructor.getParameterAnnotations()); + if (this.parameterIndex >= 0 && this.parameterIndex < annotationArray.length) { + this.parameterAnnotations = annotationArray[this.parameterIndex]; + } + else { + this.parameterAnnotations = new Annotation[0]; + } + } + return this.parameterAnnotations; + } + + /** + * Return the parameter annotation of the given type, if available. + * @param annotationType the annotation type to look for + * @return the annotation object, or {@code null} if not found + */ + @SuppressWarnings("unchecked") + public <T extends Annotation> T getParameterAnnotation(Class<T> annotationType) { + Annotation[] anns = getParameterAnnotations(); + for (Annotation ann : anns) { + if (annotationType.isInstance(ann)) { + return (T) ann; + } + } + return null; + } + + /** + * Return true if the parameter has at least one annotation, false if it has none. + */ + public boolean hasParameterAnnotations() { + return (getParameterAnnotations().length != 0); + } + + /** + * Return true if the parameter has the given annotation type, and false if it doesn't. + */ + public <T extends Annotation> boolean hasParameterAnnotation(Class<T> annotationType) { + return (getParameterAnnotation(annotationType) != null); + } + + /** + * Initialize parameter name discovery for this method parameter. + * <p>This method does not actually try to retrieve the parameter name at + * this point; it just allows discovery to happen when the application calls + * {@link #getParameterName()} (if ever). + */ + public void initParameterNameDiscovery(ParameterNameDiscoverer parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + /** + * Return the name of the method/constructor parameter. + * @return the parameter name (may be {@code null} if no + * parameter name metadata is contained in the class file or no + * {@link #initParameterNameDiscovery ParameterNameDiscoverer} + * has been set to begin with) + */ + public String getParameterName() { + if (this.parameterNameDiscoverer != null) { + String[] parameterNames = (this.method != null ? + this.parameterNameDiscoverer.getParameterNames(this.method) : + this.parameterNameDiscoverer.getParameterNames(this.constructor)); + if (parameterNames != null) { + this.parameterName = parameterNames[this.parameterIndex]; + } + this.parameterNameDiscoverer = null; + } + return this.parameterName; + } + + /** + * Increase this parameter's nesting level. + * @see #getNestingLevel() + */ + public void increaseNestingLevel() { + this.nestingLevel++; + } + + /** + * Decrease this parameter's nesting level. + * @see #getNestingLevel() + */ + public void decreaseNestingLevel() { + getTypeIndexesPerLevel().remove(this.nestingLevel); + this.nestingLevel--; + } + + /** + * Return 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 int getNestingLevel() { + return this.nestingLevel; + } + + /** + * Set the type index for the current nesting level. + * @param typeIndex the corresponding type index + * (or {@code null} for the default type index) + * @see #getNestingLevel() + */ + public void setTypeIndexForCurrentLevel(int typeIndex) { + getTypeIndexesPerLevel().put(this.nestingLevel, typeIndex); + } + + /** + * Return the type index for the current nesting level. + * @return the corresponding type index, or {@code null} + * if none specified (indicating the default type index) + * @see #getNestingLevel() + */ + public Integer getTypeIndexForCurrentLevel() { + return getTypeIndexForLevel(this.nestingLevel); + } + + /** + * Return the type index for the specified nesting level. + * @param nestingLevel the nesting level to check + * @return the corresponding type index, or {@code null} + * if none specified (indicating the default type index) + */ + public Integer getTypeIndexForLevel(int nestingLevel) { + return getTypeIndexesPerLevel().get(nestingLevel); + } + + /** + * Obtain the (lazily constructed) type-indexes-per-level Map. + */ + private Map<Integer, Integer> getTypeIndexesPerLevel() { + if (this.typeIndexesPerLevel == null) { + this.typeIndexesPerLevel = new HashMap<Integer, Integer>(4); + } + return this.typeIndexesPerLevel; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj != null && obj instanceof MethodParameter) { + MethodParameter other = (MethodParameter) obj; + return (this.parameterIndex == other.parameterIndex && getMember().equals(other.getMember())); + } + return false; + } + + @Override + public int hashCode() { + return (getMember().hashCode() * 31 + this.parameterIndex); + } + + + /** + * Create a new MethodParameter for the given method or constructor. + * <p>This is a convenience constructor for scenarios where a + * Method or Constructor reference is treated in a generic fashion. + * @param methodOrConstructor the Method or Constructor to specify a parameter for + * @param parameterIndex the index of the parameter + * @return the corresponding MethodParameter instance + */ + public static MethodParameter forMethodOrConstructor(Object methodOrConstructor, int parameterIndex) { + if (methodOrConstructor instanceof Method) { + return new MethodParameter((Method) methodOrConstructor, parameterIndex); + } + else if (methodOrConstructor instanceof Constructor) { + return new MethodParameter((Constructor<?>) methodOrConstructor, parameterIndex); + } + else { + throw new IllegalArgumentException( + "Given object [" + methodOrConstructor + "] is neither a Method nor a Constructor"); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/NamedInheritableThreadLocal.java b/spring-core/src/main/java/org/springframework/core/NamedInheritableThreadLocal.java new file mode 100644 index 00000000..3d30f246 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/NamedInheritableThreadLocal.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2008 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 org.springframework.util.Assert; + +/** + * {@link InheritableThreadLocal} subclass that exposes a specified name + * as {@link #toString()} result (allowing for introspection). + * + * @author Juergen Hoeller + * @since 2.5.2 + * @see NamedThreadLocal + */ +public class NamedInheritableThreadLocal<T> extends InheritableThreadLocal<T> { + + private final String name; + + + /** + * Create a new NamedInheritableThreadLocal with the given name. + * @param name a descriptive name for this ThreadLocal + */ + public NamedInheritableThreadLocal(String name) { + Assert.hasText(name, "Name must not be empty"); + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java b/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java new file mode 100644 index 00000000..4b2b8238 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2008 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 org.springframework.util.Assert; + +/** + * {@link ThreadLocal} subclass that exposes a specified name + * as {@link #toString()} result (allowing for introspection). + * + * @author Juergen Hoeller + * @since 2.5.2 + * @see NamedInheritableThreadLocal + */ +public class NamedThreadLocal<T> extends ThreadLocal<T> { + + private final String name; + + + /** + * Create a new NamedThreadLocal with the given name. + * @param name a descriptive name for this ThreadLocal + */ + public NamedThreadLocal(String name) { + Assert.hasText(name, "Name must not be empty"); + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/NestedCheckedException.java b/spring-core/src/main/java/org/springframework/core/NestedCheckedException.java new file mode 100644 index 00000000..8f72b5ea --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/NestedCheckedException.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2012 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; + +/** + * Handy class for wrapping checked {@code Exceptions} with a root cause. + * + * <p>This class is {@code abstract} to force the programmer to extend + * the class. {@code getMessage} will include nested exception + * information; {@code printStackTrace} and other like methods will + * delegate to the wrapped exception, if any. + * + * <p>The similarity between this class and the {@link NestedRuntimeException} + * class is unavoidable, as Java forces these two classes to have different + * superclasses (ah, the inflexibility of concrete inheritance!). + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getMessage + * @see #printStackTrace + * @see NestedRuntimeException + */ +public abstract class NestedCheckedException extends Exception { + + /** Use serialVersionUID from Spring 1.2 for interoperability */ + private static final long serialVersionUID = 7100714597678207546L; + + static { + // Eagerly load the NestedExceptionUtils class to avoid classloader deadlock + // issues on OSGi when calling getMessage(). Reported by Don Brown; SPR-5607. + NestedExceptionUtils.class.getName(); + } + + + /** + * Construct a {@code NestedCheckedException} with the specified detail message. + * @param msg the detail message + */ + public NestedCheckedException(String msg) { + super(msg); + } + + /** + * Construct a {@code NestedCheckedException} with the specified detail message + * and nested exception. + * @param msg the detail message + * @param cause the nested exception + */ + public NestedCheckedException(String msg, Throwable cause) { + super(msg, cause); + } + + + /** + * Return the detail message, including the message from the nested exception + * if there is one. + */ + @Override + public String getMessage() { + return NestedExceptionUtils.buildMessage(super.getMessage(), getCause()); + } + + + /** + * Retrieve the innermost cause of this exception, if any. + * @return the innermost exception, or {@code null} if none + */ + public Throwable getRootCause() { + Throwable rootCause = null; + Throwable cause = getCause(); + while (cause != null && cause != rootCause) { + rootCause = cause; + cause = cause.getCause(); + } + return rootCause; + } + + /** + * Retrieve the most specific cause of this exception, that is, + * either the innermost cause (root cause) or this exception itself. + * <p>Differs from {@link #getRootCause()} in that it falls back + * to the present exception if there is no root cause. + * @return the most specific cause (never {@code null}) + * @since 2.0.3 + */ + public Throwable getMostSpecificCause() { + Throwable rootCause = getRootCause(); + return (rootCause != null ? rootCause : this); + } + + /** + * Check whether this exception contains an exception of the given type: + * either it is of the given class itself or it contains a nested cause + * of the given type. + * @param exType the exception type to look for + * @return whether there is a nested exception of the specified type + */ + public boolean contains(Class exType) { + if (exType == null) { + return false; + } + if (exType.isInstance(this)) { + return true; + } + Throwable cause = getCause(); + if (cause == this) { + return false; + } + if (cause instanceof NestedCheckedException) { + return ((NestedCheckedException) cause).contains(exType); + } + else { + while (cause != null) { + if (exType.isInstance(cause)) { + return true; + } + if (cause.getCause() == cause) { + break; + } + cause = cause.getCause(); + } + return false; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java b/spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java new file mode 100644 index 00000000..8b66390f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2008 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; + +/** + * Helper class for implementing exception classes which are capable of + * holding nested exceptions. Necessary because we can't share a base + * class among different exception types. + * + * <p>Mainly for use within the framework. + * + * @author Juergen Hoeller + * @since 2.0 + * @see NestedRuntimeException + * @see NestedCheckedException + * @see NestedIOException + * @see org.springframework.web.util.NestedServletException + */ +public abstract class NestedExceptionUtils { + + /** + * Build a message for the given base message and root cause. + * @param message the base message + * @param cause the root cause + * @return the full exception message + */ + public static String buildMessage(String message, Throwable cause) { + if (cause != null) { + StringBuilder sb = new StringBuilder(); + if (message != null) { + sb.append(message).append("; "); + } + sb.append("nested exception is ").append(cause); + return sb.toString(); + } + else { + return message; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/NestedIOException.java b/spring-core/src/main/java/org/springframework/core/NestedIOException.java new file mode 100644 index 00000000..9bd7b88e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/NestedIOException.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2012 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.io.IOException; + +/** + * Subclass of {@link IOException} that properly handles a root cause, + * exposing the root cause just like NestedChecked/RuntimeException does. + * + * <p>Proper root cause handling has not been added to standard IOException before + * Java 6, which is why we need to do it ourselves for Java 5 compatibility purposes. + * + * <p>The similarity between this class and the NestedChecked/RuntimeException + * class is unavoidable, as this class needs to derive from IOException. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #getMessage + * @see #printStackTrace + * @see org.springframework.core.NestedCheckedException + * @see org.springframework.core.NestedRuntimeException + */ +@SuppressWarnings("serial") +public class NestedIOException extends IOException { + + static { + // Eagerly load the NestedExceptionUtils class to avoid classloader deadlock + // issues on OSGi when calling getMessage(). Reported by Don Brown; SPR-5607. + NestedExceptionUtils.class.getName(); + } + + + /** + * Construct a {@code NestedIOException} with the specified detail message. + * @param msg the detail message + */ + public NestedIOException(String msg) { + super(msg); + } + + /** + * Construct a {@code NestedIOException} with the specified detail message + * and nested exception. + * @param msg the detail message + * @param cause the nested exception + */ + public NestedIOException(String msg, Throwable cause) { + super(msg); + initCause(cause); + } + + + /** + * Return the detail message, including the message from the nested exception + * if there is one. + */ + @Override + public String getMessage() { + return NestedExceptionUtils.buildMessage(super.getMessage(), getCause()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java b/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java new file mode 100644 index 00000000..520c8bc9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2012 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; + +/** + * Handy class for wrapping runtime {@code Exceptions} with a root cause. + * + * <p>This class is {@code abstract} to force the programmer to extend + * the class. {@code getMessage} will include nested exception + * information; {@code printStackTrace} and other like methods will + * delegate to the wrapped exception, if any. + * + * <p>The similarity between this class and the {@link NestedCheckedException} + * class is unavoidable, as Java forces these two classes to have different + * superclasses (ah, the inflexibility of concrete inheritance!). + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getMessage + * @see #printStackTrace + * @see NestedCheckedException + */ +public abstract class NestedRuntimeException extends RuntimeException { + + /** Use serialVersionUID from Spring 1.2 for interoperability */ + private static final long serialVersionUID = 5439915454935047936L; + + static { + // Eagerly load the NestedExceptionUtils class to avoid classloader deadlock + // issues on OSGi when calling getMessage(). Reported by Don Brown; SPR-5607. + NestedExceptionUtils.class.getName(); + } + + + /** + * Construct a {@code NestedRuntimeException} with the specified detail message. + * @param msg the detail message + */ + public NestedRuntimeException(String msg) { + super(msg); + } + + /** + * Construct a {@code NestedRuntimeException} with the specified detail message + * and nested exception. + * @param msg the detail message + * @param cause the nested exception + */ + public NestedRuntimeException(String msg, Throwable cause) { + super(msg, cause); + } + + + /** + * Return the detail message, including the message from the nested exception + * if there is one. + */ + @Override + public String getMessage() { + return NestedExceptionUtils.buildMessage(super.getMessage(), getCause()); + } + + + /** + * Retrieve the innermost cause of this exception, if any. + * @return the innermost exception, or {@code null} if none + * @since 2.0 + */ + public Throwable getRootCause() { + Throwable rootCause = null; + Throwable cause = getCause(); + while (cause != null && cause != rootCause) { + rootCause = cause; + cause = cause.getCause(); + } + return rootCause; + } + + /** + * Retrieve the most specific cause of this exception, that is, + * either the innermost cause (root cause) or this exception itself. + * <p>Differs from {@link #getRootCause()} in that it falls back + * to the present exception if there is no root cause. + * @return the most specific cause (never {@code null}) + * @since 2.0.3 + */ + public Throwable getMostSpecificCause() { + Throwable rootCause = getRootCause(); + return (rootCause != null ? rootCause : this); + } + + /** + * Check whether this exception contains an exception of the given type: + * either it is of the given class itself or it contains a nested cause + * of the given type. + * @param exType the exception type to look for + * @return whether there is a nested exception of the specified type + */ + public boolean contains(Class exType) { + if (exType == null) { + return false; + } + if (exType.isInstance(this)) { + return true; + } + Throwable cause = getCause(); + if (cause == this) { + return false; + } + if (cause instanceof NestedRuntimeException) { + return ((NestedRuntimeException) cause).contains(exType); + } + else { + while (cause != null) { + if (exType.isInstance(cause)) { + return true; + } + if (cause.getCause() == cause) { + break; + } + cause = cause.getCause(); + } + return false; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/OrderComparator.java b/spring-core/src/main/java/org/springframework/core/OrderComparator.java new file mode 100644 index 00000000..16158ca1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/OrderComparator.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2012 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.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * {@link Comparator} implementation for {@link Ordered} objects, + * sorting by order value ascending (resp. by priority descending). + * + * <p>Non-{@code Ordered} objects are treated as greatest order + * values, thus ending up at the end of the list, in arbitrary order + * (just like same order values of {@code Ordered} objects). + * + * @author Juergen Hoeller + * @since 07.04.2003 + * @see Ordered + * @see java.util.Collections#sort(java.util.List, java.util.Comparator) + * @see java.util.Arrays#sort(Object[], java.util.Comparator) + */ +public class OrderComparator implements Comparator<Object> { + + /** + * Shared default instance of OrderComparator. + */ + public static final OrderComparator INSTANCE = new OrderComparator(); + + + public int compare(Object o1, Object o2) { + boolean p1 = (o1 instanceof PriorityOrdered); + boolean p2 = (o2 instanceof PriorityOrdered); + if (p1 && !p2) { + return -1; + } + else if (p2 && !p1) { + return 1; + } + + // Direct evaluation instead of Integer.compareTo to avoid unnecessary object creation. + int i1 = getOrder(o1); + int i2 = getOrder(o2); + return (i1 < i2) ? -1 : (i1 > i2) ? 1 : 0; + } + + /** + * Determine the order value for the given object. + * <p>The default implementation checks against the {@link Ordered} + * interface. Can be overridden in subclasses. + * @param obj the object to check + * @return the order value, or {@code Ordered.LOWEST_PRECEDENCE} as fallback + */ + protected int getOrder(Object obj) { + return (obj instanceof Ordered ? ((Ordered) obj).getOrder() : Ordered.LOWEST_PRECEDENCE); + } + + + /** + * Sort the given List with a default OrderComparator. + * <p>Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param list the List to sort + * @see java.util.Collections#sort(java.util.List, java.util.Comparator) + */ + public static void sort(List<?> list) { + if (list.size() > 1) { + Collections.sort(list, INSTANCE); + } + } + + /** + * Sort the given array with a default OrderComparator. + * <p>Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param array the array to sort + * @see java.util.Arrays#sort(Object[], java.util.Comparator) + */ + public static void sort(Object[] array) { + if (array.length > 1) { + Arrays.sort(array, INSTANCE); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/Ordered.java b/spring-core/src/main/java/org/springframework/core/Ordered.java new file mode 100644 index 00000000..fcabe86a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/Ordered.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2012 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 that can be implemented by objects that should be + * orderable, for example in a Collection. + * + * <p>The actual order can be interpreted as prioritization, with + * the first object (with the lowest order value) having the highest + * priority. + * + * <p>Note that there is a 'priority' marker for this interface: + * {@link PriorityOrdered}. Order values expressed by PriorityOrdered + * objects always apply before order values of 'plain' Ordered values. + * + * @author Juergen Hoeller + * @since 07.04.2003 + * @see OrderComparator + * @see org.springframework.core.annotation.Order + */ +public interface Ordered { + + /** + * Useful constant for the highest precedence value. + * @see java.lang.Integer#MIN_VALUE + */ + int HIGHEST_PRECEDENCE = Integer.MIN_VALUE; + + /** + * Useful constant for the lowest precedence value. + * @see java.lang.Integer#MAX_VALUE + */ + int LOWEST_PRECEDENCE = Integer.MAX_VALUE; + + + /** + * Return the order value of this object, with a + * higher value meaning greater in terms of sorting. + * <p>Normally starting with 0, with {@code Integer.MAX_VALUE} + * indicating the greatest value. Same order values will result + * in arbitrary positions for the affected objects. + * <p>Higher values can be interpreted as lower priority. As a + * consequence, the object with the lowest value has highest priority + * (somewhat analogous to Servlet "load-on-startup" values). + * @return the order value + */ + int getOrder(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java b/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java new file mode 100644 index 00000000..597a3923 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2012 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.io.IOException; +import java.io.InputStream; + +import org.springframework.util.FileCopyUtils; + +/** + * {@code ClassLoader} that does <i>not</i> 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. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0.1 + */ +public class OverridingClassLoader extends DecoratingClassLoader { + + /** Packages that are excluded by default */ + public static final String[] DEFAULT_EXCLUDED_PACKAGES = + new String[] {"java.", "javax.", "sun.", "oracle."}; + + private static final String CLASS_FILE_SUFFIX = ".class"; + + + /** + * Create a new OverridingClassLoader for the given class loader. + * @param parent the ClassLoader to build an overriding ClassLoader for + */ + public OverridingClassLoader(ClassLoader parent) { + super(parent); + for (String packageName : DEFAULT_EXCLUDED_PACKAGES) { + excludePackage(packageName); + } + } + + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + Class result = null; + if (isEligibleForOverriding(name)) { + result = loadClassForOverriding(name); + } + if (result != null) { + if (resolve) { + resolveClass(result); + } + return result; + } + else { + return super.loadClass(name, resolve); + } + } + + /** + * Determine whether the specified class is eligible for overriding + * by this class loader. + * @param className the class name to check + * @return whether the specified class is eligible + * @see #isExcluded + */ + protected boolean isEligibleForOverriding(String className) { + return !isExcluded(className); + } + + /** + * Load the specified class for overriding purposes in this ClassLoader. + * <p>The default implementation delegates to {@link #findLoadedClass}, + * {@link #loadBytesForClass} and {@link #defineClass}. + * @param name the name of the class + * @return the Class object, or {@code null} if no class defined for that name + * @throws ClassNotFoundException if the class for the given name couldn't be loaded + */ + protected Class loadClassForOverriding(String name) throws ClassNotFoundException { + Class result = findLoadedClass(name); + if (result == null) { + byte[] bytes = loadBytesForClass(name); + if (bytes != null) { + result = defineClass(name, bytes, 0, bytes.length); + } + } + return result; + } + + /** + * Load the defining bytes for the given class, + * to be turned into a Class object through a {@link #defineClass} call. + * <p>The default implementation delegates to {@link #openStreamForClass} + * and {@link #transformIfNecessary}. + * @param name the name of the class + * @return the byte content (with transformers already applied), + * or {@code null} if no class defined for that name + * @throws ClassNotFoundException if the class for the given name couldn't be loaded + */ + protected byte[] loadBytesForClass(String name) throws ClassNotFoundException { + InputStream is = openStreamForClass(name); + if (is == null) { + return null; + } + try { + // Load the raw bytes. + byte[] bytes = FileCopyUtils.copyToByteArray(is); + // Transform if necessary and use the potentially transformed bytes. + return transformIfNecessary(name, bytes); + } + catch (IOException ex) { + throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex); + } + } + + /** + * Open an InputStream for the specified class. + * <p>The default implementation loads a standard class file through + * the parent ClassLoader's {@code getResourceAsStream} method. + * @param name the name of the class + * @return the InputStream containing the byte code for the specified class + */ + protected InputStream openStreamForClass(String name) { + String internalName = name.replace('.', '/') + CLASS_FILE_SUFFIX; + return getParent().getResourceAsStream(internalName); + } + + + /** + * Transformation hook to be implemented by subclasses. + * <p>The default implementation simply returns the given bytes as-is. + * @param name the fully-qualified name of the class being transformed + * @param bytes the raw bytes of the class + * @return the transformed bytes (never {@code null}; + * same as the input bytes if the transformation produced no changes) + */ + protected byte[] transformIfNecessary(String name, byte[] bytes) { + return bytes; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java new file mode 100644 index 00000000..01356c29 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2012 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.Constructor; +import java.lang.reflect.Method; + +/** + * Interface to discover parameter names for methods and constructors. + * + * <p>Parameter name discovery is not always possible, but various strategies are + * available to try, such as looking for debug information that may have been + * emitted at compile time, and looking for argname annotation values optionally + * accompanying AspectJ annotated methods. + * + * @author Rod Johnson + * @author Adrian Colyer + * @since 2.0 + */ +public interface ParameterNameDiscoverer { + + /** + * Return parameter names for this method, + * or {@code null} if they cannot be determined. + * @param method method to find parameter names for + * @return an array of parameter names if the names can be resolved, + * or {@code null} if they cannot + */ + String[] getParameterNames(Method method); + + /** + * Return parameter names for this constructor, + * or {@code null} if they cannot be determined. + * @param ctor constructor to find parameter names for + * @return an array of parameter names if the names can be resolved, + * or {@code null} if they cannot + */ + String[] getParameterNames(Constructor<?> ctor); + +} diff --git a/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java new file mode 100644 index 00000000..d7165b3e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2013 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.ParameterizedType; +import java.lang.reflect.Type; + +import org.springframework.util.Assert; + +/** + * The purpose of this class is to enable capturing and passing a generic + * {@link Type}. In order to capture the generic type and retain it at runtime, + * you need to create a subclass as follows: + * + * <pre class="code"> + * ParameterizedTypeReference<List<String>> typeRef = new ParameterizedTypeReference<List<String>>() {}; + * </pre> + * + * <p>The resulting {@code typeReference} instance can then be used to obtain a + * {@link Type} instance that carries parameterized type information. + * For more information on "super type tokens" see the link to Neal Gafter's blog post. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 3.2 + * @see <a href="http://gafter.blogspot.nl/2006/12/super-type-tokens.html">Neal Gafter on Super Type Tokens</a> + */ +public abstract class ParameterizedTypeReference<T> { + + private final Type type; + + + protected ParameterizedTypeReference() { + Class<?> parameterizedTypeReferenceSubclass = findParameterizedTypeReferenceSubclass(getClass()); + Type type = parameterizedTypeReferenceSubclass.getGenericSuperclass(); + Assert.isInstanceOf(ParameterizedType.class, type); + ParameterizedType parameterizedType = (ParameterizedType) type; + Assert.isTrue(parameterizedType.getActualTypeArguments().length == 1); + this.type = parameterizedType.getActualTypeArguments()[0]; + } + + + public Type getType() { + return this.type; + } + + @Override + public boolean equals(Object obj) { + return (this == obj || (obj instanceof ParameterizedTypeReference && + this.type.equals(((ParameterizedTypeReference) obj).type))); + } + + @Override + public int hashCode() { + return this.type.hashCode(); + } + + @Override + public String toString() { + return "ParameterizedTypeReference<" + this.type + ">"; + } + + + private static Class<?> findParameterizedTypeReferenceSubclass(Class<?> child) { + Class<?> parent = child.getSuperclass(); + if (Object.class.equals(parent)) { + throw new IllegalStateException("Expected ParameterizedTypeReference superclass"); + } + else if (ParameterizedTypeReference.class.equals(parent)) { + return child; + } + else { + return findParameterizedTypeReferenceSubclass(parent); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/PrioritizedParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/PrioritizedParameterNameDiscoverer.java new file mode 100644 index 00000000..e65e88cd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/PrioritizedParameterNameDiscoverer.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2012 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.Constructor; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; + +/** + * ParameterNameDiscoverer implementation that tries several ParameterNameDiscoverers + * in succession. Those added first in the {@code addDiscoverer} method have + * highest priority. If one returns {@code null}, the next will be tried. + * + * <p>The default behavior is always to return {@code null} + * if no discoverer matches. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + */ +public class PrioritizedParameterNameDiscoverer implements ParameterNameDiscoverer { + + private final List<ParameterNameDiscoverer> parameterNameDiscoverers = + new LinkedList<ParameterNameDiscoverer>(); + + + /** + * Add a further ParameterNameDiscoverer to the list of discoverers + * that this PrioritizedParameterNameDiscoverer checks. + */ + public void addDiscoverer(ParameterNameDiscoverer pnd) { + this.parameterNameDiscoverers.add(pnd); + } + + + public String[] getParameterNames(Method method) { + for (ParameterNameDiscoverer pnd : this.parameterNameDiscoverers) { + String[] result = pnd.getParameterNames(method); + if (result != null) { + return result; + } + } + return null; + } + + public String[] getParameterNames(Constructor ctor) { + for (ParameterNameDiscoverer pnd : this.parameterNameDiscoverers) { + String[] result = pnd.getParameterNames(ctor); + if (result != null) { + return result; + } + } + return null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/PriorityOrdered.java b/spring-core/src/main/java/org/springframework/core/PriorityOrdered.java new file mode 100644 index 00000000..1ab51087 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/PriorityOrdered.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2009 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; + +/** + * Extension of the {@link Ordered} interface, expressing a 'priority' + * ordering: Order values expressed by PriorityOrdered objects always + * apply before order values of 'plain' Ordered values. + * + * <p>This is primarily a special-purpose interface, used for objects + * where it is particularly important to determine 'prioritized' + * objects first, without even obtaining the remaining objects. + * A typical example: Prioritized post-processors in a Spring + * {@link org.springframework.context.ApplicationContext}. + * + * <p>Note: PriorityOrdered post-processor beans are initialized in + * a special phase, ahead of other post-processor beans. This subtly + * affects their autowiring behavior: They will only be autowired against + * beans which do not require eager initialization for type matching. + * + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.beans.factory.config.PropertyOverrideConfigurer + * @see org.springframework.beans.factory.config.PropertyPlaceholderConfigurer + */ +public interface PriorityOrdered extends Ordered { + +} diff --git a/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java b/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java new file mode 100644 index 00000000..24b33f76 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java @@ -0,0 +1,178 @@ +/* + * Copyright 2002-2012 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.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; + +/** + * Simple implementation of the {@link AliasRegistry} interface. + * Serves as base class for + * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} + * implementations. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +public class SimpleAliasRegistry implements AliasRegistry { + + /** Map from alias to canonical name */ + private final Map<String, String> aliasMap = new ConcurrentHashMap<String, String>(16); + + + public void registerAlias(String name, String alias) { + Assert.hasText(name, "'name' must not be empty"); + Assert.hasText(alias, "'alias' must not be empty"); + if (alias.equals(name)) { + this.aliasMap.remove(alias); + } + else { + if (!allowAliasOverriding()) { + String registeredName = this.aliasMap.get(alias); + if (registeredName != null && !registeredName.equals(name)) { + throw new IllegalStateException("Cannot register alias '" + alias + "' for name '" + + name + "': It is already registered for name '" + registeredName + "'."); + } + } + checkForAliasCircle(name, alias); + this.aliasMap.put(alias, name); + } + } + + /** + * Return whether alias overriding is allowed. + * Default is {@code true}. + */ + protected boolean allowAliasOverriding() { + return true; + } + + public void removeAlias(String alias) { + String name = this.aliasMap.remove(alias); + if (name == null) { + throw new IllegalStateException("No alias '" + alias + "' registered"); + } + } + + public boolean isAlias(String name) { + return this.aliasMap.containsKey(name); + } + + public String[] getAliases(String name) { + List<String> result = new ArrayList<String>(); + synchronized (this.aliasMap) { + retrieveAliases(name, result); + } + return StringUtils.toStringArray(result); + } + + /** + * Transitively retrieve all aliases for the given name. + * @param name the target name to find aliases for + * @param result the resulting aliases list + */ + private void retrieveAliases(String name, List<String> result) { + for (Map.Entry<String, String> entry : this.aliasMap.entrySet()) { + String registeredName = entry.getValue(); + if (registeredName.equals(name)) { + String alias = entry.getKey(); + result.add(alias); + retrieveAliases(alias, result); + } + } + } + + /** + * Resolve all alias target names and aliases registered in this + * factory, applying the given StringValueResolver to them. + * <p>The value resolver may for example resolve placeholders + * in target bean names and even in alias names. + * @param valueResolver the StringValueResolver to apply + */ + public void resolveAliases(StringValueResolver valueResolver) { + Assert.notNull(valueResolver, "StringValueResolver must not be null"); + synchronized (this.aliasMap) { + Map<String, String> aliasCopy = new HashMap<String, String>(this.aliasMap); + for (String alias : aliasCopy.keySet()) { + String registeredName = aliasCopy.get(alias); + String resolvedAlias = valueResolver.resolveStringValue(alias); + String resolvedName = valueResolver.resolveStringValue(registeredName); + if (resolvedAlias.equals(resolvedName)) { + this.aliasMap.remove(alias); + } + else if (!resolvedAlias.equals(alias)) { + String existingName = this.aliasMap.get(resolvedAlias); + if (existingName != null && !existingName.equals(resolvedName)) { + throw new IllegalStateException( + "Cannot register resolved alias '" + resolvedAlias + "' (original: '" + alias + + "') for name '" + resolvedName + "': It is already registered for name '" + + registeredName + "'."); + } + checkForAliasCircle(resolvedName, resolvedAlias); + this.aliasMap.remove(alias); + this.aliasMap.put(resolvedAlias, resolvedName); + } + else if (!registeredName.equals(resolvedName)) { + this.aliasMap.put(alias, resolvedName); + } + } + } + } + + /** + * Determine the raw name, resolving aliases to canonical names. + * @param name the user-specified name + * @return the transformed name + */ + public String canonicalName(String name) { + String canonicalName = name; + // Handle aliasing... + String resolvedName; + do { + resolvedName = this.aliasMap.get(canonicalName); + if (resolvedName != null) { + canonicalName = resolvedName; + } + } + while (resolvedName != null); + return canonicalName; + } + + /** + * Check whether the given name points back to given alias as an alias + * in the other direction, catching a circular reference upfront and + * throwing a corresponding IllegalStateException. + * @param name the candidate name + * @param alias the candidate alias + * @see #registerAlias + */ + protected void checkForAliasCircle(String name, String alias) { + if (alias.equals(canonicalName(name))) { + throw new IllegalStateException("Cannot register alias '" + alias + + "' for name '" + name + "': Circular reference - '" + + name + "' is a direct or indirect alias for '" + alias + "' already"); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/SmartClassLoader.java b/spring-core/src/main/java/org/springframework/core/SmartClassLoader.java new file mode 100644 index 00000000..c036f27a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/SmartClassLoader.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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 a reloading-aware ClassLoader + * (e.g. a Groovy-based ClassLoader). Detected for example by + * Spring's CGLIB proxy factory for making a caching decision. + * + * <p>If a ClassLoader does <i>not</i> implement this interface, + * then all of the classes obtained from it should be considered + * as not reloadable (i.e. cacheable). + * + * @author Juergen Hoeller + * @since 2.5.1 + */ +public interface SmartClassLoader { + + /** + * Determine whether the given class is reloadable (in this ClassLoader). + * <p>Typically used to check whether the result may be cached (for this + * ClassLoader) or whether it should be reobtained every time. + * @param clazz the class to check (usually loaded from this ClassLoader) + * @return whether the class should be expected to appear in a reloaded + * version (with a different {@code Class} object) later on + */ + boolean isClassReloadable(Class clazz); + +} diff --git a/spring-core/src/main/java/org/springframework/core/SpringProperties.java b/spring-core/src/main/java/org/springframework/core/SpringProperties.java new file mode 100644 index 00000000..f0b25d6d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2014 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.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Static holder for local Spring properties, i.e. defined at the Spring library level. + * + * <p>Reads a {@code spring.properties} file from the root of the Spring library classpath, + * and also allows for programmatically setting properties through {@link #setProperty}. + * When checking a property, local entries are being checked first, then falling back + * to JVM-level system properties through a {@link System#getProperty} check. + * + * <p>This is an alternative way to set Spring-related system properties such as + * "spring.getenv.ignore" and "spring.beaninfo.ignore", in particular for scenarios + * where JVM system properties are locked on the target platform (e.g. WebSphere). + * See {@link #setFlag} for a convenient way to locally set such flags to "true". + * + * @author Juergen Hoeller + * @since 3.2.7 + * @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 + */ +public abstract class SpringProperties { + + private static final String PROPERTIES_RESOURCE_LOCATION = "spring.properties"; + + private static final Log logger = LogFactory.getLog(SpringProperties.class); + + private static final Properties localProperties = new Properties(); + + + static { + try { + ClassLoader cl = SpringProperties.class.getClassLoader(); + URL url = (cl != null ? cl.getResource(PROPERTIES_RESOURCE_LOCATION) : + ClassLoader.getSystemResource(PROPERTIES_RESOURCE_LOCATION)); + if (url != null) { + logger.info("Found 'spring.properties' file in local classpath"); + InputStream is = url.openStream(); + try { + localProperties.load(is); + } + finally { + is.close(); + } + } + } + catch (IOException ex) { + if (logger.isInfoEnabled()) { + logger.info("Could not load 'spring.properties' file from local classpath: " + ex); + } + } + } + + + /** + * Programmatically set a local property, overriding an entry in the + * {@code spring.properties} file (if any). + * @param key the property key + * @param value the associated property value, or {@code null} to reset it + */ + public static void setProperty(String key, String value) { + if (value != null) { + localProperties.setProperty(key, value); + } + else { + localProperties.remove(key); + } + } + + /** + * Retrieve the property value for the given key, checking local Spring + * properties first and falling back to JVM-level system properties. + * @param key the property key + * @return the associated property value, or {@code null} if none found + */ + public static String getProperty(String key) { + String value = localProperties.getProperty(key); + if (value == null) { + try { + value = System.getProperty(key); + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not retrieve system property '" + key + "': " + ex); + } + } + } + return value; + } + + /** + * Programmatically set a local flag to "true", overriding an + * entry in the {@code spring.properties} file (if any). + * @param key the property key + */ + public static void setFlag(String key) { + localProperties.put(key, Boolean.TRUE.toString()); + } + + /** + * Retrieve the flag for the given property key. + * @param key the property key + * @return {@code true} if the property is set to "true", + * {@code} false otherwise + */ + public static boolean getFlag(String key) { + return Boolean.parseBoolean(getProperty(key)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/SpringVersion.java b/spring-core/src/main/java/org/springframework/core/SpringVersion.java new file mode 100644 index 00000000..38426f1b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/SpringVersion.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2012 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; + +/** + * Class that exposes the Spring version. Fetches the + * "Implementation-Version" manifest attribute from the jar file. + * + * <p>Note that some ClassLoaders do not expose the package metadata, + * hence this class might not be able to determine the Spring version + * in all environments. Consider using a reflection-based check instead: + * For example, checking for the presence of a specific Spring 2.0 + * method that you intend to call. + * + * @author Juergen Hoeller + * @since 1.1 + */ +public class SpringVersion { + + /** + * Return the full version string of the present Spring codebase, + * or {@code null} if it cannot be determined. + * @see Package#getImplementationVersion() + */ + public static String getVersion() { + Package pkg = SpringVersion.class.getPackage(); + return (pkg != null ? pkg.getImplementationVersion() : null); + } + +} 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 new file mode 100644 index 00000000..40041e8d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2012 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.annotation; + +import static java.lang.String.format; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link LinkedHashMap} subclass representing annotation attribute key/value pairs + * as read by Spring's reflection- or ASM-based {@link + * org.springframework.core.type.AnnotationMetadata AnnotationMetadata} implementations. + * 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. + * + * @author Chris Beams + * @since 3.1.1 + */ +@SuppressWarnings("serial") +public class AnnotationAttributes extends LinkedHashMap<String, Object> { + + /** + * Create a new, empty {@link AnnotationAttributes} instance. + */ + public AnnotationAttributes() { + } + + /** + * 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); + } + + /** + * 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 to wrap + * @see #fromMap(Map) + */ + public AnnotationAttributes(Map<String, Object> map) { + super(map); + } + + /** + * Return an {@link AnnotationAttributes} instance based on the given map; if the map + * is already an {@code AnnotationAttributes} instance, it is casted and returned + * immediately without creating any new instance; otherwise create a new instance by + * wrapping the map with the {@link #AnnotationAttributes(Map)} constructor. + * @param map original source of annotation attribute key/value pairs + */ + public static AnnotationAttributes fromMap(Map<String, Object> map) { + if (map == null) { + return null; + } + + if (map instanceof AnnotationAttributes) { + return (AnnotationAttributes) map; + } + + return new AnnotationAttributes(map); + } + + public String getString(String attributeName) { + return doGet(attributeName, String.class); + } + + public String[] getStringArray(String attributeName) { + return doGet(attributeName, String[].class); + } + + public boolean getBoolean(String attributeName) { + return doGet(attributeName, Boolean.class); + } + + @SuppressWarnings("unchecked") + public <N extends Number> N getNumber(String attributeName) { + return (N) doGet(attributeName, Integer.class); + } + + @SuppressWarnings("unchecked") + public <E extends Enum<?>> E getEnum(String attributeName) { + return (E) doGet(attributeName, Enum.class); + } + + @SuppressWarnings("unchecked") + public <T> Class<? extends T> getClass(String attributeName) { + return doGet(attributeName, Class.class); + } + + public Class<?>[] getClassArray(String attributeName) { + return doGet(attributeName, Class[].class); + } + + public AnnotationAttributes getAnnotation(String attributeName) { + return doGet(attributeName, AnnotationAttributes.class); + } + + public AnnotationAttributes[] getAnnotationArray(String attributeName) { + return doGet(attributeName, AnnotationAttributes[].class); + } + + @SuppressWarnings("unchecked") + private <T> T doGet(String attributeName, Class<T> expectedType) { + Assert.hasText(attributeName, "attributeName must not be null or empty"); + Object value = this.get(attributeName); + Assert.notNull(value, format("Attribute '%s' not found", attributeName)); + Assert.isAssignable(expectedType, value.getClass(), + format("Attribute '%s' is of type [%s], but [%s] was expected. Cause: ", + attributeName, value.getClass().getSimpleName(), expectedType.getSimpleName())); + return (T) value; + } + + public String toString() { + Iterator<Map.Entry<String, Object>> entries = entrySet().iterator(); + StringBuilder sb = new StringBuilder("{"); + while (entries.hasNext()) { + Map.Entry<String, Object> entry = entries.next(); + sb.append(entry.getKey()); + sb.append('='); + sb.append(valueToString(entry.getValue())); + sb.append(entries.hasNext() ? ", " : ""); + } + sb.append("}"); + return sb.toString(); + } + + private String valueToString(Object value) { + if (value == this) { + return "(this Map)"; + } + if (value instanceof Object[]) { + return "[" + StringUtils.arrayToCommaDelimitedString((Object[]) value) + "]"; + } + return String.valueOf(value); + } +} 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 new file mode 100644 index 00000000..19ba5fec --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2013 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.annotation; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.springframework.core.OrderComparator; +import org.springframework.core.Ordered; + +/** + * {@link java.util.Comparator} implementation that checks + * {@link org.springframework.core.Ordered} as well as the + * {@link Order} annotation, with an order value provided by an + * {@code Ordered} instance overriding a statically defined + * annotation value (if any). + * + * @author Juergen Hoeller + * @author Oliver Gierke + * @since 2.0.1 + * @see org.springframework.core.Ordered + * @see Order + */ +public class AnnotationAwareOrderComparator extends OrderComparator { + + /** + * Shared default instance of AnnotationAwareOrderComparator. + */ + public static final AnnotationAwareOrderComparator INSTANCE = new AnnotationAwareOrderComparator(); + + + @Override + protected int getOrder(Object obj) { + if (obj instanceof Ordered) { + return ((Ordered) obj).getOrder(); + } + if (obj != null) { + Class<?> clazz = (obj instanceof Class ? (Class) obj : obj.getClass()); + Order order = AnnotationUtils.findAnnotation(clazz, Order.class); + if (order != null) { + return order.value(); + } + } + return Ordered.LOWEST_PRECEDENCE; + } + + + /** + * Sort the given List with a default AnnotationAwareOrderComparator. + * <p>Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param list the List to sort + * @see java.util.Collections#sort(java.util.List, java.util.Comparator) + */ + public static void sort(List<?> list) { + if (list.size() > 1) { + Collections.sort(list, INSTANCE); + } + } + + /** + * Sort the given array with a default AnnotationAwareOrderComparator. + * <p>Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param array the array to sort + * @see java.util.Arrays#sort(Object[], java.util.Comparator) + */ + public static void sort(Object[] array) { + if (array.length > 1) { + Arrays.sort(array, INSTANCE); + } + } + +} 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 new file mode 100644 index 00000000..3b4def63 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -0,0 +1,518 @@ +/* + * Copyright 2002-2014 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.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +import org.springframework.core.BridgeMethodResolver; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * General utility methods for working with annotations, handling bridge methods (which the compiler + * generates for generic declarations) as well as super methods (for optional "annotation inheritance"). + * Note that none of this is provided by the JDK's introspection facilities themselves. + * + * <p>As a general rule for runtime-retained annotations (e.g. for transaction control, authorization or service + * exposure), always use the lookup methods on this class (e.g., {@link #findAnnotation(Method, Class)}, {@link + * #getAnnotation(Method, Class)}, and {@link #getAnnotations(Method)}) instead of the plain annotation lookup + * methods in the JDK. You can still explicitly choose between lookup on the given class level only ({@link + * #getAnnotation(Method, Class)}) and lookup in the entire inheritance hierarchy of the given method ({@link + * #findAnnotation(Method, Class)}). + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Sam Brannen + * @author Mark Fisher + * @author Chris Beams + * @since 2.0 + * @see java.lang.reflect.Method#getAnnotations() + * @see java.lang.reflect.Method#getAnnotation(Class) + */ +public abstract class AnnotationUtils { + + /** The attribute name for annotations with a single element */ + static final String VALUE = "value"; + + private static final Map<Class<?>, Boolean> annotatedInterfaceCache = new WeakHashMap<Class<?>, Boolean>(); + + + /** + * Get a single {@link Annotation} of {@code annotationType} from the supplied + * Method, Constructor or Field. Meta-annotations will be searched if the annotation + * is not declared locally on the supplied element. + * @param ae the Method, Constructor or Field from which to get the annotation + * @param annotationType the annotation type to look for, both locally and as a meta-annotation + * @return the matching annotation, or {@code null} if none found + * @since 3.1 + */ + public static <T extends Annotation> T getAnnotation(AnnotatedElement ae, Class<T> annotationType) { + T ann = ae.getAnnotation(annotationType); + if (ann == null) { + for (Annotation metaAnn : ae.getAnnotations()) { + ann = metaAnn.annotationType().getAnnotation(annotationType); + if (ann != null) { + break; + } + } + } + return ann; + } + + /** + * Get all {@link Annotation Annotations} from the supplied {@link Method}. + * <p>Correctly handles bridge {@link Method Methods} generated by the compiler. + * @param method the method to look for annotations on + * @return the annotations found + * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod(Method) + */ + public static Annotation[] getAnnotations(Method method) { + return BridgeMethodResolver.findBridgedMethod(method).getAnnotations(); + } + + /** + * Get a single {@link Annotation} of {@code annotationType} from the supplied {@link Method}. + * <p>Correctly handles bridge {@link Method Methods} generated by the compiler. + * @param method the method to look for annotations on + * @param annotationType the annotation type to look for + * @return the annotations found + * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod(Method) + */ + public static <A extends Annotation> A getAnnotation(Method method, Class<A> annotationType) { + Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method); + A ann = resolvedMethod.getAnnotation(annotationType); + if (ann == null) { + for (Annotation metaAnn : resolvedMethod.getAnnotations()) { + ann = metaAnn.annotationType().getAnnotation(annotationType); + if (ann != null) { + break; + } + } + } + return ann; + } + + /** + * Get a single {@link Annotation} of {@code annotationType} from the supplied {@link Method}, + * traversing its super methods if no annotation can be found on the given method itself. + * <p>Annotations on methods are not inherited by default, so we need to handle this explicitly. + * @param method the method to look for annotations on + * @param annotationType the annotation type to look for + * @return the annotation found, or {@code null} if none + */ + public static <A extends Annotation> A findAnnotation(Method method, Class<A> annotationType) { + A annotation = getAnnotation(method, annotationType); + Class<?> clazz = method.getDeclaringClass(); + if (annotation == null) { + annotation = searchOnInterfaces(method, annotationType, clazz.getInterfaces()); + } + while (annotation == null) { + clazz = clazz.getSuperclass(); + if (clazz == null || clazz.equals(Object.class)) { + break; + } + try { + Method equivalentMethod = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); + annotation = getAnnotation(equivalentMethod, annotationType); + } + catch (NoSuchMethodException ex) { + // No equivalent method found + } + if (annotation == null) { + annotation = searchOnInterfaces(method, annotationType, clazz.getInterfaces()); + } + } + return annotation; + } + + private static <A extends Annotation> A searchOnInterfaces(Method method, Class<A> annotationType, Class<?>[] ifcs) { + A annotation = null; + for (Class<?> iface : ifcs) { + if (isInterfaceWithAnnotatedMethods(iface)) { + try { + Method equivalentMethod = iface.getMethod(method.getName(), method.getParameterTypes()); + annotation = getAnnotation(equivalentMethod, annotationType); + } + catch (NoSuchMethodException ex) { + // Skip this interface - it doesn't have the method... + } + if (annotation != null) { + break; + } + } + } + return annotation; + } + + private static boolean isInterfaceWithAnnotatedMethods(Class<?> iface) { + synchronized (annotatedInterfaceCache) { + Boolean flag = annotatedInterfaceCache.get(iface); + if (flag != null) { + return flag; + } + boolean found = false; + for (Method ifcMethod : iface.getMethods()) { + if (ifcMethod.getAnnotations().length > 0) { + found = true; + break; + } + } + annotatedInterfaceCache.put(iface, found); + return found; + } + } + + /** + * Find a single {@link Annotation} of {@code annotationType} from the supplied {@link Class}, + * traversing its interfaces and superclasses if no annotation can be found on the given class itself. + * <p>This method explicitly handles class-level annotations which are not declared as + * {@link java.lang.annotation.Inherited inherited} <i>as well as annotations on interfaces</i>. + * <p>The algorithm operates as follows: Searches for an annotation on the given class and returns + * it if found. Else searches all interfaces that the given class declares, returning the annotation + * from the first matching candidate, if any. Else proceeds with introspection of the superclass + * of the given class, checking the superclass itself; if no annotation found there, proceeds + * with the interfaces that the superclass declares. Recursing up through the entire superclass + * hierarchy if no match is found. + * @param clazz the class to look for annotations on + * @param annotationType the annotation type to look for + * @return the annotation found, or {@code null} if none found + */ + public static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType) { + Assert.notNull(clazz, "Class must not be null"); + A annotation = clazz.getAnnotation(annotationType); + if (annotation != null) { + return annotation; + } + for (Class<?> ifc : clazz.getInterfaces()) { + annotation = findAnnotation(ifc, annotationType); + if (annotation != null) { + return annotation; + } + } + if (!Annotation.class.isAssignableFrom(clazz)) { + for (Annotation ann : clazz.getAnnotations()) { + annotation = findAnnotation(ann.annotationType(), annotationType); + if (annotation != null) { + return annotation; + } + } + } + Class<?> superclass = clazz.getSuperclass(); + if (superclass == null || superclass.equals(Object.class)) { + return null; + } + return findAnnotation(superclass, annotationType); + } + + /** + * Find the first {@link Class} in the inheritance hierarchy of the specified {@code clazz} + * (including the specified {@code clazz} itself) which declares an annotation for the + * specified {@code annotationType}, or {@code null} if not found. If the supplied + * {@code clazz} is {@code null}, {@code null} will be returned. + * <p>If the supplied {@code clazz} is an interface, only the interface itself will be checked; + * the inheritance hierarchy for interfaces will not be traversed. + * <p>The standard {@link Class} API does not provide a mechanism for determining which class + * in an inheritance hierarchy actually declares an {@link Annotation}, so we need to handle + * this explicitly. + * @param annotationType the annotation type to look for, both locally and as a meta-annotation + * @param clazz the class on which to check for the annotation (may be {@code null}) + * @return the first {@link Class} in the inheritance hierarchy of the specified {@code clazz} + * which declares an annotation for the specified {@code annotationType}, or {@code null} + * if not found + * @see Class#isAnnotationPresent(Class) + * @see Class#getDeclaredAnnotations() + * @see #findAnnotationDeclaringClassForTypes(List, Class) + * @see #isAnnotationDeclaredLocally(Class, Class) + */ + public static Class<?> findAnnotationDeclaringClass(Class<? extends Annotation> annotationType, Class<?> clazz) { + Assert.notNull(annotationType, "Annotation type must not be null"); + if (clazz == null || clazz.equals(Object.class)) { + return null; + } + if (isAnnotationDeclaredLocally(annotationType, clazz)) { + return clazz; + } + return findAnnotationDeclaringClass(annotationType, clazz.getSuperclass()); + } + + /** + * Find the first {@link Class} in the inheritance hierarchy of the specified + * {@code clazz} (including the specified {@code clazz} itself) which declares + * at least one of the specified {@code annotationTypes}, or {@code null} if + * none of the specified annotation types could be found. + * <p>If the supplied {@code clazz} is {@code null}, {@code null} will be + * returned. + * <p>If the supplied {@code clazz} is an interface, only the interface itself + * will be checked; the inheritance hierarchy for interfaces will not be traversed. + * <p>The standard {@link Class} API does not provide a mechanism for determining + * which class in an inheritance hierarchy actually declares one of several + * candidate {@linkplain Annotation annotations}, so we need to handle this + * explicitly. + * @param annotationTypes the list of Class objects corresponding to the + * annotation types + * @param clazz the Class object corresponding to the class on which to check + * for the annotations, or {@code null} + * @return the first {@link Class} in the inheritance hierarchy of the specified + * {@code clazz} which declares an annotation of at least one of the specified + * {@code annotationTypes}, or {@code null} if not found + * @since 3.2.2 + * @see Class#isAnnotationPresent(Class) + * @see Class#getDeclaredAnnotations() + * @see #findAnnotationDeclaringClass(Class, Class) + * @see #isAnnotationDeclaredLocally(Class, Class) + */ + public static Class<?> findAnnotationDeclaringClassForTypes(List<Class<? extends Annotation>> annotationTypes, Class<?> clazz) { + Assert.notEmpty(annotationTypes, "The list of annotation types must not be empty"); + if (clazz == null || clazz.equals(Object.class)) { + return null; + } + for (Class<? extends Annotation> annotationType : annotationTypes) { + if (isAnnotationDeclaredLocally(annotationType, clazz)) { + return clazz; + } + } + return findAnnotationDeclaringClassForTypes(annotationTypes, clazz.getSuperclass()); + } + + /** + * Determine whether an annotation for the specified {@code annotationType} is + * declared locally on the supplied {@code clazz}. The supplied {@link Class} + * may represent any type. + * <p>Note: This method does <strong>not</strong> determine if the annotation is + * {@linkplain java.lang.annotation.Inherited inherited}. For greater clarity + * regarding inherited annotations, consider using + * {@link #isAnnotationInherited(Class, Class)} instead. + * @param annotationType the Class object corresponding to the annotation type + * @param clazz the Class object corresponding to the class on which to check for the annotation + * @return {@code true} if an annotation for the specified {@code annotationType} + * is declared locally on the supplied {@code clazz} + * @see Class#getDeclaredAnnotations() + * @see #isAnnotationInherited(Class, Class) + */ + public static boolean isAnnotationDeclaredLocally(Class<? extends Annotation> annotationType, Class<?> clazz) { + Assert.notNull(annotationType, "Annotation type must not be null"); + Assert.notNull(clazz, "Class must not be null"); + boolean declaredLocally = false; + for (Annotation annotation : clazz.getDeclaredAnnotations()) { + if (annotation.annotationType().equals(annotationType)) { + declaredLocally = true; + break; + } + } + return declaredLocally; + } + + /** + * Determine whether an annotation for the specified {@code annotationType} is present + * on the supplied {@code clazz} and is {@linkplain java.lang.annotation.Inherited inherited} + * (i.e., not declared locally for the class). + * <p>If the supplied {@code clazz} is an interface, only the interface itself will be checked. + * In accordance with standard meta-annotation semantics, the inheritance hierarchy for interfaces + * will not be traversed. See the {@linkplain java.lang.annotation.Inherited Javadoc} for the + * {@code @Inherited} meta-annotation for further details regarding annotation inheritance. + * @param annotationType the Class object corresponding to the annotation type + * @param clazz the Class object corresponding to the class on which to check for the annotation + * @return {@code true} if an annotation for the specified {@code annotationType} is present + * on the supplied {@code clazz} and is <em>inherited</em> + * @see Class#isAnnotationPresent(Class) + * @see #isAnnotationDeclaredLocally(Class, Class) + */ + public static boolean isAnnotationInherited(Class<? extends Annotation> annotationType, Class<?> clazz) { + Assert.notNull(annotationType, "Annotation type must not be null"); + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isAnnotationPresent(annotationType) && !isAnnotationDeclaredLocally(annotationType, clazz)); + } + + /** + * Retrieve the given annotation's attributes as a Map, preserving all attribute types + * as-is. + * <p>Note: As of Spring 3.1.1, the returned map is actually an + * {@link AnnotationAttributes} instance, however the Map signature of this method has + * been preserved for binary compatibility. + * @param annotation the annotation to retrieve the attributes for + * @return the Map of annotation attributes, with attribute names as keys and + * corresponding attribute values as values + */ + public static Map<String, Object> getAnnotationAttributes(Annotation annotation) { + return getAnnotationAttributes(annotation, false, false); + } + + /** + * Retrieve the given annotation's attributes as a Map. Equivalent to calling + * {@link #getAnnotationAttributes(Annotation, boolean, boolean)} with + * the {@code nestedAnnotationsAsMap} parameter set to {@code false}. + * <p>Note: As of Spring 3.1.1, the returned map is actually an + * {@link AnnotationAttributes} instance, however the Map signature of this method has + * been preserved for binary compatibility. + * @param annotation the annotation to retrieve the attributes for + * @param classValuesAsString whether to turn Class references into Strings (for + * compatibility with {@link org.springframework.core.type.AnnotationMetadata} or to + * preserve them as Class references + * @return the Map of annotation attributes, with attribute names as keys and + * corresponding attribute values as values + */ + public static Map<String, Object> getAnnotationAttributes(Annotation annotation, boolean classValuesAsString) { + return getAnnotationAttributes(annotation, classValuesAsString, false); + } + + /** + * Retrieve the given annotation's attributes as an {@link AnnotationAttributes} + * map structure. Implemented in Spring 3.1.1 to provide fully recursive annotation + * reading capabilities on par with that of the reflection-based + * {@link org.springframework.core.type.StandardAnnotationMetadata}. + * @param annotation the annotation to retrieve the attributes for + * @param classValuesAsString whether to turn Class references into Strings (for + * compatibility with {@link org.springframework.core.type.AnnotationMetadata} or to + * preserve them as Class references + * @param nestedAnnotationsAsMap whether to turn nested Annotation instances into + * {@link AnnotationAttributes} maps (for compatibility with + * {@link org.springframework.core.type.AnnotationMetadata} or to preserve them as + * Annotation instances + * @return the annotation attributes (a specialized Map) with attribute names as keys + * and corresponding attribute values as values + * @since 3.1.1 + */ + public static AnnotationAttributes getAnnotationAttributes(Annotation annotation, boolean classValuesAsString, + boolean nestedAnnotationsAsMap) { + + AnnotationAttributes attrs = new AnnotationAttributes(); + Method[] methods = annotation.annotationType().getDeclaredMethods(); + for (Method method : methods) { + if (method.getParameterTypes().length == 0 && method.getReturnType() != void.class) { + try { + Object value = method.invoke(annotation); + if (classValuesAsString) { + if (value instanceof Class) { + value = ((Class<?>) value).getName(); + } + 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(); + } + value = newValue; + } + } + if (nestedAnnotationsAsMap && value instanceof Annotation) { + attrs.put(method.getName(), + getAnnotationAttributes((Annotation) value, classValuesAsString, true)); + } + else if (nestedAnnotationsAsMap && value instanceof Annotation[]) { + Annotation[] realAnnotations = (Annotation[]) value; + AnnotationAttributes[] mappedAnnotations = new AnnotationAttributes[realAnnotations.length]; + for (int i = 0; i < realAnnotations.length; i++) { + mappedAnnotations[i] = getAnnotationAttributes(realAnnotations[i], classValuesAsString, true); + } + attrs.put(method.getName(), mappedAnnotations); + } + else { + attrs.put(method.getName(), value); + } + } + catch (Exception ex) { + throw new IllegalStateException("Could not obtain annotation attribute values", ex); + } + } + } + return attrs; + } + + /** + * Retrieve the <em>value</em> of the {@code "value"} attribute of a + * single-element Annotation, given an annotation instance. + * @param annotation the annotation instance from which to retrieve the value + * @return the attribute value, or {@code null} if not found + * @see #getValue(Annotation, String) + */ + public static Object getValue(Annotation annotation) { + return getValue(annotation, VALUE); + } + + /** + * Retrieve the <em>value</em> of a named Annotation attribute, given an annotation instance. + * @param annotation the annotation instance from which to retrieve the value + * @param attributeName the name of the attribute value to retrieve + * @return the attribute value, or {@code null} if not found + * @see #getValue(Annotation) + */ + public static Object getValue(Annotation annotation, String attributeName) { + try { + Method method = annotation.annotationType().getDeclaredMethod(attributeName); + ReflectionUtils.makeAccessible(method); + return method.invoke(annotation); + } + catch (Exception ex) { + return null; + } + } + + /** + * Retrieve the <em>default value</em> of the {@code "value"} attribute + * of a single-element Annotation, given an annotation instance. + * @param annotation the annotation instance from which to retrieve the default value + * @return the default value, or {@code null} if not found + * @see #getDefaultValue(Annotation, String) + */ + public static Object getDefaultValue(Annotation annotation) { + return getDefaultValue(annotation, VALUE); + } + + /** + * Retrieve the <em>default value</em> of a named Annotation attribute, given an annotation instance. + * @param annotation the annotation instance from which to retrieve the default value + * @param attributeName the name of the attribute value to retrieve + * @return the default value of the named attribute, or {@code null} if not found + * @see #getDefaultValue(Class, String) + */ + public static Object getDefaultValue(Annotation annotation, String attributeName) { + return getDefaultValue(annotation.annotationType(), attributeName); + } + + /** + * Retrieve the <em>default value</em> of the {@code "value"} attribute + * of a single-element Annotation, given the {@link Class annotation type}. + * @param annotationType the <em>annotation type</em> for which the default value should be retrieved + * @return the default value, or {@code null} if not found + * @see #getDefaultValue(Class, String) + */ + public static Object getDefaultValue(Class<? extends Annotation> annotationType) { + return getDefaultValue(annotationType, VALUE); + } + + /** + * Retrieve the <em>default value</em> of a named Annotation attribute, given the {@link Class annotation type}. + * @param annotationType the <em>annotation type</em> for which the default value should be retrieved + * @param attributeName the name of the attribute value to retrieve. + * @return the default value of the named attribute, or {@code null} if not found + * @see #getDefaultValue(Annotation, String) + */ + public static Object getDefaultValue(Class<? extends Annotation> annotationType, String attributeName) { + try { + return annotationType.getDeclaredMethod(attributeName).getDefaultValue(); + } + catch (Exception ex) { + return null; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/Order.java b/spring-core/src/main/java/org/springframework/core/annotation/Order.java new file mode 100644 index 00000000..54f81677 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/Order.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.Ordered; + +/** + * Annotation that defines ordering. The value is optional, and represents order value + * as defined in the {@link Ordered} interface. Lower values have higher priority. + * The default value is {@code Ordered.LOWEST_PRECEDENCE}, indicating + * lowest priority (losing to any other specified order value). + * + * <p><b>NOTE:</b> Annotation-based ordering is supported for specific kinds of + * components only, e.g. for annotation-based AspectJ aspects. Spring container + * strategies, on the other hand, are typically based on the {@link Ordered} + * interface in order to allow for configurable ordering of each <i>instance</i>. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.core.Ordered + * @see AnnotationAwareOrderComparator + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) +public @interface Order { + + /** + * The order value. Default is {@link Ordered#LOWEST_PRECEDENCE}. + * @see Ordered#getOrder() + */ + int value() default Ordered.LOWEST_PRECEDENCE; + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/package-info.java b/spring-core/src/main/java/org/springframework/core/annotation/package-info.java new file mode 100644 index 00000000..435f79ae --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * Core support package for Java 5 annotations. + * + */ +package org.springframework.core.annotation; + diff --git a/spring-core/src/main/java/org/springframework/core/convert/AbstractDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/AbstractDescriptor.java new file mode 100644 index 00000000..86995f32 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/AbstractDescriptor.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2011 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; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * @author Keith Donald + * @since 3.1 + */ +abstract class AbstractDescriptor { + + private final Class<?> type; + + + protected AbstractDescriptor(Class<?> type) { + Assert.notNull(type, "Type must not be null"); + this.type = type; + } + + + public Class<?> getType() { + return this.type; + } + + public TypeDescriptor getElementTypeDescriptor() { + if (isCollection()) { + Class<?> elementType = resolveCollectionElementType(); + return (elementType != null ? new TypeDescriptor(nested(elementType, 0)) : null); + } + else if (isArray()) { + Class<?> elementType = getType().getComponentType(); + return new TypeDescriptor(nested(elementType, 0)); + } + else { + return null; + } + } + + public TypeDescriptor getMapKeyTypeDescriptor() { + if (isMap()) { + Class<?> keyType = resolveMapKeyType(); + return keyType != null ? new TypeDescriptor(nested(keyType, 0)) : null; + } + else { + return null; + } + } + + public TypeDescriptor getMapValueTypeDescriptor() { + if (isMap()) { + Class<?> valueType = resolveMapValueType(); + return valueType != null ? new TypeDescriptor(nested(valueType, 1)) : null; + } + else { + return null; + } + } + + public AbstractDescriptor nested() { + if (isCollection()) { + Class<?> elementType = resolveCollectionElementType(); + return (elementType != null ? nested(elementType, 0) : null); + } + else if (isArray()) { + return nested(getType().getComponentType(), 0); + } + else if (isMap()) { + Class<?> mapValueType = resolveMapValueType(); + return (mapValueType != null ? nested(mapValueType, 1) : null); + } + else if (Object.class.equals(getType())) { + // could be a collection type but we don't know about its element type, + // so let's just assume there is an element type of type Object + return this; + } + else { + throw new IllegalStateException("Not a collection, array, or map: cannot resolve nested value types"); + } + } + + + // subclassing hooks + + public abstract Annotation[] getAnnotations(); + + protected abstract Class<?> resolveCollectionElementType(); + + protected abstract Class<?> resolveMapKeyType(); + + protected abstract Class<?> resolveMapValueType(); + + protected abstract AbstractDescriptor nested(Class<?> type, int typeIndex); + + + // internal helpers + + private boolean isCollection() { + return Collection.class.isAssignableFrom(getType()); + } + + private boolean isArray() { + return getType().isArray(); + } + + private boolean isMap() { + return Map.class.isAssignableFrom(getType()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/BeanPropertyDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/BeanPropertyDescriptor.java new file mode 100644 index 00000000..8be6b0e2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/BeanPropertyDescriptor.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2011 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; + +import java.lang.annotation.Annotation; + +import org.springframework.core.GenericCollectionTypeResolver; +import org.springframework.core.MethodParameter; + +/** + * @author Keith Donald + * @since 3.1 + */ +class BeanPropertyDescriptor extends AbstractDescriptor { + + private final Property property; + + private final MethodParameter methodParameter; + + private final Annotation[] annotations; + + + public BeanPropertyDescriptor(Property property) { + super(property.getType()); + this.property = property; + this.methodParameter = property.getMethodParameter(); + this.annotations = property.getAnnotations(); + } + + + @Override + public Annotation[] getAnnotations() { + return this.annotations; + } + + @Override + protected Class<?> resolveCollectionElementType() { + return GenericCollectionTypeResolver.getCollectionParameterType(this.methodParameter); + } + + @Override + protected Class<?> resolveMapKeyType() { + return GenericCollectionTypeResolver.getMapKeyParameterType(this.methodParameter); + } + + @Override + protected Class<?> resolveMapValueType() { + return GenericCollectionTypeResolver.getMapValueParameterType(this.methodParameter); + } + + @Override + protected AbstractDescriptor nested(Class<?> type, int typeIndex) { + MethodParameter methodParameter = new MethodParameter(this.methodParameter); + methodParameter.increaseNestingLevel(); + methodParameter.setTypeIndexForCurrentLevel(typeIndex); + return new BeanPropertyDescriptor(type, this.property, methodParameter, this.annotations); + } + + + // internal + + private BeanPropertyDescriptor(Class<?> type, Property propertyDescriptor, MethodParameter methodParameter, Annotation[] annotations) { + super(type); + this.property = propertyDescriptor; + this.methodParameter = methodParameter; + this.annotations = annotations; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/ClassDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/ClassDescriptor.java new file mode 100644 index 00000000..87f8e066 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/ClassDescriptor.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2012 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; + +import java.lang.annotation.Annotation; + +import org.springframework.core.GenericCollectionTypeResolver; + +/** + * @author Keith Donald + * @author Phillip Webb + * @since 3.1 + */ +class ClassDescriptor extends AbstractDescriptor { + + ClassDescriptor(Class<?> type) { + super(type); + } + + @Override + public Annotation[] getAnnotations() { + return TypeDescriptor.EMPTY_ANNOTATION_ARRAY; + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected Class<?> resolveCollectionElementType() { + return GenericCollectionTypeResolver.getCollectionType((Class) getType()); + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected Class<?> resolveMapKeyType() { + return GenericCollectionTypeResolver.getMapKeyType((Class) getType()); + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected Class<?> resolveMapValueType() { + return GenericCollectionTypeResolver.getMapValueType((Class) getType()); + } + + @Override + protected AbstractDescriptor nested(Class<?> type, int typeIndex) { + return new ClassDescriptor(type); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConversionException.java b/spring-core/src/main/java/org/springframework/core/convert/ConversionException.java new file mode 100644 index 00000000..f93e2e67 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/ConversionException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2010 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; + +import org.springframework.core.NestedRuntimeException; + +/** + * Base class for exceptions thrown by the conversion system. + * + * @author Keith Donald + * @since 3.0 + */ +@SuppressWarnings("serial") +public abstract class ConversionException extends NestedRuntimeException { + + /** + * Construct a new conversion exception. + * @param message the exception message + */ + public ConversionException(String message) { + super(message); + } + + /** + * Construct a new conversion exception. + * @param message the exception message + * @param cause the cause + */ + public ConversionException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java b/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java new file mode 100644 index 00000000..63f4678f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2010 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; + +import org.springframework.util.ObjectUtils; + +/** + * Exception to be thrown when an actual type conversion attempt fails. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +@SuppressWarnings("serial") +public final class ConversionFailedException extends ConversionException { + + private final TypeDescriptor sourceType; + + private final TypeDescriptor targetType; + + private final Object value; + + + /** + * Create a new conversion exception. + * @param sourceType the value's original type + * @param targetType the value's target type + * @param value the value we tried to convert + * @param cause the cause of the conversion failure + */ + public ConversionFailedException(TypeDescriptor sourceType, TypeDescriptor targetType, Object value, Throwable cause) { + super("Failed to convert from type " + sourceType + " to type " + targetType + " for value '" + ObjectUtils.nullSafeToString(value) + "'", cause); + this.sourceType = sourceType; + this.targetType = targetType; + this.value = value; + } + + + /** + * Return the source type we tried to convert the value from. + */ + public TypeDescriptor getSourceType() { + return this.sourceType; + } + + /** + * Return the target type we tried to convert the value to. + */ + public TypeDescriptor getTargetType() { + return this.targetType; + } + + /** + * Return the offending value. + */ + public Object getValue() { + return this.value; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java new file mode 100644 index 00000000..75f9a276 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2013 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; + +/** + * A service interface for type conversion. This is the entry point into the convert system. + * Call {@link #convert(Object, Class)} to perform a thread-safe type conversion using this system. + * + * @author Keith Donald + * @author Phillip Webb + * @since 3.0 + */ +public interface ConversionService { + + /** + * Return {@code true} if objects of {@code sourceType} can be converted to the {@code targetType}. + * <p>If this method returns {@code true}, it means {@link #convert(Object, Class)} is capable + * of converting an instance of {@code sourceType} to {@code targetType}. + * <p>Special note on collections, arrays, and maps types: + * For conversion between collection, array, and map types, this method will return {@code true} + * even though a convert invocation may still generate a {@link ConversionException} if the + * underlying elements are not convertible. Callers are expected to handle this exceptional case + * when working with collections and maps. + * @param sourceType the source type to convert from (may be {@code null} if source is {@code null}) + * @param targetType the target type to convert to (required) + * @return {@code true} if a conversion can be performed, {@code false} if not + * @throws IllegalArgumentException if {@code targetType} is {@code null} + */ + boolean canConvert(Class<?> sourceType, Class<?> targetType); + + /** + * Return {@code true} if objects of {@code sourceType} can be converted to the {@code targetType}. + * The TypeDescriptors provide additional context about the source and target locations + * where conversion would occur, often object fields or property locations. + * <p>If this method returns {@code true}, it means {@link #convert(Object, TypeDescriptor, TypeDescriptor)} + * is capable of converting an instance of {@code sourceType} to {@code targetType}. + * <p>Special note on collections, arrays, and maps types: + * For conversion between collection, array, and map types, this method will return {@code true} + * even though a convert invocation may still generate a {@link ConversionException} if the + * underlying elements are not convertible. Callers are expected to handle this exceptional case + * when working with collections and maps. + * @param sourceType context about the source type to convert from + * (may be {@code null} if source is {@code null}) + * @param targetType context about the target type to convert to (required) + * @return {@code true} if a conversion can be performed between the source and target types, + * {@code false} if not + * @throws IllegalArgumentException if {@code targetType} is {@code null} + */ + boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType); + + /** + * Convert the given {@code source} to the specified {@code targetType}. + * @param source the source object to convert (may be null) + * @param targetType the target type to convert to (required) + * @return the converted object, an instance of targetType + * @throws ConversionException if a conversion exception occurred + * @throws IllegalArgumentException if targetType is null + */ + <T> T convert(Object source, Class<T> targetType); + + /** + * Convert the given {@code source} to the specified {@code targetType}. + * The TypeDescriptors provide additional context about the source and target locations + * where conversion will occur, often object fields or property locations. + * @param source the source object to convert (may be null) + * @param sourceType context about the source type to convert from + * (may be {@code null} if source is {@code null}) + * @param targetType context about the target type to convert to (required) + * @return the converted object, an instance of {@link TypeDescriptor#getObjectType() targetType} + * @throws ConversionException if a conversion exception occurred + * @throws IllegalArgumentException if targetType is {@code null}, + * or {@code sourceType} is {@code null} but source is not {@code null} + */ + Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConverterNotFoundException.java b/spring-core/src/main/java/org/springframework/core/convert/ConverterNotFoundException.java new file mode 100644 index 00000000..ec966b70 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/ConverterNotFoundException.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2010 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; + +/** + * Thrown when a suitable converter could not be found in a conversion service. + * + * @author Keith Donald + * @since 3.0 + */ +@SuppressWarnings("serial") +public final class ConverterNotFoundException extends ConversionException { + + private final TypeDescriptor sourceType; + + private final TypeDescriptor targetType; + + + /** + * Creates a new conversion executor not found exception. + * @param sourceType the source type requested to convert from + * @param targetType the target type requested to convert to + */ + public ConverterNotFoundException(TypeDescriptor sourceType, TypeDescriptor targetType) { + super("No converter found capable of converting from type " + sourceType + " to type " + targetType); + this.sourceType = sourceType; + this.targetType = targetType; + } + + + /** + * Returns the source type that was requested to convert from. + */ + public TypeDescriptor getSourceType() { + return this.sourceType; + } + + /** + * Returns the target type that was requested to convert to. + */ + public TypeDescriptor getTargetType() { + return this.targetType; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/FieldDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/FieldDescriptor.java new file mode 100644 index 00000000..9d951bb3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/FieldDescriptor.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2011 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; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.core.GenericCollectionTypeResolver; + +/** + * @author Keith Donald + * @since 3.1 + */ +class FieldDescriptor extends AbstractDescriptor { + + private final Field field; + + private final int nestingLevel; + + private Map<Integer, Integer> typeIndexesPerLevel; + + + public FieldDescriptor(Field field) { + super(field.getType()); + this.field = field; + this.nestingLevel = 1; + } + + private FieldDescriptor(Class<?> type, Field field, int nestingLevel, int typeIndex, Map<Integer, Integer> typeIndexesPerLevel) { + super(type); + this.field = field; + this.nestingLevel = nestingLevel; + this.typeIndexesPerLevel = typeIndexesPerLevel; + this.typeIndexesPerLevel.put(nestingLevel, typeIndex); + } + + + @Override + public Annotation[] getAnnotations() { + return TypeDescriptor.nullSafeAnnotations(this.field.getAnnotations()); + } + + @Override + protected Class<?> resolveCollectionElementType() { + return GenericCollectionTypeResolver.getCollectionFieldType(this.field, this.nestingLevel, this.typeIndexesPerLevel); + } + + @Override + protected Class<?> resolveMapKeyType() { + return GenericCollectionTypeResolver.getMapKeyFieldType(this.field, this.nestingLevel, this.typeIndexesPerLevel); + } + + @Override + protected Class<?> resolveMapValueType() { + return GenericCollectionTypeResolver.getMapValueFieldType(this.field, this.nestingLevel, this.typeIndexesPerLevel); + } + + @Override + protected AbstractDescriptor nested(Class<?> type, int typeIndex) { + if (this.typeIndexesPerLevel == null) { + this.typeIndexesPerLevel = new HashMap<Integer, Integer>(4); + } + return new FieldDescriptor(type, this.field, this.nestingLevel + 1, typeIndex, this.typeIndexesPerLevel); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/ParameterDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/ParameterDescriptor.java new file mode 100644 index 00000000..c4ccebe7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/ParameterDescriptor.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2014 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; + +import java.lang.annotation.Annotation; + +import org.springframework.core.GenericCollectionTypeResolver; +import org.springframework.core.MethodParameter; + +/** + * @author Keith Donald + * @since 3.1 + */ +class ParameterDescriptor extends AbstractDescriptor { + + private final MethodParameter methodParameter; + + + public ParameterDescriptor(MethodParameter methodParameter) { + super(methodParameter.getParameterType()); + this.methodParameter = methodParameter; + } + + private ParameterDescriptor(Class<?> type, MethodParameter methodParameter) { + super(type); + this.methodParameter = methodParameter; + } + + + @Override + public Annotation[] getAnnotations() { + if (this.methodParameter.getParameterIndex() == -1) { + return TypeDescriptor.nullSafeAnnotations(this.methodParameter.getMethodAnnotations()); + } + else { + return TypeDescriptor.nullSafeAnnotations(this.methodParameter.getParameterAnnotations()); + } + } + + @Override + protected Class<?> resolveCollectionElementType() { + return GenericCollectionTypeResolver.getCollectionParameterType(this.methodParameter); + } + + @Override + protected Class<?> resolveMapKeyType() { + return GenericCollectionTypeResolver.getMapKeyParameterType(this.methodParameter); + } + + @Override + protected Class<?> resolveMapValueType() { + return GenericCollectionTypeResolver.getMapValueParameterType(this.methodParameter); + } + + @Override + protected AbstractDescriptor nested(Class<?> type, int typeIndex) { + MethodParameter methodParameter = new MethodParameter(this.methodParameter); + methodParameter.increaseNestingLevel(); + methodParameter.setTypeIndexForCurrentLevel(typeIndex); + return new ParameterDescriptor(type, methodParameter); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/Property.java b/spring-core/src/main/java/org/springframework/core/convert/Property.java new file mode 100644 index 00000000..84258e9d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/Property.java @@ -0,0 +1,266 @@ +/* + * Copyright 2002-2014 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; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * A description of a JavaBeans Property that allows us to avoid a dependency on + * {@code java.beans.PropertyDescriptor}. The {@code java.beans} package + * is not available in a number of environments (e.g. Android, Java ME), so this is + * desirable for portability of Spring's core conversion facility. + * + * <p>Used to build a TypeDescriptor from a property location. + * The built TypeDescriptor can then be used to convert from/to the property type. + * + * @author Keith Donald + * @author Phillip Webb + * @since 3.1 + * @see TypeDescriptor#TypeDescriptor(Property) + * @see TypeDescriptor#nested(Property, int) + */ +public final class Property { + + private static Map<Property, Annotation[]> annotationCache = + new ConcurrentReferenceHashMap<Property, Annotation[]>(); + + private final Class<?> objectType; + + private final Method readMethod; + + private final Method writeMethod; + + private final String name; + + private final MethodParameter methodParameter; + + private Annotation[] annotations; + + + public Property(Class<?> objectType, Method readMethod, Method writeMethod) { + this(objectType, readMethod, writeMethod, null); + } + + public Property(Class<?> objectType, Method readMethod, Method writeMethod, String name) { + this.objectType = objectType; + this.readMethod = readMethod; + this.writeMethod = writeMethod; + this.methodParameter = resolveMethodParameter(); + this.name = (name == null ? resolveName() : name); + } + + + /** + * The object declaring this property, either directly or in a superclass the object extends. + */ + public Class<?> getObjectType() { + return this.objectType; + } + + /** + * The name of the property: e.g. 'foo' + */ + public String getName() { + return this.name; + } + + /** + * The property type: e.g. {@code java.lang.String} + */ + public Class<?> getType() { + return this.methodParameter.getParameterType(); + } + + /** + * The property getter method: e.g. {@code getFoo()} + */ + public Method getReadMethod() { + return this.readMethod; + } + + /** + * The property setter method: e.g. {@code setFoo(String)} + */ + public Method getWriteMethod() { + return this.writeMethod; + } + + + // package private + + MethodParameter getMethodParameter() { + return this.methodParameter; + } + + Annotation[] getAnnotations() { + if (this.annotations == null) { + this.annotations = resolveAnnotations(); + } + return this.annotations; + } + + + // internal helpers + + private String resolveName() { + if (this.readMethod != null) { + int index = this.readMethod.getName().indexOf("get"); + if (index != -1) { + index += 3; + } + else { + index = this.readMethod.getName().indexOf("is"); + if (index == -1) { + throw new IllegalArgumentException("Not a getter method"); + } + index += 2; + } + return StringUtils.uncapitalize(this.readMethod.getName().substring(index)); + } + else { + int index = this.writeMethod.getName().indexOf("set") + 3; + if (index == -1) { + throw new IllegalArgumentException("Not a setter method"); + } + return StringUtils.uncapitalize(this.writeMethod.getName().substring(index)); + } + } + + private MethodParameter resolveMethodParameter() { + MethodParameter read = resolveReadMethodParameter(); + MethodParameter write = resolveWriteMethodParameter(); + if (write == null) { + if (read == null) { + throw new IllegalStateException("Property is neither readable nor writeable"); + } + return read; + } + if (read != null) { + Class<?> readType = read.getParameterType(); + Class<?> writeType = write.getParameterType(); + if (!writeType.equals(readType) && writeType.isAssignableFrom(readType)) { + return read; + } + } + return write; + } + + private MethodParameter resolveReadMethodParameter() { + if (getReadMethod() == null) { + return null; + } + return resolveParameterType(new MethodParameter(getReadMethod(), -1)); + } + + private MethodParameter resolveWriteMethodParameter() { + if (getWriteMethod() == null) { + return null; + } + return resolveParameterType(new MethodParameter(getWriteMethod(), 0)); + } + + private MethodParameter resolveParameterType(MethodParameter parameter) { + // needed to resolve generic property types that parameterized by sub-classes e.g. T getFoo(); + GenericTypeResolver.resolveParameterType(parameter, getObjectType()); + return parameter; + } + + private Annotation[] resolveAnnotations() { + Annotation[] annotations = annotationCache.get(this); + if (annotations == null) { + Map<Class<? extends Annotation>, Annotation> annotationMap = new LinkedHashMap<Class<? extends Annotation>, Annotation>(); + addAnnotationsToMap(annotationMap, getReadMethod()); + addAnnotationsToMap(annotationMap, getWriteMethod()); + addAnnotationsToMap(annotationMap, getField()); + annotations = annotationMap.values().toArray(new Annotation[annotationMap.size()]); + annotationCache.put(this, annotations); + } + return annotations; + } + + private void addAnnotationsToMap( + Map<Class<? extends Annotation>, Annotation> annotationMap, + AnnotatedElement object) { + if (object != null) { + for (Annotation annotation : object.getAnnotations()) { + annotationMap.put(annotation.annotationType(), annotation); + } + } + } + + private Field getField() { + String name = getName(); + if (!StringUtils.hasLength(name)) { + return null; + } + Class<?> declaringClass = declaringClass(); + Field field = ReflectionUtils.findField(declaringClass, name); + if (field == null) { + // Same lenient fallback checking as in CachedIntrospectionResults... + field = ReflectionUtils.findField(declaringClass, + name.substring(0, 1).toLowerCase() + name.substring(1)); + if (field == null) { + field = ReflectionUtils.findField(declaringClass, + name.substring(0, 1).toUpperCase() + name.substring(1)); + } + } + return field; + } + + private Class<?> declaringClass() { + if (getReadMethod() != null) { + return getReadMethod().getDeclaringClass(); + } + else { + return getWriteMethod().getDeclaringClass(); + } + } + + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Property)) { + return false; + } + Property otherProperty = (Property) other; + return (ObjectUtils.nullSafeEquals(this.objectType, otherProperty.objectType) && + ObjectUtils.nullSafeEquals(this.name, otherProperty.name) && + ObjectUtils.nullSafeEquals(this.readMethod, otherProperty.readMethod) && + ObjectUtils.nullSafeEquals(this.writeMethod, otherProperty.writeMethod)); + } + + @Override + public int hashCode() { + return (ObjectUtils.nullSafeHashCode(this.objectType) * 31 + ObjectUtils.nullSafeHashCode(this.name)); + } + +} 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 new file mode 100644 index 00000000..39d0f847 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -0,0 +1,703 @@ +/* + * Copyright 2002-2014 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; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Context about a type to convert from or to. + * + * @author Keith Donald + * @author Andy Clement + * @author Juergen Hoeller + * @author Phillip Webb + * @author Sam Brannen + * @since 3.0 + */ +@SuppressWarnings("serial") +public class TypeDescriptor implements Serializable { + + static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + + private static final Map<Class<?>, TypeDescriptor> typeDescriptorCache = new HashMap<Class<?>, TypeDescriptor>(18); + + static { + typeDescriptorCache.put(boolean.class, new TypeDescriptor(boolean.class)); + typeDescriptorCache.put(Boolean.class, new TypeDescriptor(Boolean.class)); + typeDescriptorCache.put(byte.class, new TypeDescriptor(byte.class)); + typeDescriptorCache.put(Byte.class, new TypeDescriptor(Byte.class)); + typeDescriptorCache.put(char.class, new TypeDescriptor(char.class)); + typeDescriptorCache.put(Character.class, new TypeDescriptor(Character.class)); + typeDescriptorCache.put(double.class, new TypeDescriptor(double.class)); + typeDescriptorCache.put(Double.class, new TypeDescriptor(Double.class)); + typeDescriptorCache.put(int.class, new TypeDescriptor(int.class)); + typeDescriptorCache.put(Integer.class, new TypeDescriptor(Integer.class)); + typeDescriptorCache.put(long.class, new TypeDescriptor(long.class)); + typeDescriptorCache.put(Long.class, new TypeDescriptor(Long.class)); + typeDescriptorCache.put(float.class, new TypeDescriptor(float.class)); + typeDescriptorCache.put(Float.class, new TypeDescriptor(Float.class)); + typeDescriptorCache.put(short.class, new TypeDescriptor(short.class)); + typeDescriptorCache.put(Short.class, new TypeDescriptor(Short.class)); + typeDescriptorCache.put(String.class, new TypeDescriptor(String.class)); + typeDescriptorCache.put(Object.class, new TypeDescriptor(Object.class)); + } + + + private final Class<?> type; + + private final TypeDescriptor elementTypeDescriptor; + + private final TypeDescriptor mapKeyTypeDescriptor; + + private final TypeDescriptor mapValueTypeDescriptor; + + private final Annotation[] annotations; + + + /** + * Create a new type descriptor from a {@link MethodParameter}. + * <p>Use this constructor when a source or target conversion point is a + * constructor parameter, method parameter, or method return value. + * @param methodParameter the method parameter + */ + public TypeDescriptor(MethodParameter methodParameter) { + this(new ParameterDescriptor(methodParameter)); + } + + /** + * Create a new type descriptor from a {@link Field}. + * <p>Use this constructor when a source or target conversion point is a field. + * @param field the field + */ + public TypeDescriptor(Field field) { + this(new FieldDescriptor(field)); + } + + /** + * Create a new type descriptor from a {@link Property}. + * <p>Use this constructor when a source or target conversion point is a + * property on a Java class. + * @param property the property + */ + public TypeDescriptor(Property property) { + this(new BeanPropertyDescriptor(property)); + } + + + /** + * Create a new type descriptor from the given type. + * <p>Use this to instruct the conversion system to convert an object to a + * specific target type, when no type location such as a method parameter or + * field is available to provide additional conversion context. + * <p>Generally prefer use of {@link #forObject(Object)} for constructing type + * descriptors from source objects, as it handles the {@code null} object case. + * @param type the class + * @return the type descriptor + */ + public static TypeDescriptor valueOf(Class<?> type) { + if (type == null) { + type = Object.class; + } + TypeDescriptor desc = typeDescriptorCache.get(type); + return (desc != null ? desc : new TypeDescriptor(type)); + } + + /** + * Create a new type descriptor from a {@link java.util.Collection} type. + * <p>Useful for converting to typed Collections. + * <p>For example, a {@code List<String>} could be converted to a + * {@code List<EmailAddress>} by converting to a targetType built with this method. + * The method call to construct such a {@code TypeDescriptor} would look something + * like: {@code collection(List.class, TypeDescriptor.valueOf(EmailAddress.class));} + * @param collectionType the collection type, which must implement {@link Collection}. + * @param elementTypeDescriptor a descriptor for the collection's element type, + * used to convert collection elements + * @return the collection type descriptor + */ + public static TypeDescriptor collection(Class<?> collectionType, TypeDescriptor elementTypeDescriptor) { + if (!Collection.class.isAssignableFrom(collectionType)) { + throw new IllegalArgumentException("collectionType must be a java.util.Collection"); + } + return new TypeDescriptor(collectionType, elementTypeDescriptor); + } + + /** + * Create a new type descriptor from a {@link java.util.Map} type. + * <p>Useful for converting to typed Maps. + * <p>For example, a Map<String, String> could be converted to a Map<Id, EmailAddress> + * by converting to a targetType built with this method: + * The method call to construct such a TypeDescriptor would look something like: + * map(Map.class, TypeDescriptor.valueOf(Id.class), TypeDescriptor.valueOf(EmailAddress.class)); + * @param mapType the map type, which must implement {@link Map} + * @param keyTypeDescriptor a descriptor for the map's key type, used to convert map keys + * @param valueTypeDescriptor the map's value type, used to convert map values + * @return the map type descriptor + */ + public static TypeDescriptor map(Class<?> mapType, TypeDescriptor keyTypeDescriptor, TypeDescriptor valueTypeDescriptor) { + if (!Map.class.isAssignableFrom(mapType)) { + throw new IllegalArgumentException("mapType must be a java.util.Map"); + } + return new TypeDescriptor(mapType, keyTypeDescriptor, valueTypeDescriptor); + } + + /** + * Create a new type descriptor as an array of the specified type. + * <p>For example to create a {@code Map<String,String>[]} use + * {@code TypeDescriptor.array(TypeDescriptor.map(Map.class, TypeDescriptor.value(String.class), TypeDescriptor.value(String.class)))}. + * @param elementTypeDescriptor the {@link TypeDescriptor} of the array element or {@code null} + * @return an array {@link TypeDescriptor} or {@code null} if {@code elementTypeDescriptor} is {@code null} + * @since 3.2.1 + */ + public static TypeDescriptor array(TypeDescriptor elementTypeDescriptor) { + if (elementTypeDescriptor == null) { + return null; + } + Class<?> type = Array.newInstance(elementTypeDescriptor.getType(), 0).getClass(); + return new TypeDescriptor(type, elementTypeDescriptor, null, null, elementTypeDescriptor.getAnnotations()); + } + + /** + * Creates a type descriptor for a nested type declared within the method parameter. + * <p>For example, if the methodParameter is a {@code List<String>} and the + * nesting level is 1, the nested type descriptor will be String.class. + * <p>If the methodParameter is a {@code List<List<String>>} and the nesting + * level is 2, the nested type descriptor will also be a String.class. + * <p>If the methodParameter is a {@code Map<Integer, String>} and the nesting + * level is 1, the nested type descriptor will be String, derived from the map value. + * <p>If the methodParameter is a {@code List<Map<Integer, String>>} and the + * nesting level is 2, the nested type descriptor will be String, derived from the map value. + * <p>Returns {@code null} if a nested type cannot be obtained because it was not declared. + * For example, if the method parameter is a {@code List<?>}, the nested type + * descriptor returned will be {@code null}. + * @param methodParameter the method parameter with a nestingLevel of 1 + * @param nestingLevel the nesting level of the collection/array element or + * map key/value declaration within the method parameter + * @return the nested type descriptor at the specified nesting level, or null + * if it could not be obtained + * @throws IllegalArgumentException if the nesting level of the input + * {@link MethodParameter} argument is not 1, or if the types up to the + * specified nesting level are not of collection, array, or map types + */ + public static TypeDescriptor nested(MethodParameter methodParameter, int nestingLevel) { + if (methodParameter.getNestingLevel() != 1) { + throw new IllegalArgumentException("methodParameter nesting level must be 1: " + + "use the nestingLevel parameter to specify the desired nestingLevel for nested type traversal"); + } + return nested(new ParameterDescriptor(methodParameter), nestingLevel); + } + + /** + * Creates a type descriptor for a nested type declared within the field. + * <p>For example, if the field is a {@code List<String>} and the nesting + * level is 1, the nested type descriptor will be {@code String.class}. + * <p>If the field is a {@code List<List<String>>} and the nesting level is + * 2, the nested type descriptor will also be a {@code String.class}. + * <p>If the field is a {@code Map<Integer, String>} and the nesting level + * is 1, the nested type descriptor will be String, derived from the map value. + * <p>If the field is a {@code List<Map<Integer, String>>} and the nesting + * level is 2, the nested type descriptor will be String, derived from the map value. + * <p>Returns {@code null} if a nested type cannot be obtained because it was not declared. + * For example, if the field is a {@code List<?>}, the nested type descriptor returned will be {@code null}. + * @param field the field + * @param nestingLevel the nesting level of the collection/array element or + * map key/value declaration within the field + * @return the nested type descriptor at the specified nesting level, or null + * if it could not be obtained + * @throws IllegalArgumentException if the types up to the specified nesting + * level are not of collection, array, or map types + */ + public static TypeDescriptor nested(Field field, int nestingLevel) { + return nested(new FieldDescriptor(field), nestingLevel); + } + + /** + * Creates a type descriptor for a nested type declared within the property. + * <p>For example, if the property is a {@code List<String>} and the nesting + * level is 1, the nested type descriptor will be {@code String.class}. + * <p>If the property is a {@code List<List<String>>} and the nesting level + * is 2, the nested type descriptor will also be a {@code String.class}. + * <p>If the property is a {@code Map<Integer, String>} and the nesting level + * is 1, the nested type descriptor will be String, derived from the map value. + * <p>If the property is a {@code List<Map<Integer, String>>} and the nesting + * level is 2, the nested type descriptor will be String, derived from the map value. + * <p>Returns {@code null} if a nested type cannot be obtained because it was not declared. + * For example, if the property is a {@code List<?>}, the nested type descriptor + * returned will be {@code null}. + * @param property the property + * @param nestingLevel the nesting level of the collection/array element or + * map key/value declaration within the property + * @return the nested type descriptor at the specified nesting level, or + * {@code null} if it could not be obtained + * @throws IllegalArgumentException if the types up to the specified nesting + * level are not of collection, array, or map types + */ + public static TypeDescriptor nested(Property property, int nestingLevel) { + return nested(new BeanPropertyDescriptor(property), nestingLevel); + } + + /** + * Create a new type descriptor for an object. + * <p>Use this factory method to introspect a source object before asking the + * conversion system to convert it to some another type. + * <p>If the provided object is null, returns null, else calls {@link #valueOf(Class)} + * to build a TypeDescriptor from the object's class. + * @param source the source object + * @return the type descriptor + */ + public static TypeDescriptor forObject(Object source) { + return (source != null ? valueOf(source.getClass()) : null); + } + + + /** + * The type of the backing class, method parameter, field, or property described by this TypeDescriptor. + * <p>Returns primitive types as-is. + * <p>See {@link #getObjectType()} for a variation of this operation that resolves primitive types + * to their corresponding Object types if necessary. + * @return the type, or {@code null} + * @see #getObjectType() + */ + public Class<?> getType() { + return this.type; + } + + /** + * Variation of {@link #getType()} that accounts for a primitive type by returning its object wrapper type. + * <p>This is useful for conversion service implementations that wish to normalize to object-based types + * and not work with primitive types directly. + */ + public Class<?> getObjectType() { + return ClassUtils.resolvePrimitiveIfNecessary(getType()); + } + + /** + * Narrows this {@link TypeDescriptor} by setting its type to the class of the provided value. + * <p>If the value is {@code null}, no narrowing is performed and this TypeDescriptor is returned unchanged. + * <p>Designed to be called by binding frameworks when they read property, field, or method return values. + * Allows such frameworks to narrow a TypeDescriptor built from a declared property, field, or method return + * value type. For example, a field declared as {@code java.lang.Object} would be narrowed to + * {@code java.util.HashMap} if it was set to a {@code java.util.HashMap} value. The narrowed + * TypeDescriptor can then be used to convert the HashMap to some other type. Annotation and + * nested type context is preserved by the narrowed copy. + * @param value the value to use for narrowing this type descriptor + * @return this TypeDescriptor narrowed (returns a copy with its type updated to the class of the provided value) + */ + public TypeDescriptor narrow(Object value) { + if (value == null) { + return this; + } + return new TypeDescriptor(value.getClass(), this.elementTypeDescriptor, + this.mapKeyTypeDescriptor, this.mapValueTypeDescriptor, this.annotations); + } + + /** + * Cast this {@link TypeDescriptor} to a superclass or implemented interface + * preserving annotations and nested type context. + * @param superType the super type to cast to (can be {@code null} + * @return a new TypeDescriptor for the up-cast type + * @throws IllegalArgumentException if this type is not assignable to the super-type + * @since 3.2 + */ + public TypeDescriptor upcast(Class<?> superType) { + if (superType == null) { + return null; + } + Assert.isAssignable(superType, getType()); + return new TypeDescriptor(superType, this.elementTypeDescriptor, + this.mapKeyTypeDescriptor, this.mapValueTypeDescriptor, this.annotations); + } + + /** + * Returns the name of this type: the fully qualified class name. + */ + public String getName() { + return ClassUtils.getQualifiedName(getType()); + } + + /** + * Is this type a primitive type? + */ + public boolean isPrimitive() { + return getType().isPrimitive(); + } + + /** + * The annotations associated with this type descriptor, if any. + * @return the annotations, or an empty array if none + */ + public Annotation[] getAnnotations() { + return this.annotations; + } + + /** + * Determine if this type descriptor has the specified annotation. + * @param annotationType the annotation type + * @return <tt>true</tt> if the annotation is present + */ + public boolean hasAnnotation(Class<? extends Annotation> annotationType) { + return getAnnotation(annotationType) != null; + } + + /** + * Obtain the annotation associated with this type descriptor of the specified type. + * @param annotationType the annotation type + * @return the annotation, or {@code null} if no such annotation exists on this type descriptor + */ + @SuppressWarnings("unchecked") + public <T extends Annotation> T getAnnotation(Class<T> annotationType) { + for (Annotation annotation : this.annotations) { + if (annotation.annotationType().equals(annotationType)) { + return (T) annotation; + } + } + for (Annotation metaAnn : this.annotations) { + T ann = metaAnn.annotationType().getAnnotation(annotationType); + if (ann != null) { + return ann; + } + } + return null; + } + + /** + * Returns true if an object of this type descriptor can be assigned to the location described by the + * given type descriptor. + * <p>For example, valueOf(String.class).isAssignableTo(valueOf(CharSequence.class)) returns true + * because a String value can be assigned to a CharSequence variable. On the other hand, + * valueOf(Number.class).isAssignableTo(valueOf(Integer.class)) returns false because, + * while all Integers are Numbers, not all Numbers are Integers. + * <p>For arrays, collections, and maps, element and key/value types are checked if declared. + * For example, a List<String> field value is assignable to a Collection<CharSequence> + * field, but List<Number> is not assignable to List<Integer>. + * @return true if this type is assignable to the type represented by the provided type descriptor + * @see #getObjectType() + */ + public boolean isAssignableTo(TypeDescriptor typeDescriptor) { + boolean typesAssignable = typeDescriptor.getObjectType().isAssignableFrom(getObjectType()); + if (!typesAssignable) { + return false; + } + if (isArray() && typeDescriptor.isArray()) { + return getElementTypeDescriptor().isAssignableTo(typeDescriptor.getElementTypeDescriptor()); + } + else if (isCollection() && typeDescriptor.isCollection()) { + return isNestedAssignable(getElementTypeDescriptor(), typeDescriptor.getElementTypeDescriptor()); + } + else if (isMap() && typeDescriptor.isMap()) { + return isNestedAssignable(getMapKeyTypeDescriptor(), typeDescriptor.getMapKeyTypeDescriptor()) && + isNestedAssignable(getMapValueTypeDescriptor(), typeDescriptor.getMapValueTypeDescriptor()); + } + else { + return true; + } + } + + + // indexable type descriptor operations + + /** + * Is this type a {@link Collection} type? + */ + public boolean isCollection() { + return Collection.class.isAssignableFrom(getType()); + } + + /** + * Is this type an array type? + */ + public boolean isArray() { + return getType().isArray(); + } + + /** + * If this type is an array, returns the array's component type. + * If this type is a {@link Collection} and it is parameterized, returns the Collection's element type. + * If the Collection is not parameterized, returns null indicating the element type is not declared. + * @return the array component type or Collection element type, or {@code null} if this type is a + * Collection but its element type is not parameterized + * @throws IllegalStateException if this type is not a java.util.Collection or Array type + */ + public TypeDescriptor getElementTypeDescriptor() { + assertCollectionOrArray(); + return this.elementTypeDescriptor; + } + + /** + * If this type is a {@link Collection} or an Array, creates a element TypeDescriptor from the provided + * collection or array element. + * <p>Narrows the {@link #getElementTypeDescriptor() elementType} property to the class of the provided + * collection or array element. For example, if this describes a java.util.List<java.lang.Number< + * and the element argument is a java.lang.Integer, the returned TypeDescriptor will be java.lang.Integer. + * If this describes a java.util.List<?> and the element argument is a java.lang.Integer, the returned + * TypeDescriptor will be java.lang.Integer as well. + * <p>Annotation and nested type context will be preserved in the narrowed TypeDescriptor that is returned. + * @param element the collection or array element + * @return a element type descriptor, narrowed to the type of the provided element + * @throws IllegalStateException if this type is not a java.util.Collection or Array type + * @see #narrow(Object) + */ + public TypeDescriptor elementTypeDescriptor(Object element) { + return narrow(element, getElementTypeDescriptor()); + } + + + // map type descriptor operations + + /** + * Is this type a {@link Map} type? + */ + public boolean isMap() { + return Map.class.isAssignableFrom(getType()); + } + + /** + * If this type is a {@link Map} and its key type is parameterized, returns the map's key type. + * If the Map's key type is not parameterized, returns null indicating the key type is not declared. + * @return the Map key type, or {@code null} if this type is a Map but its key type is not parameterized + * @throws IllegalStateException if this type is not a java.util.Map + */ + public TypeDescriptor getMapKeyTypeDescriptor() { + assertMap(); + return this.mapKeyTypeDescriptor; + } + + /** + * If this type is a {@link Map}, creates a mapKey {@link TypeDescriptor} from the provided map key. + * <p>Narrows the {@link #getMapKeyTypeDescriptor() mapKeyType} property to the class of the provided map key. + * For example, if this describes a java.util.Map<java.lang.Number, java.lang.String< and the key argument + * is a java.lang.Integer, the returned TypeDescriptor will be java.lang.Integer. + * <p>If this describes a java.util.Map<?, ?> and the key argument is a java.lang.Integer, the returned + * TypeDescriptor will be java.lang.Integer as well. + * <p>Annotation and nested type context will be preserved in the narrowed TypeDescriptor that is returned. + * @param mapKey the map key + * @return the map key type descriptor + * @throws IllegalStateException if this type is not a java.util.Map + * @see #narrow(Object) + */ + public TypeDescriptor getMapKeyTypeDescriptor(Object mapKey) { + return narrow(mapKey, getMapKeyTypeDescriptor()); + } + + /** + * If this type is a {@link Map} and its value type is parameterized, returns the map's value type. + * <p>If the Map's value type is not parameterized, returns null indicating the value type is not declared. + * @return the Map value type, or {@code null} if this type is a Map but its value type is not parameterized + * @throws IllegalStateException if this type is not a java.util.Map + */ + public TypeDescriptor getMapValueTypeDescriptor() { + assertMap(); + return this.mapValueTypeDescriptor; + } + + /** + * If this type is a {@link Map}, creates a mapValue {@link TypeDescriptor} from the provided map value. + * <p>Narrows the {@link #getMapValueTypeDescriptor() mapValueType} property to the class of the provided + * map value. For example, if this describes a java.util.Map<java.lang.String, java.lang.Number< + * and the value argument is a java.lang.Integer, the returned TypeDescriptor will be java.lang.Integer. + * If this describes a java.util.Map<?, ?> and the value argument is a java.lang.Integer, the + * returned TypeDescriptor will be java.lang.Integer as well. + * <p>Annotation and nested type context will be preserved in the narrowed TypeDescriptor that is returned. + * @param mapValue the map value + * @return the map value type descriptor + * @throws IllegalStateException if this type is not a java.util.Map + */ + public TypeDescriptor getMapValueTypeDescriptor(Object mapValue) { + return narrow(mapValue, getMapValueTypeDescriptor()); + } + + + // deprecations in Spring 3.1 + + /** + * Returns the value of {@link TypeDescriptor#getType() getType()} for the + * {@link #getElementTypeDescriptor() elementTypeDescriptor}. + * @deprecated in Spring 3.1 in favor of {@link #getElementTypeDescriptor()} + * @throws IllegalStateException if this type is not a java.util.Collection or Array type + */ + @Deprecated + public Class<?> getElementType() { + return getElementTypeDescriptor().getType(); + } + + /** + * Returns the value of {@link TypeDescriptor#getType() getType()} for the + * {@link #getMapKeyTypeDescriptor() getMapKeyTypeDescriptor}. + * @deprecated in Spring 3.1 in favor of {@link #getMapKeyTypeDescriptor()} + * @throws IllegalStateException if this type is not a java.util.Map + */ + @Deprecated + public Class<?> getMapKeyType() { + return getMapKeyTypeDescriptor().getType(); + } + + /** + * Returns the value of {@link TypeDescriptor#getType() getType()} for the + * {@link #getMapValueTypeDescriptor() getMapValueTypeDescriptor}. + * @deprecated in Spring 3.1 in favor of {@link #getMapValueTypeDescriptor()} + * @throws IllegalStateException if this type is not a java.util.Map + */ + @Deprecated + public Class<?> getMapValueType() { + return getMapValueTypeDescriptor().getType(); + } + + + // internal constructors + + private TypeDescriptor(Class<?> type) { + this(new ClassDescriptor(type)); + } + + private TypeDescriptor(Class<?> collectionType, TypeDescriptor elementTypeDescriptor) { + this(collectionType, elementTypeDescriptor, null, null, EMPTY_ANNOTATION_ARRAY); + } + + private TypeDescriptor(Class<?> mapType, TypeDescriptor keyTypeDescriptor, TypeDescriptor valueTypeDescriptor) { + this(mapType, null, keyTypeDescriptor, valueTypeDescriptor, EMPTY_ANNOTATION_ARRAY); + } + + private TypeDescriptor(Class<?> type, TypeDescriptor elementTypeDescriptor, TypeDescriptor mapKeyTypeDescriptor, + TypeDescriptor mapValueTypeDescriptor, Annotation[] annotations) { + + this.type = type; + this.elementTypeDescriptor = elementTypeDescriptor; + this.mapKeyTypeDescriptor = mapKeyTypeDescriptor; + this.mapValueTypeDescriptor = mapValueTypeDescriptor; + this.annotations = annotations; + } + + TypeDescriptor(AbstractDescriptor descriptor) { + this.type = descriptor.getType(); + this.elementTypeDescriptor = descriptor.getElementTypeDescriptor(); + this.mapKeyTypeDescriptor = descriptor.getMapKeyTypeDescriptor(); + this.mapValueTypeDescriptor = descriptor.getMapValueTypeDescriptor(); + this.annotations = descriptor.getAnnotations(); + } + + + // internal helpers + + static Annotation[] nullSafeAnnotations(Annotation[] annotations) { + return (annotations != null ? annotations : EMPTY_ANNOTATION_ARRAY); + } + + private static TypeDescriptor nested(AbstractDescriptor descriptor, int nestingLevel) { + for (int i = 0; i < nestingLevel; i++) { + descriptor = descriptor.nested(); + if (descriptor == null) { + return null; + } + } + return new TypeDescriptor(descriptor); + } + + private void assertCollectionOrArray() { + if (!isCollection() && !isArray()) { + throw new IllegalStateException("Not a java.util.Collection or Array"); + } + } + + private void assertMap() { + if (!isMap()) { + throw new IllegalStateException("Not a java.util.Map"); + } + } + + private TypeDescriptor narrow(Object value, TypeDescriptor typeDescriptor) { + if (typeDescriptor != null) { + return typeDescriptor.narrow(value); + } + else { + return (value != null ? new TypeDescriptor(value.getClass(), null, null, null, this.annotations) : null); + } + } + + private boolean isNestedAssignable(TypeDescriptor nestedTypeDescriptor, TypeDescriptor otherNestedTypeDescriptor) { + if (nestedTypeDescriptor == null || otherNestedTypeDescriptor == null) { + return true; + } + return nestedTypeDescriptor.isAssignableTo(otherNestedTypeDescriptor); + } + + private String wildcard(TypeDescriptor typeDescriptor) { + return (typeDescriptor != null ? typeDescriptor.toString() : "?"); + } + + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof TypeDescriptor)) { + return false; + } + TypeDescriptor other = (TypeDescriptor) obj; + if (!ObjectUtils.nullSafeEquals(this.type, other.type)) { + return false; + } + if (this.annotations.length != other.annotations.length) { + return false; + } + for (Annotation ann : this.annotations) { + if (other.getAnnotation(ann.annotationType()) == null) { + return false; + } + } + if (isCollection() || isArray()) { + return ObjectUtils.nullSafeEquals(this.elementTypeDescriptor, other.elementTypeDescriptor); + } + else if (isMap()) { + return ObjectUtils.nullSafeEquals(this.mapKeyTypeDescriptor, other.mapKeyTypeDescriptor) && + ObjectUtils.nullSafeEquals(this.mapValueTypeDescriptor, other.mapValueTypeDescriptor); + } + else { + return true; + } + } + + public int hashCode() { + return getType().hashCode(); + } + + public String toString() { + StringBuilder builder = new StringBuilder(); + for (Annotation ann : this.annotations) { + builder.append("@").append(ann.annotationType().getName()).append(' '); + } + builder.append(ClassUtils.getQualifiedName(getType())); + if (isMap()) { + builder.append("<").append(wildcard(this.mapKeyTypeDescriptor)); + builder.append(", ").append(wildcard(this.mapValueTypeDescriptor)).append(">"); + } + else if (isCollection()) { + builder.append("<").append(wildcard(this.elementTypeDescriptor)).append(">"); + } + return builder.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalConverter.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalConverter.java new file mode 100644 index 00000000..964139e0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalConverter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2014 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.converter; + +import org.springframework.core.convert.TypeDescriptor; + +/** + * Allows a {@link Converter}, {@link GenericConverter} or {@link ConverterFactory} to + * conditionally execute based on attributes of the {@code source} and {@code target} + * {@link TypeDescriptor}. + * + * <p>Often used to selectively match custom conversion logic based on the presence of a + * field or class-level characteristic, such as an annotation or method. For example, when + * converting from a String field to a Date field, an implementation might return + * {@code true} if the target field has also been annotated with {@code @DateTimeFormat}. + * + * <p>As another example, when converting from a String field to an {@code Account} field, + * an implementation might return {@code true} if the target Account class defines a + * {@code public static findAccount(String)} method. + * + * @author Phillip Webb + * @author Keith Donald + * @since 3.2 + * @see Converter + * @see GenericConverter + * @see ConverterFactory + * @see ConditionalGenericConverter + */ +public interface ConditionalConverter { + + /** + * Should the conversion from {@code sourceType} to {@code targetType} currently under + * consideration be selected? + * @param sourceType the type descriptor of the field we are converting from + * @param targetType the type descriptor of the field we are converting to + * @return true if conversion should be performed, false otherwise + */ + boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalGenericConverter.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalGenericConverter.java new file mode 100644 index 00000000..56dadbfc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalGenericConverter.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2014 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.converter; + +import org.springframework.core.convert.TypeDescriptor; + +/** + * A {@link GenericConverter} that may conditionally execute based on attributes + * of the {@code source} and {@code target} {@link TypeDescriptor}. + * See {@link ConditionalConverter} for details. + * + * @author Keith Donald + * @author Phillip Webb + * @since 3.0 + * @see GenericConverter + * @see ConditionalConverter + */ +public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter { + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java b/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java new file mode 100644 index 00000000..6710b666 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2012 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.converter; + +/** + * A converter converts a source object of type S to a target of type T. + * Implementations of this interface are thread-safe and can be shared. + * + * <p>Implementations may additionally implement {@link ConditionalConverter}. + * + * @author Keith Donald + * @since 3.0 + * @see ConditionalConverter + * @param <S> The source type + * @param <T> The target type + */ +public interface Converter<S, T> { + + /** + * Convert the source of type S to target type T. + * @param source the source object to convert, which must be an instance of S + * @return the converted object, which must be an instance of T + * @throws IllegalArgumentException if the source could not be converted to the desired target type + */ + T convert(S source); + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java new file mode 100644 index 00000000..97ae018d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2012 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.converter; + +/** + * A factory for "ranged" converters that can convert objects from S to subtypes of R. + * + * <p>Implementations may additionally implement {@link ConditionalConverter}. + * + * @author Keith Donald + * @since 3.0 + * @see ConditionalConverter + * @param <S> The source type converters created by this factory can convert from + * @param <R> The target range (or base) type converters created by this factory can convert to; + * for example {@link Number} for a set of number subtypes. + */ +public interface ConverterFactory<S, R> { + + /** + * Get the converter to convert from S to target type T, where T is also an instance of R. + * @param <T> the target type + * @param targetType the target type to convert to + * @return A converter from S to T + */ + <T extends R> Converter<S, T> getConverter(Class<T> targetType); + +} 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 new file mode 100644 index 00000000..9280c4ea --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2009 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.converter; + +/** + * For registering converters with a type conversion system. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +public interface ConverterRegistry { + + /** + * Add a plain converter to this registry. + * The convertible sourceType/targetType 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. + * @since 3.1 + */ + void addConverter(Class<?> sourceType, Class<?> targetType, Converter<?, ?> converter); + + /** + * Add a generic converter to this registry. + */ + void addConverter(GenericConverter converter); + + /** + * Add a ranged converter factory to this registry. + * The convertible sourceType/rangeType pair is derived from the ConverterFactory's parameterized types. + * @throws IllegalArgumentException if the parameterized types could not be resolved. + */ + void addConverterFactory(ConverterFactory<?, ?> converterFactory); + + /** + * Remove any converters from sourceType to targetType. + * @param sourceType the source type + * @param targetType the target type + */ + void removeConvertible(Class<?> sourceType, Class<?> targetType); + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConvertingComparator.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConvertingComparator.java new file mode 100644 index 00000000..3e2d7415 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConvertingComparator.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2012 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.converter; + +import java.util.Comparator; +import java.util.Map; + +import org.springframework.core.convert.ConversionService; +import org.springframework.util.Assert; +import org.springframework.util.comparator.ComparableComparator; + +/** + * A {@link Comparator} that converts values before they are compared. The specified + * {@link Converter} will be used to convert each value before it passed to the underlying + * {@code Comparator}. + * + * @author Phillip Webb + * @param <S> the source type + * @param <T> the target type + * @since 3.2 + */ +public class ConvertingComparator<S, T> implements Comparator<S> { + + private Comparator<T> comparator; + + private Converter<S, T> converter; + + + /** + * Create a new {@link ConvertingComparator} instance. + * + * @param converter the converter + */ + @SuppressWarnings("unchecked") + public ConvertingComparator(Converter<S, T> converter) { + this(ComparableComparator.INSTANCE, converter); + } + + /** + * Create a new {@link ConvertingComparator} instance. + * + * @param comparator the underlying comparator used to compare the converted values + * @param converter the converter + */ + public ConvertingComparator(Comparator<T> comparator, Converter<S, T> converter) { + Assert.notNull(comparator, "Comparator must not be null"); + Assert.notNull(converter, "Converter must not be null"); + this.comparator = comparator; + this.converter = converter; + } + + /** + * Create a new {@link ComparableComparator} instance. + * + * @param comparator the underlying comparator + * @param conversionService the conversion service + * @param targetType the target type + */ + public ConvertingComparator(Comparator<T> comparator, + ConversionService conversionService, Class<? extends T> targetType) { + this(comparator, new ConversionServiceConverter<S, T>( + conversionService, targetType)); + } + + + public int compare(S o1, S o2) { + T c1 = this.converter.convert(o1); + T c2 = this.converter.convert(o2); + return this.comparator.compare(c1, c2); + } + + /** + * Create a new {@link ConvertingComparator} that compares {@link java.util.Map.Entry + * map * entries} based on their {@link java.util.Map.Entry#getKey() keys}. + * + * @param comparator the underlying comparator used to compare keys + * @return a new {@link ConvertingComparator} instance + */ + public static <K, V> ConvertingComparator<Map.Entry<K, V>, K> mapEntryKeys( + Comparator<K> comparator) { + return new ConvertingComparator<Map.Entry<K,V>, K>(comparator, new Converter<Map.Entry<K, V>, K>() { + + public K convert(Map.Entry<K, V> source) { + return source.getKey(); + } + }); + } + + /** + * Create a new {@link ConvertingComparator} that compares {@link java.util.Map.Entry + * map entries} based on their {@link java.util.Map.Entry#getValue() values}. + * + * @param comparator the underlying comparator used to compare values + * @return a new {@link ConvertingComparator} instance + */ + public static <K, V> ConvertingComparator<Map.Entry<K, V>, V> mapEntryValues( + Comparator<V> comparator) { + return new ConvertingComparator<Map.Entry<K,V>, V>(comparator, new Converter<Map.Entry<K, V>, V>() { + + public V convert(Map.Entry<K, V> source) { + return source.getValue(); + } + }); + } + + + /** + * Adapts a {@link ConversionService} and <tt>targetType</tt> to a {@link Converter}. + */ + private static class ConversionServiceConverter<S, T> implements Converter<S, T> { + + private final ConversionService conversionService; + + private final Class<? extends T> targetType; + + public ConversionServiceConverter(ConversionService conversionService, + Class<? extends T> targetType) { + Assert.notNull(conversionService, "ConversionService must not be null"); + Assert.notNull(targetType, "TargetType must not be null"); + this.conversionService = conversionService; + this.targetType = targetType; + } + + public T convert(S source) { + return this.conversionService.convert(source, this.targetType); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/GenericConverter.java b/spring-core/src/main/java/org/springframework/core/convert/converter/GenericConverter.java new file mode 100644 index 00000000..5258497b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/GenericConverter.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2014 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.converter; + +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.util.Assert; + +/** + * Generic converter interface for converting between two or more types. + * + * <p>This is the most flexible of the Converter SPI interfaces, but also the most complex. + * It is flexible in that a GenericConverter may support converting between multiple source/target + * type pairs (see {@link #getConvertibleTypes()}. In addition, GenericConverter implementations + * have access to source/target {@link TypeDescriptor field context} during the type conversion process. + * This allows for resolving source and target field metadata such as annotations and generics + * information, which can be used influence the conversion logic. + * + * <p>This interface should generally not be used when the simpler {@link Converter} or + * {@link ConverterFactory} interfaces are sufficient. + * + * <p>Implementations may additionally implement {@link ConditionalConverter}. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + * @see TypeDescriptor + * @see Converter + * @see ConverterFactory + * @see ConditionalConverter + */ +public interface GenericConverter { + + /** + * Return the source and target types which this converter can convert between. Each + * entry is a convertible source-to-target type pair. + * <p> + * For {@link ConditionalConverter conditional} converters this method may return + * {@code null} to indicate all source-to-target pairs should be considered. * + */ + Set<ConvertiblePair> getConvertibleTypes(); + + /** + * Convert the source to the targetType described by the TypeDescriptor. + * @param source the source object to convert (may be null) + * @param sourceType the type descriptor of the field we are converting from + * @param targetType the type descriptor of the field we are converting to + * @return the converted object + */ + Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); + + + /** + * Holder for a source-to-target class pair. + */ + public static final class ConvertiblePair { + + private final Class<?> sourceType; + + private final Class<?> targetType; + + /** + * Create a new source-to-target pair. + * @param sourceType the source type + * @param targetType the target type + */ + public ConvertiblePair(Class<?> sourceType, Class<?> targetType) { + Assert.notNull(sourceType, "Source type must not be null"); + Assert.notNull(targetType, "Target type must not be null"); + this.sourceType = sourceType; + this.targetType = targetType; + } + + public Class<?> getSourceType() { + return this.sourceType; + } + + public Class<?> getTargetType() { + return this.targetType; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || obj.getClass() != ConvertiblePair.class) { + return false; + } + ConvertiblePair other = (ConvertiblePair) obj; + return this.sourceType.equals(other.sourceType) && this.targetType.equals(other.targetType); + } + + @Override + public int hashCode() { + return this.sourceType.hashCode() * 31 + this.targetType.hashCode(); + } + + @Override + public String toString() { + return this.sourceType.getName() + " -> " + this.targetType.getName(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/package-info.java b/spring-core/src/main/java/org/springframework/core/convert/converter/package-info.java new file mode 100644 index 00000000..a6d95c86 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * SPI to implement Converters for the type conversion system. + * + */ +package org.springframework.core.convert.converter; + diff --git a/spring-core/src/main/java/org/springframework/core/convert/package-info.java b/spring-core/src/main/java/org/springframework/core/convert/package-info.java new file mode 100644 index 00000000..8d8bd415 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * Type conversion system API. + * + */ +package org.springframework.core.convert; + diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java new file mode 100644 index 00000000..b2098494 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2014 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 java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.util.ObjectUtils; + +/** + * Converts an Array to another Array. First adapts the source array to a List, then + * delegates to {@link CollectionToArrayConverter} to perform the target array conversion. + * + * @author Keith Donald + * @author Phillip Webb + * @since 3.0 + */ +final class ArrayToArrayConverter implements ConditionalGenericConverter { + + private final CollectionToArrayConverter helperConverter; + + private final ConversionService conversionService; + + + public ArrayToArrayConverter(ConversionService conversionService) { + this.helperConverter = new CollectionToArrayConverter(conversionService); + this.conversionService = conversionService; + } + + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object[].class, Object[].class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.helperConverter.matches(sourceType, targetType); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (this.conversionService instanceof GenericConversionService && + ((GenericConversionService) this.conversionService).canBypassConvert( + sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor())) { + return source; + } + List<Object> sourceList = Arrays.asList(ObjectUtils.toObjectArray(source)); + return this.helperConverter.convert(sourceList, sourceType, targetType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java new file mode 100644 index 00000000..4e028b49 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2011 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 java.lang.reflect.Array; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Converts an Array to a Collection. + * + * <p>First, creates a new Collection of the requested targetType. + * Then adds each array element to the target collection. + * Will perform an element conversion from the source component type to the collection's parameterized type if necessary. + * + * @author Keith Donald + * @since 3.0 + */ +final class ArrayToCollectionConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + public ArrayToCollectionConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object[].class, Collection.class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements( + sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor(), this.conversionService); + } + + @SuppressWarnings("unchecked") + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + int length = Array.getLength(source); + Collection<Object> target = CollectionFactory.createCollection(targetType.getType(), length); + if (targetType.getElementTypeDescriptor() == null) { + for (int i = 0; i < length; i++) { + Object sourceElement = Array.get(source, i); + target.add(sourceElement); + } + } + else { + for (int i = 0; i < length; i++) { + Object sourceElement = Array.get(source, i); + Object targetElement = this.conversionService.convert(sourceElement, + sourceType.elementTypeDescriptor(sourceElement), targetType.getElementTypeDescriptor()); + target.add(targetElement); + } + } + return target; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToObjectConverter.java new file mode 100644 index 00000000..1db3b2ec --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToObjectConverter.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2011 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 java.lang.reflect.Array; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Converts an Array to an Object by returning the first array element after converting it to the desired targetType. + * + * @author Keith Donald + * @since 3.0 + */ +final class ArrayToObjectConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + public ArrayToObjectConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object[].class, Object.class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType, this.conversionService); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + if (sourceType.isAssignableTo(targetType)) { + return source; + } + if (Array.getLength(source) == 0) { + return null; + } + Object firstElement = Array.get(source, 0); + return this.conversionService.convert(firstElement, sourceType.elementTypeDescriptor(firstElement), targetType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToStringConverter.java new file mode 100644 index 00000000..7ffdb862 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToStringConverter.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2014 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 java.util.Arrays; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.util.ObjectUtils; + +/** + * Converts an Array to a comma-delimited String. + * This implementation first adapts the source Array to a List, + * then delegates to {@link CollectionToStringConverter} to perform the target String conversion. + * + * @author Keith Donald + * @since 3.0 + */ +final class ArrayToStringConverter implements ConditionalGenericConverter { + + private final CollectionToStringConverter helperConverter; + + + public ArrayToStringConverter(ConversionService conversionService) { + this.helperConverter = new CollectionToStringConverter(conversionService); + } + + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object[].class, String.class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.helperConverter.matches(sourceType, targetType); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.helperConverter.convert(Arrays.asList(ObjectUtils.toObjectArray(source)), sourceType, targetType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java new file mode 100644 index 00000000..8f9642b0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2009 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; +import org.springframework.util.NumberUtils; + +/** + * Converts from a Character to any JDK-standard Number implementation. + * + * <p>Support Number classes including Byte, Short, Integer, Float, Double, Long, BigInteger, BigDecimal. This class + * delegates to {@link NumberUtils#convertNumberToTargetClass(Number, Class)} to perform the conversion. + * + * @author Keith Donald + * @since 3.0 + * @see java.lang.Byte + * @see java.lang.Short + * @see java.lang.Integer + * @see java.lang.Long + * @see java.math.BigInteger + * @see java.lang.Float + * @see java.lang.Double + * @see java.math.BigDecimal + * @see NumberUtils + */ +final class CharacterToNumberFactory implements ConverterFactory<Character, Number> { + + public <T extends Number> Converter<Character, T> getConverter(Class<T> targetType) { + return new CharacterToNumber<T>(targetType); + } + + private static final class CharacterToNumber<T extends Number> implements Converter<Character, T> { + + private final Class<T> targetType; + + public CharacterToNumber(Class<T> targetType) { + this.targetType = targetType; + } + + public T convert(Character source) { + return NumberUtils.convertNumberToTargetClass((short) source.charValue(), this.targetType); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToArrayConverter.java new file mode 100644 index 00000000..d616eeb0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToArrayConverter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2011 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 java.lang.reflect.Array; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Converts a Collection to an array. + * + * <p>First, creates a new array of the requested targetType with a length equal to the + * size of the source Collection. Then sets each collection element into the array. + * Will perform an element conversion from the collection's parameterized type to the + * array's component type if necessary. + * + * @author Keith Donald + * @since 3.0 + */ +final class CollectionToArrayConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + public CollectionToArrayConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Collection.class, Object[].class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor(), this.conversionService); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Collection<?> sourceCollection = (Collection<?>) source; + Object array = Array.newInstance(targetType.getElementTypeDescriptor().getType(), sourceCollection.size()); + int i = 0; + for (Object sourceElement : sourceCollection) { + Object targetElement = this.conversionService.convert(sourceElement, sourceType.elementTypeDescriptor(sourceElement), targetType.getElementTypeDescriptor()); + Array.set(array, i++, targetElement); + } + return array; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToCollectionConverter.java new file mode 100644 index 00000000..f0dfd3d6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToCollectionConverter.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2014 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 java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Converts from a Collection to another Collection. + * + * <p>First, creates a new Collection of the requested targetType with a size equal to the + * size of the source Collection. Then copies each element in the source collection to the + * target collection. Will perform an element conversion from the source collection's + * parameterized type to the target collection's parameterized type if necessary. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class CollectionToCollectionConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public CollectionToCollectionConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Collection.class, Collection.class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements( + sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor(), this.conversionService); + } + + @SuppressWarnings("unchecked") + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Collection<?> sourceCollection = (Collection<?>) source; + + // Shortcut if possible... + boolean copyRequired = !targetType.getType().isInstance(source); + if (!copyRequired && sourceCollection.isEmpty()) { + return source; + } + TypeDescriptor elementDesc = targetType.getElementTypeDescriptor(); + if (elementDesc == null && !copyRequired) { + return source; + } + + // At this point, we need a collection copy in any case, even if just for finding out about element copies... + Collection<Object> target = CollectionFactory.createCollection(targetType.getType(), sourceCollection.size()); + if (elementDesc == null) { + target.addAll(sourceCollection); + } + else { + for (Object sourceElement : sourceCollection) { + Object targetElement = this.conversionService.convert(sourceElement, + sourceType.elementTypeDescriptor(sourceElement), elementDesc); + target.add(targetElement); + if (sourceElement != targetElement) { + copyRequired = true; + } + } + } + + return (copyRequired ? target : source); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToObjectConverter.java new file mode 100644 index 00000000..b9913ef6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToObjectConverter.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2009 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 java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Converts a Collection to an Object by returning the first collection element after converting it to the desired targetType. + * + * @author Keith Donald + * @since 3.0 + */ +final class CollectionToObjectConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + public CollectionToObjectConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Collection.class, Object.class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType, this.conversionService); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + if (sourceType.isAssignableTo(targetType)) { + return source; + } + Collection<?> sourceCollection = (Collection<?>) source; + if (sourceCollection.size() == 0) { + return null; + } + Object firstElement = sourceCollection.iterator().next(); + return this.conversionService.convert(firstElement, sourceType.elementTypeDescriptor(firstElement), targetType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToStringConverter.java new file mode 100644 index 00000000..02b9d0eb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToStringConverter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2011 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 java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Converts a Collection to a comma-delimited String. + * + * @author Keith Donald + * @since 3.0 + */ +final class CollectionToStringConverter implements ConditionalGenericConverter { + + private static final String DELIMITER = ","; + + private final ConversionService conversionService; + + public CollectionToStringConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Collection.class, String.class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType, this.conversionService); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Collection<?> sourceCollection = (Collection<?>) source; + if (sourceCollection.size() == 0) { + return ""; + } + StringBuilder sb = new StringBuilder(); + int i = 0; + for (Object sourceElement : sourceCollection) { + if (i > 0) { + sb.append(DELIMITER); + } + Object targetElement = this.conversionService.convert(sourceElement, sourceType.elementTypeDescriptor(sourceElement), targetType); + sb.append(targetElement); + i++; + } + return sb.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ConfigurableConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/ConfigurableConversionService.java new file mode 100644 index 00000000..a6123c0b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConfigurableConversionService.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2011 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.ConverterRegistry; + +/** + * Configuration interface to be implemented by most if not all {@link ConversionService} + * types. Consolidates the read-only operations exposed by {@link ConversionService} and + * the mutating operations of {@link ConverterRegistry} to allow for convenient ad-hoc + * addition and removal of {@link org.springframework.core.convert.converter.Converter + * Converters} through. The latter is particularly useful when working against a + * {@link org.springframework.core.env.ConfigurableEnvironment ConfigurableEnvironment} + * instance in application context bootstrapping code. + * + * @author Chris Beams + * @since 3.1 + * @see org.springframework.core.env.ConfigurablePropertyResolver#getConversionService() + * @see org.springframework.core.env.ConfigurableEnvironment + * @see org.springframework.context.ConfigurableApplicationContext#getEnvironment() + */ +public interface ConfigurableConversionService extends ConversionService, ConverterRegistry { + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java new file mode 100644 index 00000000..8a45f037 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2012 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 java.util.Set; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * A factory for common {@link org.springframework.core.convert.ConversionService} + * configurations. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Chris Beams + * @since 3.0 + */ +public abstract class ConversionServiceFactory { + + /** + * Register the given Converter objects with the given target ConverterRegistry. + * @param converters the converter objects: implementing {@link Converter}, + * {@link ConverterFactory}, or {@link GenericConverter} + * @param registry the target registry + */ + public static void registerConverters(Set<?> converters, ConverterRegistry registry) { + if (converters != null) { + for (Object converter : converters) { + if (converter instanceof GenericConverter) { + registry.addConverter((GenericConverter) converter); + } + else if (converter instanceof Converter<?, ?>) { + registry.addConverter((Converter<?, ?>) converter); + } + else if (converter instanceof ConverterFactory<?, ?>) { + registry.addConverterFactory((ConverterFactory<?, ?>) converter); + } + else { + throw new IllegalArgumentException("Each converter object must implement one of the " + + "Converter, ConverterFactory, or GenericConverter interfaces"); + } + } + } + } + + /** + * Create a new default GenericConversionService instance that can be safely modified. + * @deprecated in Spring 3.1 in favor of {@link DefaultConversionService#DefaultConversionService()} + */ + @Deprecated + public static GenericConversionService createDefaultConversionService() { + return new DefaultConversionService(); + } + + /** + * Populate the given GenericConversionService instance with the set of default converters. + * @deprecated in Spring 3.1 in favor of {@link DefaultConversionService#addDefaultConverters(ConverterRegistry)} + */ + @Deprecated + public static void addDefaultConverters(GenericConversionService conversionService) { + DefaultConversionService.addDefaultConverters(conversionService); + } + +} 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 new file mode 100644 index 00000000..c4d41ec3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2011 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.ConversionFailedException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * Internal utilities for the conversion package. + * + * @author Keith Donald + * @since 3.0 + */ +abstract class ConversionUtils { + + public static Object invokeConverter(GenericConverter converter, Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + try { + return converter.convert(source, sourceType, targetType); + } + catch (ConversionFailedException ex) { + throw ex; + } + catch (Exception ex) { + throw new ConversionFailedException(sourceType, targetType, source, ex); + } + } + + public static boolean canConvertElements(TypeDescriptor sourceElementType, TypeDescriptor targetElementType, ConversionService conversionService) { + if (targetElementType == null) { + // yes + return true; + } + if (sourceElementType == null) { + // maybe + return true; + } + if (conversionService.canConvert(sourceElementType, targetElementType)) { + // yes + return true; + } + else if (sourceElementType.getType().isAssignableFrom(targetElementType.getType())) { + // maybe; + return true; + } + else { + // no; + return false; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ConvertingPropertyEditorAdapter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ConvertingPropertyEditorAdapter.java new file mode 100644 index 00000000..47cadd66 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConvertingPropertyEditorAdapter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2011 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 java.beans.PropertyEditorSupport; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.util.Assert; + +/** + * Adapter that exposes a {@link java.beans.PropertyEditor} for any given + * {@link org.springframework.core.convert.ConversionService} and specific target type. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class ConvertingPropertyEditorAdapter extends PropertyEditorSupport { + + private final ConversionService conversionService; + + private final TypeDescriptor targetDescriptor; + + private final boolean canConvertToString; + + + /** + * Create a new ConvertingPropertyEditorAdapter for a given + * {@link org.springframework.core.convert.ConversionService} + * and the given target type. + * @param conversionService the ConversionService to delegate to + * @param targetDescriptor the target type to convert to + */ + public ConvertingPropertyEditorAdapter(ConversionService conversionService, TypeDescriptor targetDescriptor) { + Assert.notNull(conversionService, "ConversionService must not be null"); + Assert.notNull(targetDescriptor, "TypeDescriptor must not be null"); + this.conversionService = conversionService; + this.targetDescriptor = targetDescriptor; + this.canConvertToString = conversionService.canConvert(this.targetDescriptor, TypeDescriptor.valueOf(String.class)); + } + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(this.conversionService.convert(text, TypeDescriptor.valueOf(String.class), this.targetDescriptor)); + } + + @Override + public String getAsText() { + if (this.canConvertToString) { + return (String) this.conversionService.convert(getValue(), this.targetDescriptor, TypeDescriptor.valueOf(String.class)); + } + else { + return null; + } + } + +} 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 new file mode 100644 index 00000000..86e58098 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2012 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 java.util.Locale; +import java.util.UUID; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.ConverterRegistry; + +/** + * A specialization of {@link GenericConversionService} configured by default with + * converters appropriate for most environments. + * + * <p>Designed for direct instantiation but also exposes the static + * {@link #addDefaultConverters(ConverterRegistry)} utility method for ad hoc use against any + * {@code ConverterRegistry} instance. + * + * @author Chris Beams + * @since 3.1 + */ +public class DefaultConversionService extends GenericConversionService { + + /** + * Create a new {@code DefaultConversionService} with the set of + * {@linkplain DefaultConversionService#addDefaultConverters(ConverterRegistry) default converters}. + */ + public DefaultConversionService() { + addDefaultConverters(this); + } + + // static utility methods + + /** + * Add converters appropriate for most environments. + * @param converterRegistry the registry of converters to add to (must also be castable to ConversionService) + * @throws ClassCastException if the converterRegistry could not be cast to a ConversionService + */ + public static void addDefaultConverters(ConverterRegistry converterRegistry) { + addScalarConverters(converterRegistry); + addCollectionConverters(converterRegistry); + addFallbackConverters(converterRegistry); + } + + // internal helpers + + private static void addScalarConverters(ConverterRegistry converterRegistry) { + ConversionService conversionService = (ConversionService) converterRegistry; + converterRegistry.addConverter(new StringToBooleanConverter()); + converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter()); + + converterRegistry.addConverterFactory(new StringToNumberConverterFactory()); + converterRegistry.addConverter(Number.class, String.class, new ObjectToStringConverter()); + + converterRegistry.addConverterFactory(new NumberToNumberConverterFactory()); + + converterRegistry.addConverter(new StringToCharacterConverter()); + converterRegistry.addConverter(Character.class, String.class, new ObjectToStringConverter()); + + converterRegistry.addConverter(new NumberToCharacterConverter()); + converterRegistry.addConverterFactory(new CharacterToNumberFactory()); + + converterRegistry.addConverterFactory(new StringToEnumConverterFactory()); + converterRegistry.addConverter(Enum.class, String.class, new EnumToStringConverter(conversionService)); + + converterRegistry.addConverter(new StringToLocaleConverter()); + converterRegistry.addConverter(Locale.class, String.class, new ObjectToStringConverter()); + + converterRegistry.addConverter(new PropertiesToStringConverter()); + converterRegistry.addConverter(new StringToPropertiesConverter()); + + converterRegistry.addConverter(new StringToUUIDConverter()); + converterRegistry.addConverter(UUID.class, String.class, new ObjectToStringConverter()); + } + + private static void addCollectionConverters(ConverterRegistry converterRegistry) { + ConversionService conversionService = (ConversionService) converterRegistry; + converterRegistry.addConverter(new ArrayToCollectionConverter(conversionService)); + converterRegistry.addConverter(new CollectionToArrayConverter(conversionService)); + + converterRegistry.addConverter(new ArrayToArrayConverter(conversionService)); + converterRegistry.addConverter(new CollectionToCollectionConverter(conversionService)); + converterRegistry.addConverter(new MapToMapConverter(conversionService)); + + converterRegistry.addConverter(new ArrayToStringConverter(conversionService)); + converterRegistry.addConverter(new StringToArrayConverter(conversionService)); + + converterRegistry.addConverter(new ArrayToObjectConverter(conversionService)); + converterRegistry.addConverter(new ObjectToArrayConverter(conversionService)); + + converterRegistry.addConverter(new CollectionToStringConverter(conversionService)); + converterRegistry.addConverter(new StringToCollectionConverter(conversionService)); + + converterRegistry.addConverter(new CollectionToObjectConverter(conversionService)); + converterRegistry.addConverter(new ObjectToCollectionConverter(conversionService)); + } + + private static void addFallbackConverters(ConverterRegistry converterRegistry) { + ConversionService conversionService = (ConversionService) converterRegistry; + converterRegistry.addConverter(new ObjectToObjectConverter()); + converterRegistry.addConverter(new IdToEntityConverter(conversionService)); + converterRegistry.addConverter(new FallbackObjectToStringConverter()); + } + +} 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 new file mode 100644 index 00000000..e391b75b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/EnumToStringConverter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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.core.convert.converter.Converter; +import org.springframework.util.ClassUtils; + +/** + * Calls {@link Enum#name()} to convert a source Enum to a String. This converter will + * not match enums with interfaces that can be converterd. + * @author Keith Donald + * @author Phillip Webb + * @since 3.0 + */ +final class EnumToStringConverter implements Converter<Enum<?>, String>, ConditionalConverter { + + private final ConversionService conversionService; + + public EnumToStringConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + for (Class<?> interfaceType : ClassUtils.getAllInterfacesForClass(sourceType.getType())) { + if (conversionService.canConvert(TypeDescriptor.valueOf(interfaceType), targetType)) { + return false; + } + } + return true; + } + + public String convert(Enum<?> source) { + return source.name(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/FallbackObjectToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/FallbackObjectToStringConverter.java new file mode 100644 index 00000000..071ad3b6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/FallbackObjectToStringConverter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2012 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 java.io.StringWriter; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Simply calls {@link Object#toString()} to convert any supported Object to a String. + * Supports CharSequence, StringWriter, and any class with a String constructor or {@code valueOf(String)} method. + * + * <p>Used by the default ConversionService as a fallback if there are no other explicit + * to-String converters registered. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class FallbackObjectToStringConverter implements ConditionalGenericConverter { + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object.class, String.class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + Class<?> sourceClass = sourceType.getObjectType(); + if (String.class.equals(sourceClass)) { + return false; + } + return CharSequence.class.isAssignableFrom(sourceClass) || StringWriter.class.isAssignableFrom(sourceClass) || + ObjectToObjectConverter.hasValueOfMethodOrConstructor(sourceClass, String.class); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return (source != null ? source.toString() : null); + } + +} 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 new file mode 100644 index 00000000..9f78f660 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java @@ -0,0 +1,618 @@ +/* + * Copyright 2002-2014 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 java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.convert.ConversionException; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Base {@link ConversionService} implementation suitable for use in most environments. + * Indirectly implements {@link ConverterRegistry} as registration API through the + * {@link ConfigurableConversionService} interface. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Chris Beams + * @author Phillip Webb + * @since 3.0 + */ +public class GenericConversionService implements ConfigurableConversionService { + + /** + * General NO-OP converter used when conversion is not required. + */ + private static final GenericConverter NO_OP_CONVERTER = new NoOpConverter("NO_OP"); + + /** + * Used as a cache entry when no converter is available. This converter is never + * returned. + */ + private static final GenericConverter NO_MATCH = new NoOpConverter("NO_MATCH"); + + + private final Converters converters = new Converters(); + + private final Map<ConverterCacheKey, GenericConverter> converterCache = + new ConcurrentHashMap<ConverterCacheKey, GenericConverter>(64); + + + // implementing ConverterRegistry + + public void addConverter(Converter<?, ?> converter) { + GenericConverter.ConvertiblePair typeInfo = getRequiredTypeInfo(converter, Converter.class); + Assert.notNull(typeInfo, "Unable to the determine sourceType <S> and targetType " + + "<T> which your Converter<S, T> converts between; declare these generic types."); + addConverter(new ConverterAdapter(converter, typeInfo)); + } + + public void addConverter(Class<?> sourceType, Class<?> targetType, Converter<?, ?> converter) { + GenericConverter.ConvertiblePair typeInfo = new GenericConverter.ConvertiblePair(sourceType, targetType); + addConverter(new ConverterAdapter(converter, typeInfo)); + } + + public void addConverter(GenericConverter converter) { + this.converters.add(converter); + invalidateCache(); + } + + public void addConverterFactory(ConverterFactory<?, ?> converterFactory) { + GenericConverter.ConvertiblePair typeInfo = getRequiredTypeInfo(converterFactory, ConverterFactory.class); + if (typeInfo == null) { + throw new IllegalArgumentException("Unable to the determine sourceType <S> and " + + "targetRangeType R which your ConverterFactory<S, R> converts between; " + + "declare these generic types."); + } + addConverter(new ConverterFactoryAdapter(converterFactory, typeInfo)); + } + + public void removeConvertible(Class<?> sourceType, Class<?> targetType) { + this.converters.remove(sourceType, targetType); + invalidateCache(); + } + + // implementing ConversionService + + public boolean canConvert(Class<?> sourceType, Class<?> targetType) { + Assert.notNull(targetType, "targetType to convert to cannot be null"); + return canConvert((sourceType != null ? TypeDescriptor.valueOf(sourceType) : null), + TypeDescriptor.valueOf(targetType)); + } + + public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { + Assert.notNull(targetType, "targetType to convert to cannot be null"); + if (sourceType == null) { + return true; + } + GenericConverter converter = getConverter(sourceType, targetType); + return (converter != null); + } + + /** + * Returns true if conversion between the sourceType and targetType can be bypassed. + * More precisely this method will return true if objects of sourceType can be + * converted to the targetType by returning the source object unchanged. + * @param sourceType context about the source type to convert from (may be null if source is null) + * @param targetType context about the target type to convert to (required) + * @return true if conversion can be bypassed + * @throws IllegalArgumentException if targetType is null + * @since 3.2 + */ + public boolean canBypassConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { + Assert.notNull(targetType, "The targetType to convert to cannot be null"); + if (sourceType == null) { + return true; + } + GenericConverter converter = getConverter(sourceType, targetType); + return (converter == NO_OP_CONVERTER); + } + + @SuppressWarnings("unchecked") + public <T> T convert(Object source, Class<T> targetType) { + Assert.notNull(targetType,"The targetType to convert to cannot be null"); + return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType)); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + Assert.notNull(targetType,"The targetType to convert to cannot be null"); + if (sourceType == null) { + Assert.isTrue(source == null, "The source must be [null] if sourceType == [null]"); + return handleResult(sourceType, targetType, convertNullSource(sourceType, targetType)); + } + if (source != null && !sourceType.getObjectType().isInstance(source)) { + throw new IllegalArgumentException("The source to convert from must be an instance of " + + sourceType + "; instead it was a " + source.getClass().getName()); + } + GenericConverter converter = getConverter(sourceType, targetType); + if (converter != null) { + Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType); + return handleResult(sourceType, targetType, result); + } + return handleConverterNotFound(source, sourceType, targetType); + } + + /** + * Convenience operation for converting a source object to the specified targetType, + * where the targetType is a descriptor that provides additional conversion context. + * Simply delegates to {@link #convert(Object, TypeDescriptor, TypeDescriptor)} and + * encapsulates the construction of the sourceType descriptor using + * {@link TypeDescriptor#forObject(Object)}. + * @param source the source object + * @param targetType the target type + * @return the converted value + * @throws ConversionException if a conversion exception occurred + * @throws IllegalArgumentException if targetType is null, + * or sourceType is null but source is not null + */ + public Object convert(Object source, TypeDescriptor targetType) { + return convert(source, TypeDescriptor.forObject(source), targetType); + } + + @Override + public String toString() { + return this.converters.toString(); + } + + + // Protected template methods + + /** + * Template method to convert a null source. + * <p>Default implementation returns {@code null}. + * Subclasses may override to return custom null objects for specific target types. + * @param sourceType the sourceType to convert from + * @param targetType the targetType to convert to + * @return the converted null object + */ + protected Object convertNullSource(TypeDescriptor sourceType, TypeDescriptor targetType) { + return null; + } + + /** + * Hook method to lookup the converter for a given sourceType/targetType pair. + * First queries this ConversionService's converter cache. + * On a cache miss, then performs an exhaustive search for a matching converter. + * If no converter matches, returns the default converter. + * Subclasses may override. + * @param sourceType the source type to convert from + * @param targetType the target type to convert to + * @return the generic converter that will perform the conversion, or {@code null} if + * no suitable converter was found + * @see #getDefaultConverter(TypeDescriptor, TypeDescriptor) + */ + protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType); + GenericConverter converter = this.converterCache.get(key); + if (converter != null) { + return (converter != NO_MATCH ? converter : null); + } + + converter = this.converters.find(sourceType, targetType); + if (converter == null) { + converter = getDefaultConverter(sourceType, targetType); + } + + if (converter != null) { + this.converterCache.put(key, converter); + return converter; + } + + this.converterCache.put(key, NO_MATCH); + return null; + } + + /** + * Return the default converter if no converter is found for the given sourceType/targetType pair. + * Returns a NO_OP Converter if the sourceType is assignable to the targetType. + * Returns {@code null} otherwise, indicating no suitable converter could be found. + * Subclasses may override. + * @param sourceType the source type to convert from + * @param targetType the target type to convert to + * @return the default generic converter that will perform the conversion + */ + protected GenericConverter getDefaultConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + return (sourceType.isAssignableTo(targetType) ? NO_OP_CONVERTER : null); + } + + // internal helpers + + private GenericConverter.ConvertiblePair getRequiredTypeInfo(Object converter, Class<?> genericIfc) { + Class<?>[] args = GenericTypeResolver.resolveTypeArguments(converter.getClass(), genericIfc); + return (args != null ? new GenericConverter.ConvertiblePair(args[0], args[1]) : null); + } + + private void invalidateCache() { + this.converterCache.clear(); + } + + private Object handleConverterNotFound(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + assertNotPrimitiveTargetType(sourceType, targetType); + return source; + } + if (sourceType.isAssignableTo(targetType) && targetType.getObjectType().isInstance(source)) { + return source; + } + throw new ConverterNotFoundException(sourceType, targetType); + } + + private Object handleResult(TypeDescriptor sourceType, TypeDescriptor targetType, Object result) { + if (result == null) { + assertNotPrimitiveTargetType(sourceType, targetType); + } + return result; + } + + private void assertNotPrimitiveTargetType(TypeDescriptor sourceType, TypeDescriptor targetType) { + if (targetType.isPrimitive()) { + throw new ConversionFailedException(sourceType, targetType, null, + new IllegalArgumentException("A null value cannot be assigned to a primitive type")); + } + } + + + /** + * Adapts a {@link Converter} to a {@link GenericConverter}. + */ + @SuppressWarnings("unchecked") + private final class ConverterAdapter implements ConditionalGenericConverter { + + private final Converter<Object, Object> converter; + + private final ConvertiblePair typeInfo; + + public ConverterAdapter(Converter<?, ?> converter, ConvertiblePair typeInfo) { + this.converter = (Converter<Object, Object>) converter; + this.typeInfo = typeInfo; + } + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(this.typeInfo); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + if (!this.typeInfo.getTargetType().equals(targetType.getObjectType())) { + return false; + } + if (this.converter instanceof ConditionalConverter) { + return ((ConditionalConverter) this.converter).matches(sourceType, targetType); + } + return true; + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return convertNullSource(sourceType, targetType); + } + return this.converter.convert(source); + } + + @Override + public String toString() { + return this.typeInfo + " : " + this.converter; + } + } + + + /** + * Adapts a {@link ConverterFactory} to a {@link GenericConverter}. + */ + @SuppressWarnings("unchecked") + private final class ConverterFactoryAdapter implements ConditionalGenericConverter { + + private final ConverterFactory<Object, Object> converterFactory; + + private final ConvertiblePair typeInfo; + + public ConverterFactoryAdapter(ConverterFactory<?, ?> converterFactory, ConvertiblePair typeInfo) { + this.converterFactory = (ConverterFactory<Object, Object>) converterFactory; + this.typeInfo = typeInfo; + } + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(this.typeInfo); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + boolean matches = true; + if (this.converterFactory instanceof ConditionalConverter) { + matches = ((ConditionalConverter) this.converterFactory).matches(sourceType, targetType); + } + if (matches) { + Converter<?, ?> converter = this.converterFactory.getConverter(targetType.getType()); + if (converter instanceof ConditionalConverter) { + matches = ((ConditionalConverter) converter).matches(sourceType, targetType); + } + } + return matches; + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return convertNullSource(sourceType, targetType); + } + return this.converterFactory.getConverter(targetType.getObjectType()).convert(source); + } + + @Override + public String toString() { + return this.typeInfo + " : " + this.converterFactory; + } + } + + + /** + * Key for use with the converter cache. + */ + private static final class ConverterCacheKey { + + private final TypeDescriptor sourceType; + + private final TypeDescriptor targetType; + + public ConverterCacheKey(TypeDescriptor sourceType, TypeDescriptor targetType) { + this.sourceType = sourceType; + this.targetType = targetType; + } + + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ConverterCacheKey)) { + return false; + } + ConverterCacheKey otherKey = (ConverterCacheKey) other; + return ObjectUtils.nullSafeEquals(this.sourceType, otherKey.sourceType) && + ObjectUtils.nullSafeEquals(this.targetType, otherKey.targetType); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.sourceType) * 29 + + ObjectUtils.nullSafeHashCode(this.targetType); + } + + @Override + public String toString() { + return "ConverterCacheKey [sourceType = " + this.sourceType + + ", targetType = " + this.targetType + "]"; + } + } + + + /** + * Manages all converters registered with the service. + */ + private static class Converters { + + private final Set<GenericConverter> globalConverters = new LinkedHashSet<GenericConverter>(); + + private final Map<ConvertiblePair, ConvertersForPair> converters = + new LinkedHashMap<ConvertiblePair, ConvertersForPair>(36); + + public void add(GenericConverter converter) { + Set<ConvertiblePair> convertibleTypes = converter.getConvertibleTypes(); + if (convertibleTypes == null) { + Assert.state(converter instanceof ConditionalConverter, + "Only conditional converters may return null convertible types"); + this.globalConverters.add(converter); + } + else { + for (ConvertiblePair convertiblePair : convertibleTypes) { + ConvertersForPair convertersForPair = getMatchableConverters(convertiblePair); + convertersForPair.add(converter); + } + } + } + + private ConvertersForPair getMatchableConverters(ConvertiblePair convertiblePair) { + ConvertersForPair convertersForPair = this.converters.get(convertiblePair); + if (convertersForPair == null) { + convertersForPair = new ConvertersForPair(); + this.converters.put(convertiblePair, convertersForPair); + } + return convertersForPair; + } + + public void remove(Class<?> sourceType, Class<?> targetType) { + this.converters.remove(new ConvertiblePair(sourceType, targetType)); + } + + /** + * Find a {@link GenericConverter} given a source and target type. + * <p>This method will attempt to match all possible converters by working + * through the class and interface hierarchy of the types. + * @param sourceType the source type + * @param targetType the target type + * @return a matching {@link GenericConverter}, or {@code null} if none found + */ + public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) { + // Search the full type hierarchy + List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType()); + List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType()); + for (Class<?> sourceCandidate : sourceCandidates) { + for (Class<?> targetCandidate : targetCandidates) { + ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate); + GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair); + if (converter != null) { + return converter; + } + } + } + return null; + } + + private GenericConverter getRegisteredConverter(TypeDescriptor sourceType, + TypeDescriptor targetType, ConvertiblePair convertiblePair) { + + // Check specifically registered converters + ConvertersForPair convertersForPair = this.converters.get(convertiblePair); + if (convertersForPair != null) { + GenericConverter converter = convertersForPair.getConverter(sourceType, targetType); + if (converter != null) { + return converter; + } + } + // Check ConditionalGenericConverter that match all types + for (GenericConverter globalConverter : this.globalConverters) { + if (((ConditionalConverter)globalConverter).matches(sourceType, targetType)) { + return globalConverter; + } + } + return null; + } + + /** + * Returns an ordered class hierarchy for the given type. + * @param type the type + * @return an ordered list of all classes that the given type extends or implements + */ + private List<Class<?>> getClassHierarchy(Class<?> type) { + List<Class<?>> hierarchy = new ArrayList<Class<?>>(20); + Set<Class<?>> visited = new HashSet<Class<?>>(20); + addToClassHierarchy(0, ClassUtils.resolvePrimitiveIfNecessary(type), false, hierarchy, visited); + boolean array = type.isArray(); + int i = 0; + while (i < hierarchy.size()) { + Class<?> candidate = hierarchy.get(i); + candidate = (array ? candidate.getComponentType() : ClassUtils.resolvePrimitiveIfNecessary(candidate)); + Class<?> superclass = candidate.getSuperclass(); + if (candidate.getSuperclass() != null && superclass != Object.class) { + addToClassHierarchy(i + 1, candidate.getSuperclass(), array, hierarchy, visited); + } + for (Class<?> implementedInterface : candidate.getInterfaces()) { + addToClassHierarchy(hierarchy.size(), implementedInterface, array, hierarchy, visited); + } + i++; + } + addToClassHierarchy(hierarchy.size(), Object.class, array, hierarchy, visited); + addToClassHierarchy(hierarchy.size(), Object.class, false, hierarchy, visited); + return hierarchy; + } + + private void addToClassHierarchy(int index, Class<?> type, boolean asArray, + List<Class<?>> hierarchy, Set<Class<?>> visited) { + if (asArray) { + type = Array.newInstance(type, 0).getClass(); + } + if (visited.add(type)) { + hierarchy.add(index, type); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ConversionService converters =\n"); + for (String converterString : getConverterStrings()) { + builder.append('\t').append(converterString).append('\n'); + } + return builder.toString(); + } + + private List<String> getConverterStrings() { + List<String> converterStrings = new ArrayList<String>(); + for (ConvertersForPair convertersForPair : converters.values()) { + converterStrings.add(convertersForPair.toString()); + } + Collections.sort(converterStrings); + return converterStrings; + } + } + + + /** + * Manages converters registered with a specific {@link ConvertiblePair}. + */ + private static class ConvertersForPair { + + private final LinkedList<GenericConverter> converters = new LinkedList<GenericConverter>(); + + public void add(GenericConverter converter) { + this.converters.addFirst(converter); + } + + public GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + for (GenericConverter converter : this.converters) { + if (!(converter instanceof ConditionalGenericConverter) || + ((ConditionalGenericConverter) converter).matches(sourceType, targetType)) { + return converter; + } + } + return null; + } + + @Override + public String toString() { + return StringUtils.collectionToCommaDelimitedString(this.converters); + } + } + + + /** + * Internal converter that performs no operation. + */ + private static class NoOpConverter implements GenericConverter { + + private final String name; + + public NoOpConverter(String name) { + this.name = name; + } + + public Set<ConvertiblePair> getConvertibleTypes() { + return null; + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return source; + } + + @Override + public String toString() { + return this.name; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/IdToEntityConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/IdToEntityConverter.java new file mode 100644 index 00000000..da30e0d3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/IdToEntityConverter.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2014 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 java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Converts an entity identifier to a entity reference by calling a static finder method + * on the target entity type. + * + * <p>For this converter to match, the finder method must be static, have the signature + * {@code find[EntityName]([IdType])}, and return an instance of the desired entity type. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class IdToEntityConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public IdToEntityConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object.class, Object.class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + Method finder = getFinder(targetType.getType()); + return (finder != null && + this.conversionService.canConvert(sourceType, TypeDescriptor.valueOf(finder.getParameterTypes()[0]))); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Method finder = getFinder(targetType.getType()); + Object id = this.conversionService.convert( + source, sourceType, TypeDescriptor.valueOf(finder.getParameterTypes()[0])); + return ReflectionUtils.invokeMethod(finder, source, id); + } + + + private Method getFinder(Class<?> entityClass) { + String finderMethod = "find" + getEntityName(entityClass); + Method[] methods; + boolean localOnlyFiltered; + try { + methods = entityClass.getDeclaredMethods(); + localOnlyFiltered = true; + } + catch (SecurityException ex) { + // Not allowed to access non-public methods... + // Fallback: check locally declared public methods only. + methods = entityClass.getMethods(); + localOnlyFiltered = false; + } + for (Method method : methods) { + if (Modifier.isStatic(method.getModifiers()) && method.getName().equals(finderMethod) && + method.getParameterTypes().length == 1 && method.getReturnType().equals(entityClass) && + (localOnlyFiltered || method.getDeclaringClass().equals(entityClass))) { + return method; + } + } + return null; + } + + private String getEntityName(Class<?> entityClass) { + String shortName = ClassUtils.getShortName(entityClass); + int lastDot = shortName.lastIndexOf('.'); + if (lastDot != -1) { + return shortName.substring(lastDot + 1); + } + else { + return shortName; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/MapToMapConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/MapToMapConverter.java new file mode 100644 index 00000000..c9f7f9a5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/MapToMapConverter.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2014 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Converts a Map to another Map. + * + * <p>First, creates a new Map of the requested targetType with a size equal to the + * size of the source Map. Then copies each element in the source map to the target map. + * Will perform a conversion from the source maps's parameterized K,V types to the target + * map's parameterized types K,V if necessary. + * + * @author Keith Donald + * @since 3.0 + */ +final class MapToMapConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public MapToMapConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Map.class, Map.class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return canConvertKey(sourceType, targetType) && canConvertValue(sourceType, targetType); + } + + @SuppressWarnings("unchecked") + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + boolean copyRequired = !targetType.getType().isInstance(source); + Map<Object, Object> sourceMap = (Map<Object, Object>) source; + if (!copyRequired && sourceMap.isEmpty()) { + return sourceMap; + } + List<MapEntry> targetEntries = new ArrayList<MapEntry>(sourceMap.size()); + for (Map.Entry<Object, Object> entry : sourceMap.entrySet()) { + Object sourceKey = entry.getKey(); + Object sourceValue = entry.getValue(); + Object targetKey = convertKey(sourceKey, sourceType, targetType.getMapKeyTypeDescriptor()); + Object targetValue = convertValue(sourceValue, sourceType, targetType.getMapValueTypeDescriptor()); + targetEntries.add(new MapEntry(targetKey, targetValue)); + if (sourceKey != targetKey || sourceValue != targetValue) { + copyRequired = true; + } + } + if (!copyRequired) { + return sourceMap; + } + Map<Object, Object> targetMap = CollectionFactory.createMap(targetType.getType(), sourceMap.size()); + for (MapEntry entry : targetEntries) { + entry.addToMap(targetMap); + } + return targetMap; + } + + + // internal helpers + + private boolean canConvertKey(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getMapKeyTypeDescriptor(), + targetType.getMapKeyTypeDescriptor(), this.conversionService); + } + + private boolean canConvertValue(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getMapValueTypeDescriptor(), + targetType.getMapValueTypeDescriptor(), this.conversionService); + } + + private Object convertKey(Object sourceKey, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (targetType == null) { + return sourceKey; + } + return this.conversionService.convert(sourceKey, sourceType.getMapKeyTypeDescriptor(sourceKey), targetType); + } + + private Object convertValue(Object sourceValue, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (targetType == null) { + return sourceValue; + } + return this.conversionService.convert(sourceValue, sourceType.getMapValueTypeDescriptor(sourceValue), targetType); + } + + + private static class MapEntry { + + private final Object key; + + private final Object value; + + public MapEntry(Object key, Object value) { + this.key = key; + this.value = value; + } + + public void addToMap(Map<Object, Object> map) { + map.put(this.key, this.value); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/NumberToCharacterConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/NumberToCharacterConverter.java new file mode 100644 index 00000000..b272abc6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/NumberToCharacterConverter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2009 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; + +/** + * Converts from any JDK-standard Number implementation to a Character. + * + * @author Keith Donald + * @since 3.0 + * @see java.lang.Character + * @see java.lang.Short + * @see java.lang.Integer + * @see java.lang.Long + * @see java.math.BigInteger + * @see java.lang.Float + * @see java.lang.Double + * @see java.math.BigDecimal + */ +final class NumberToCharacterConverter implements Converter<Number, Character> { + + public Character convert(Number source) { + return (char) source.shortValue(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/NumberToNumberConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/NumberToNumberConverterFactory.java new file mode 100644 index 00000000..240880d6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/NumberToNumberConverterFactory.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2012 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.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.util.NumberUtils; + +/** + * Converts from any JDK-standard Number implementation to any other JDK-standard Number implementation. + * + * <p>Support Number classes including Byte, Short, Integer, Float, Double, Long, BigInteger, BigDecimal. This class + * delegates to {@link NumberUtils#convertNumberToTargetClass(Number, Class)} to perform the conversion. + * + * @author Keith Donald + * @since 3.0 + * @see java.lang.Byte + * @see java.lang.Short + * @see java.lang.Integer + * @see java.lang.Long + * @see java.math.BigInteger + * @see java.lang.Float + * @see java.lang.Double + * @see java.math.BigDecimal + * @see NumberUtils + */ +final class NumberToNumberConverterFactory implements ConverterFactory<Number, Number>, + ConditionalConverter { + + public <T extends Number> Converter<Number, T> getConverter(Class<T> targetType) { + return new NumberToNumber<T>(targetType); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return !sourceType.equals(targetType); + } + + private final static class NumberToNumber<T extends Number> implements Converter<Number, T> { + + private final Class<T> targetType; + + public NumberToNumber(Class<T> targetType) { + this.targetType = targetType; + } + + public T convert(Number source) { + return NumberUtils.convertNumberToTargetClass(source, this.targetType); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToArrayConverter.java new file mode 100644 index 00000000..2e8d850d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToArrayConverter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2011 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 java.lang.reflect.Array; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Converts an Object to a single-element Array containing the Object. + * Will convert the Object to the target Array's component type if necessary. + * + * @author Keith Donald + * @since 3.0 + */ +final class ObjectToArrayConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + public ObjectToArrayConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object.class, Object[].class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType, targetType.getElementTypeDescriptor(), this.conversionService); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Object target = Array.newInstance(targetType.getElementTypeDescriptor().getType(), 1); + Object targetElement = this.conversionService.convert(source, sourceType, targetType.getElementTypeDescriptor()); + Array.set(target, 0, targetElement); + return target; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToCollectionConverter.java new file mode 100644 index 00000000..7741128b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToCollectionConverter.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2011 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 java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Converts an Object to a single-element Collection containing the Object. + * Will convert the Object to the target Collection's parameterized type if necessary. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class ObjectToCollectionConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + public ObjectToCollectionConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object.class, Collection.class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType, targetType.getElementTypeDescriptor(), this.conversionService); + } + + @SuppressWarnings("unchecked") + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Collection<Object> target = CollectionFactory.createCollection(targetType.getType(), 1); + if (targetType.getElementTypeDescriptor() == null || targetType.getElementTypeDescriptor().isCollection()) { + target.add(source); + } + else { + Object singleElement = this.conversionService.convert(source, sourceType, targetType.getElementTypeDescriptor()); + target.add(singleElement); + } + return target; + } + +} 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 new file mode 100644 index 00000000..0448e582 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2012 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 java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Generic Converter that attempts to convert a source Object to a target type + * by delegating to methods on the target type. + * + * <p>Calls the static {@code valueOf(sourceType)} method on the target type + * to perform the conversion, if such a method exists. Else calls the target type's + * Constructor that accepts a single sourceType argument, if such a Constructor exists. + * Else throws a ConversionFailedException. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class ObjectToObjectConverter implements ConditionalGenericConverter { + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object.class, Object.class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + if (sourceType.getType().equals(targetType.getType())) { + // no conversion required + return false; + } + return hasValueOfMethodOrConstructor(targetType.getType(), sourceType.getType()); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Class<?> sourceClass = sourceType.getType(); + Class<?> targetClass = targetType.getType(); + Method method = getValueOfMethodOn(targetClass, sourceClass); + try { + if (method != null) { + ReflectionUtils.makeAccessible(method); + return method.invoke(null, source); + } + else { + Constructor<?> constructor = getConstructor(targetClass, sourceClass); + if (constructor != null) { + return constructor.newInstance(source); + } + } + } + catch (InvocationTargetException ex) { + throw new ConversionFailedException(sourceType, targetType, source, ex.getTargetException()); + } + catch (Throwable ex) { + throw new ConversionFailedException(sourceType, targetType, source, ex); + } + throw new IllegalStateException("No static valueOf(" + sourceClass.getName() + + ") method or Constructor(" + sourceClass.getName() + ") exists on " + targetClass.getName()); + } + + static boolean hasValueOfMethodOrConstructor(Class<?> clazz, Class<?> sourceParameterType) { + return getValueOfMethodOn(clazz, sourceParameterType) != null || getConstructor(clazz, sourceParameterType) != null; + } + + private static Method getValueOfMethodOn(Class<?> clazz, Class<?> sourceParameterType) { + return ClassUtils.getStaticMethod(clazz, "valueOf", sourceParameterType); + } + + private static Constructor<?> getConstructor(Class<?> clazz, Class<?> sourceParameterType) { + return ClassUtils.getConstructorIfAvailable(clazz, sourceParameterType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToStringConverter.java new file mode 100644 index 00000000..420183b3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToStringConverter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2014 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; + +/** + * Simply calls {@link Object#toString()} to convert a source Object to a String. + * + * @author Keith Donald + * @since 3.0 + */ +final class ObjectToStringConverter implements Converter<Object, String> { + + public String convert(Object source) { + return source.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/PropertiesToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/PropertiesToStringConverter.java new file mode 100644 index 00000000..f4962d01 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/PropertiesToStringConverter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2014 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 java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Properties; + +import org.springframework.core.convert.converter.Converter; + +/** + * Converts from a Properties to a String by calling {@link Properties#store(java.io.OutputStream, String)}. + * Decodes with the ISO-8859-1 charset before returning the String. + * + * @author Keith Donald + * @since 3.0 + */ +final class PropertiesToStringConverter implements Converter<Properties, String> { + + public String convert(Properties source) { + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(256); + source.store(os, null); + return os.toString("ISO-8859-1"); + } + catch (IOException ex) { + // Should never happen. + throw new IllegalArgumentException("Failed to store [" + source + "] into String", ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java new file mode 100644 index 00000000..4bf32af3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2010 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 java.lang.reflect.Array; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.util.StringUtils; + +/** + * Converts a comma-delimited String to an Array. + * Only matches if String.class can be converted to the target array element type. + * + * @author Keith Donald + * @since 3.0 + */ +final class StringToArrayConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + public StringToArrayConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Object[].class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.conversionService.canConvert(sourceType, targetType.getElementTypeDescriptor()); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + String string = (String) source; + String[] fields = StringUtils.commaDelimitedListToStringArray(string); + Object target = Array.newInstance(targetType.getElementTypeDescriptor().getType(), fields.length); + for (int i = 0; i < fields.length; i++) { + String sourceElement = fields[i]; + Object targetElement = this.conversionService.convert(sourceElement.trim(), sourceType, targetType.getElementTypeDescriptor()); + Array.set(target, i, targetElement); + } + return target; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToBooleanConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToBooleanConverter.java new file mode 100644 index 00000000..1e23ee61 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToBooleanConverter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2011 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 java.util.HashSet; +import java.util.Set; + +import org.springframework.core.convert.converter.Converter; + +/** + * Converts String to a Boolean. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class StringToBooleanConverter implements Converter<String, Boolean> { + + private static final Set<String> trueValues = new HashSet<String>(4); + + private static final Set<String> falseValues = new HashSet<String>(4); + + static { + trueValues.add("true"); + trueValues.add("on"); + trueValues.add("yes"); + trueValues.add("1"); + + falseValues.add("false"); + falseValues.add("off"); + falseValues.add("no"); + falseValues.add("0"); + } + + public Boolean convert(String source) { + String value = source.trim(); + if ("".equals(value)) { + return null; + } + value = value.toLowerCase(); + if (trueValues.contains(value)) { + return Boolean.TRUE; + } + else if (falseValues.contains(value)) { + return Boolean.FALSE; + } + else { + throw new IllegalArgumentException("Invalid boolean value '" + source + "'"); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharacterConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharacterConverter.java new file mode 100644 index 00000000..3e379164 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharacterConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2009 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; + +/** + * Converts a String to a Character. + * + * @author Keith Donald + * @since 3.0 + */ +final class StringToCharacterConverter implements Converter<String, Character> { + + public Character convert(String source) { + if (source.length() == 0) { + return null; + } + if (source.length() > 1) { + throw new IllegalArgumentException( + "Can only convert a [String] with length of 1 to a [Character]; string value '" + source + "' has length of " + source.length()); + } + return source.charAt(0); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCollectionConverter.java new file mode 100644 index 00000000..74f912c4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCollectionConverter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2014 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 java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.util.StringUtils; + +/** + * Converts a comma-delimited String to a Collection. + * If the target collection element type is declared, only matches if + * {@code String.class} can be converted to it. + * + * @author Keith Donald + * @since 3.0 + */ +final class StringToCollectionConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public StringToCollectionConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + public Set<ConvertiblePair> getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Collection.class)); + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return (targetType.getElementTypeDescriptor() == null || + this.conversionService.canConvert(sourceType, targetType.getElementTypeDescriptor())); + } + + @SuppressWarnings("unchecked") + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + String string = (String) source; + String[] fields = StringUtils.commaDelimitedListToStringArray(string); + Collection<Object> target = CollectionFactory.createCollection(targetType.getType(), fields.length); + if (targetType.getElementTypeDescriptor() == null) { + for (String field : fields) { + target.add(field.trim()); + } + } + else { + for (String field : fields) { + Object targetElement = this.conversionService.convert(field.trim(), sourceType, targetType.getElementTypeDescriptor()); + target.add(targetElement); + } + } + return target; + } + +} 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 new file mode 100644 index 00000000..d22ed96b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2014 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 String to a java.lang.Enum by calling {@link Enum#valueOf(Class, String)}. + * + * @author Keith Donald + * @since 3.0 + */ +@SuppressWarnings({ "unchecked", "rawtypes" }) +final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> { + + public <T extends Enum> Converter<String, T> getConverter(Class<T> 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); + } + + + private class StringToEnum<T extends Enum> implements Converter<String, T> { + + private final Class<T> enumType; + + public StringToEnum(Class<T> enumType) { + this.enumType = enumType; + } + + public T convert(String source) { + if (source.length() == 0) { + // It's an empty enum identifier: reset the enum value to null. + return null; + } + return (T) Enum.valueOf(this.enumType, source.trim()); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java new file mode 100644 index 00000000..3ca91385 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2009 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 java.util.Locale; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.StringUtils; + +/** + * Converts a String to a Locale. + * + * @author Keith Donald + * @since 3.0 + */ +final class StringToLocaleConverter implements Converter<String, Locale> { + + public Locale convert(String source) { + return StringUtils.parseLocaleString(source); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java new file mode 100644 index 00000000..8bb0d646 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2009 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; +import org.springframework.util.NumberUtils; + +/** + * Converts from a String any JDK-standard Number implementation. + * + * <p>Support Number classes including Byte, Short, Integer, Float, Double, Long, BigInteger, BigDecimal. This class + * delegates to {@link NumberUtils#parseNumber(String, Class)} to perform the conversion. + * + * @author Keith Donald + * @since 3.0 + * @see java.lang.Byte + * @see java.lang.Short + * @see java.lang.Integer + * @see java.lang.Long + * @see java.math.BigInteger + * @see java.lang.Float + * @see java.lang.Double + * @see java.math.BigDecimal + * @see NumberUtils + */ +final class StringToNumberConverterFactory implements ConverterFactory<String, Number> { + + public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) { + return new StringToNumber<T>(targetType); + } + + private static final class StringToNumber<T extends Number> implements Converter<String, T> { + + private final Class<T> targetType; + + public StringToNumber(Class<T> targetType) { + this.targetType = targetType; + } + + public T convert(String source) { + if (source.length() == 0) { + return null; + } + return NumberUtils.parseNumber(source, this.targetType); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToPropertiesConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToPropertiesConverter.java new file mode 100644 index 00000000..e97cae67 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToPropertiesConverter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2009 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 java.io.ByteArrayInputStream; +import java.util.Properties; + +import org.springframework.core.convert.converter.Converter; + +/** + * Converts a String to a Properties by calling Properties#load(java.io.InputStream). + * Uses ISO-8559-1 encoding required by Properties. + * + * @author Keith Donald + * @since 3.0 + */ +final class StringToPropertiesConverter implements Converter<String, Properties> { + + public Properties convert(String source) { + try { + Properties props = new Properties(); + // Must use the ISO-8859-1 encoding because Properties.load(stream) expects it. + props.load(new ByteArrayInputStream(source.getBytes("ISO-8859-1"))); + return props; + } + catch (Exception ex) { + // Should never happen. + throw new IllegalArgumentException("Failed to parse [" + source + "] into Properties", ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToUUIDConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToUUIDConverter.java new file mode 100644 index 00000000..db5a3ebf --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToUUIDConverter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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 java.util.UUID; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.StringUtils; + +/** + * Converts from a String to a java.util.UUID by calling {@link UUID#fromString(String)}. + * + * @author Phillip Webb + * @since 3.2 + */ +final class StringToUUIDConverter implements Converter<String, UUID> { + + public UUID convert(String source) { + if (StringUtils.hasLength(source)) { + return UUID.fromString(source.trim()); + } + return null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/package-info.java b/spring-core/src/main/java/org/springframework/core/convert/support/package-info.java new file mode 100644 index 00000000..3c0c0ded --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * Default implementation of the type conversion system. + * + */ +package org.springframework.core.convert.support; + diff --git a/spring-core/src/main/java/org/springframework/core/enums/AbstractCachingLabeledEnumResolver.java b/spring-core/src/main/java/org/springframework/core/enums/AbstractCachingLabeledEnumResolver.java new file mode 100644 index 00000000..4a278f92 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/enums/AbstractCachingLabeledEnumResolver.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2012 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.enums; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; +import org.springframework.util.CachingMapDecorator; +import org.springframework.util.ClassUtils; + +/** + * Abstract base class for {@link LabeledEnumResolver} implementations, + * caching all retrieved {@link LabeledEnum} instances. + * + * <p>Subclasses need to implement the template method + * {@link #findLabeledEnums(Class)}. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + * @see #findLabeledEnums(Class) + * @deprecated as of Spring 3.0, in favor of Java 5 enums. + */ +@Deprecated +public abstract class AbstractCachingLabeledEnumResolver implements LabeledEnumResolver { + + protected transient final Log logger = LogFactory.getLog(getClass()); + + private final LabeledEnumCache labeledEnumCache = new LabeledEnumCache(); + + + public Set<LabeledEnum> getLabeledEnumSet(Class type) throws IllegalArgumentException { + return new TreeSet<LabeledEnum>(getLabeledEnumMap(type).values()); + } + + public Map<Comparable, LabeledEnum> getLabeledEnumMap(Class type) throws IllegalArgumentException { + Assert.notNull(type, "No type specified"); + return this.labeledEnumCache.get(type); + } + + public LabeledEnum getLabeledEnumByCode(Class type, Comparable code) throws IllegalArgumentException { + Assert.notNull(code, "No enum code specified"); + Map<Comparable, LabeledEnum> typeEnums = getLabeledEnumMap(type); + LabeledEnum codedEnum = typeEnums.get(code); + if (codedEnum == null) { + throw new IllegalArgumentException( + "No enumeration with code '" + code + "'" + " of type [" + type.getName() + + "] exists: this is likely a configuration error. " + + "Make sure the code value matches a valid instance's code property!"); + } + return codedEnum; + } + + public LabeledEnum getLabeledEnumByLabel(Class type, String label) throws IllegalArgumentException { + Map<Comparable, LabeledEnum> typeEnums = getLabeledEnumMap(type); + for (LabeledEnum value : typeEnums.values()) { + if (value.getLabel().equalsIgnoreCase(label)) { + return value; + } + } + throw new IllegalArgumentException( + "No enumeration with label '" + label + "' of type [" + type + + "] exists: this is likely a configuration error. " + + "Make sure the label string matches a valid instance's label property!"); + } + + + /** + * Template method to be implemented by subclasses. + * Supposed to find all LabeledEnum instances for the given type. + * @param type the enum type + * @return the Set of LabeledEnum instances + * @see org.springframework.core.enums.LabeledEnum + */ + protected abstract Set<LabeledEnum> findLabeledEnums(Class type); + + + /** + * Inner cache class that implements lazy building of LabeledEnum Maps. + */ + @SuppressWarnings("serial") + private class LabeledEnumCache extends CachingMapDecorator<Class, Map<Comparable, LabeledEnum>> { + + public LabeledEnumCache() { + super(true); + } + + @Override + protected Map<Comparable, LabeledEnum> create(Class key) { + Set<LabeledEnum> typeEnums = findLabeledEnums(key); + if (typeEnums == null || typeEnums.isEmpty()) { + throw new IllegalArgumentException( + "Unsupported labeled enumeration type '" + key + "': " + + "make sure you've properly defined this enumeration! " + + "If it is static, are the class and its fields public/static/final?"); + } + Map<Comparable, LabeledEnum> typeEnumMap = new HashMap<Comparable, LabeledEnum>(typeEnums.size()); + for (LabeledEnum labeledEnum : typeEnums) { + typeEnumMap.put(labeledEnum.getCode(), labeledEnum); + } + return Collections.unmodifiableMap(typeEnumMap); + } + + @Override + protected boolean useWeakValue(Class key, Map<Comparable, LabeledEnum> value) { + if (!ClassUtils.isCacheSafe(key, AbstractCachingLabeledEnumResolver.this.getClass().getClassLoader())) { + if (logger != null && logger.isDebugEnabled()) { + logger.debug("Not strongly caching class [" + key.getName() + "] because it is not cache-safe"); + } + return true; + } + else { + return false; + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/enums/AbstractGenericLabeledEnum.java b/spring-core/src/main/java/org/springframework/core/enums/AbstractGenericLabeledEnum.java new file mode 100644 index 00000000..cee9535c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/enums/AbstractGenericLabeledEnum.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2012 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.enums; + +/** + * Base class for labeled enum instances that aren't static. + * + * @author Keith Donald + * @since 1.2.6 + * @deprecated as of Spring 3.0, in favor of Java 5 enums. + */ +@Deprecated +@SuppressWarnings("serial") +public abstract class AbstractGenericLabeledEnum extends AbstractLabeledEnum { + + /** + * A descriptive label for the enum. + */ + private final String label; + + + /** + * Create a new StaticLabeledEnum instance. + * @param label the label; if {@code null}), the enum's code + * will be used as label + */ + protected AbstractGenericLabeledEnum(String label) { + this.label = label; + } + + + public String getLabel() { + if (this.label != null) { + return label; + } + else { + return getCode().toString(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/enums/AbstractLabeledEnum.java b/spring-core/src/main/java/org/springframework/core/enums/AbstractLabeledEnum.java new file mode 100644 index 00000000..3c2860f7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/enums/AbstractLabeledEnum.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2012 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.enums; + +/** + * Abstract base superclass for LabeledEnum implementations. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Sam Brannen + * @since 1.2.2 + * @deprecated as of Spring 3.0, in favor of Java 5 enums. + */ +@Deprecated +@SuppressWarnings("serial") +public abstract class AbstractLabeledEnum implements LabeledEnum { + + /** + * Create a new AbstractLabeledEnum instance. + */ + protected AbstractLabeledEnum() { + } + + public Class getType() { + // Could be coded as getClass().isAnonymousClass() on JDK 1.5 + boolean isAnonymous = (getClass().getDeclaringClass() == null && getClass().getName().indexOf('$') != -1); + return (isAnonymous ? getClass().getSuperclass() : getClass()); + } + + public int compareTo(Object obj) { + if (!(obj instanceof LabeledEnum)) { + throw new ClassCastException("You may only compare LabeledEnums"); + } + LabeledEnum that = (LabeledEnum) obj; + if (!this.getType().equals(that.getType())) { + throw new ClassCastException("You may only compare LabeledEnums of the same type"); + } + return this.getCode().compareTo(that.getCode()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof LabeledEnum)) { + return false; + } + LabeledEnum other = (LabeledEnum) obj; + return (this.getType().equals(other.getType()) && this.getCode().equals(other.getCode())); + } + + @Override + public int hashCode() { + return (getType().hashCode() * 29 + getCode().hashCode()); + } + + @Override + public String toString() { + return getLabel(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/enums/LabeledEnum.java b/spring-core/src/main/java/org/springframework/core/enums/LabeledEnum.java new file mode 100644 index 00000000..dc2325bf --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/enums/LabeledEnum.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2012 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.enums; + +import java.io.Serializable; +import java.util.Comparator; + +import org.springframework.util.comparator.CompoundComparator; +import org.springframework.util.comparator.NullSafeComparator; + +/** + * An interface for objects that represent a labeled enumeration. + * Each such enum instance has the following characteristics: + * + * <ul> + * <li>A type that identifies the enum's class. + * For example: {@code com.mycompany.util.FileFormat}.</li> + * + * <li>A code that uniquely identifies the enum within the context of its type. + * For example: "CSV". Different classes of codes are possible + * (e.g., Character, Integer, String).</li> + * + * <li>A descriptive label. For example: "the CSV File Format".</li> + * </ul> + * + * @author Keith Donald + * @since 1.2.2 + * @deprecated as of Spring 3.0, in favor of Java 5 enums. + */ +@Deprecated +public interface LabeledEnum extends Comparable, Serializable { + + /** + * Return this enumeration's type. + */ + Class getType(); + + /** + * Return this enumeration's code. + * <p>Each code should be unique within enumerations of the same type. + */ + Comparable getCode(); + + /** + * Return a descriptive, optional label. + */ + String getLabel(); + + + // Constants for standard enum ordering (Comparator implementations) + + /** + * Shared Comparator instance that sorts enumerations by {@code CODE_ORDER}. + */ + Comparator CODE_ORDER = new Comparator() { + public int compare(Object o1, Object o2) { + Comparable c1 = ((LabeledEnum) o1).getCode(); + Comparable c2 = ((LabeledEnum) o2).getCode(); + return c1.compareTo(c2); + } + }; + + /** + * Shared Comparator instance that sorts enumerations by {@code LABEL_ORDER}. + */ + Comparator LABEL_ORDER = new Comparator() { + public int compare(Object o1, Object o2) { + LabeledEnum e1 = (LabeledEnum) o1; + LabeledEnum e2 = (LabeledEnum) o2; + Comparator comp = new NullSafeComparator(String.CASE_INSENSITIVE_ORDER, true); + return comp.compare(e1.getLabel(), e2.getLabel()); + } + }; + + /** + * Shared Comparator instance that sorts enumerations by {@code LABEL_ORDER}, + * then {@code CODE_ORDER}. + */ + Comparator DEFAULT_ORDER = + new CompoundComparator(new Comparator[] { LABEL_ORDER, CODE_ORDER }); + +} diff --git a/spring-core/src/main/java/org/springframework/core/enums/LabeledEnumResolver.java b/spring-core/src/main/java/org/springframework/core/enums/LabeledEnumResolver.java new file mode 100644 index 00000000..7b374a70 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/enums/LabeledEnumResolver.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2012 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.enums; + +import java.util.Map; +import java.util.Set; + +/** + * Interface for looking up {@code LabeledEnum} instances. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + * @deprecated as of Spring 3.0, in favor of Java 5 enums. + */ +@Deprecated +public interface LabeledEnumResolver { + + /** + * Return a set of enumerations of a particular type. Each element in the + * set should be an instance of LabeledEnum. + * @param type the enum type + * @return a set of localized enumeration instances for the provided type + * @throws IllegalArgumentException if the type is not supported + */ + public Set getLabeledEnumSet(Class type) throws IllegalArgumentException; + + /** + * Return a map of enumerations of a particular type. Each element in the + * map should be a key/value pair, where the key is the enum code, and the + * value is the {@code LabeledEnum} instance. + * @param type the enum type + * @return a Map of localized enumeration instances, + * with enum code as key and {@code LabeledEnum} instance as value + * @throws IllegalArgumentException if the type is not supported + */ + public Map getLabeledEnumMap(Class type) throws IllegalArgumentException; + + /** + * Resolve a single {@code LabeledEnum} by its identifying code. + * @param type the enum type + * @param code the enum code + * @return the enum + * @throws IllegalArgumentException if the code did not map to a valid instance + */ + public LabeledEnum getLabeledEnumByCode(Class type, Comparable code) throws IllegalArgumentException; + + /** + * Resolve a single {@code LabeledEnum} by its identifying code. + * @param type the enum type + * @param label the enum label + * @return the enum + * @throws IllegalArgumentException if the label did not map to a valid instance + */ + public LabeledEnum getLabeledEnumByLabel(Class type, String label) throws IllegalArgumentException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/enums/LetterCodedLabeledEnum.java b/spring-core/src/main/java/org/springframework/core/enums/LetterCodedLabeledEnum.java new file mode 100644 index 00000000..cb70971a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/enums/LetterCodedLabeledEnum.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2012 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.enums; + +import org.springframework.util.Assert; + +/** + * Implementation of LabeledEnum which uses a letter as the code type. + * + * <p>Should almost always be subclassed, but for some simple situations it may be + * used directly. Note that you will not be able to use unique type-based functionality + * like {@code LabeledEnumResolver.getLabeledEnumSet(type)} in this case. + * + * @author Keith Donald + * @since 1.2.2 + * @deprecated as of Spring 3.0, in favor of Java 5 enums. + */ +@Deprecated +@SuppressWarnings("serial") +public class LetterCodedLabeledEnum extends AbstractGenericLabeledEnum { + + /** + * The unique code of this enum. + */ + private final Character code; + + + /** + * Create a new LetterCodedLabeledEnum instance. + * @param code the letter code + * @param label the label (can be {@code null}) + */ + public LetterCodedLabeledEnum(char code, String label) { + super(label); + Assert.isTrue(Character.isLetter(code), + "The code '" + code + "' is invalid: it must be a letter"); + this.code = new Character(code); + } + + + public Comparable getCode() { + return code; + } + + /** + * Return the letter code of this LabeledEnum instance. + */ + public char getLetterCode() { + return ((Character) getCode()).charValue(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/enums/ShortCodedLabeledEnum.java b/spring-core/src/main/java/org/springframework/core/enums/ShortCodedLabeledEnum.java new file mode 100644 index 00000000..0c5e3fae --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/enums/ShortCodedLabeledEnum.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2012 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.enums; + +/** + * Implementation of LabeledEnum which uses Short as the code type. + * + * <p>Should almost always be subclassed, but for some simple situations it may be + * used directly. Note that you will not be able to use unique type-based functionality + * like {@code LabeledEnumResolver.getLabeledEnumSet(type)} in this case. + * + * @author Keith Donald + * @since 1.2.2 + * @deprecated as of Spring 3.0, in favor of Java 5 enums. + */ +@Deprecated +@SuppressWarnings("serial") +public class ShortCodedLabeledEnum extends AbstractGenericLabeledEnum { + + /** + * The unique code of this enum. + */ + private final Short code; + + + /** + * Create a new ShortCodedLabeledEnum instance. + * @param code the short code + * @param label the label (can be {@code null}) + */ + public ShortCodedLabeledEnum(int code, String label) { + super(label); + this.code = new Short((short) code); + } + + + public Comparable getCode() { + return code; + } + + /** + * Return the short code of this LabeledEnum instance. + */ + public short getShortCode() { + return ((Short) getCode()).shortValue(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/enums/StaticLabeledEnum.java b/spring-core/src/main/java/org/springframework/core/enums/StaticLabeledEnum.java new file mode 100644 index 00000000..e6440cc3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/enums/StaticLabeledEnum.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2012 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.enums; + +/** + * Base class for static type-safe labeled enum instances. + * + * Usage example: + * + * <pre> + * public class FlowSessionStatus extends StaticLabeledEnum { + * + * // public static final instances! + * public static FlowSessionStatus CREATED = new FlowSessionStatus(0, "Created"); + * public static FlowSessionStatus ACTIVE = new FlowSessionStatus(1, "Active"); + * public static FlowSessionStatus PAUSED = new FlowSessionStatus(2, "Paused"); + * public static FlowSessionStatus SUSPENDED = new FlowSessionStatus(3, "Suspended"); + * public static FlowSessionStatus ENDED = new FlowSessionStatus(4, "Ended"); + * + * // private constructor! + * private FlowSessionStatus(int code, String label) { + * super(code, label); + * } + * + * // custom behavior + * }</pre> + * + * @author Keith Donald + * @since 1.2.6 + * @deprecated as of Spring 3.0, in favor of Java 5 enums. + */ +@Deprecated +@SuppressWarnings("serial") +public abstract class StaticLabeledEnum extends AbstractLabeledEnum { + + /** + * The unique code of the enum. + */ + private final Short code; + + /** + * A descriptive label for the enum. + */ + private final transient String label; + + + /** + * Create a new StaticLabeledEnum instance. + * @param code the short code + * @param label the label (can be {@code null}) + */ + protected StaticLabeledEnum(int code, String label) { + this.code = new Short((short) code); + if (label != null) { + this.label = label; + } + else { + this.label = this.code.toString(); + } + } + + public Comparable getCode() { + return code; + } + + public String getLabel() { + return label; + } + + /** + * Return the code of this LabeledEnum instance as a short. + */ + public short shortValue() { + return ((Number) getCode()).shortValue(); + } + + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + /** + * Return the resolved type safe static enum instance. + */ + protected Object readResolve() { + return StaticLabeledEnumResolver.instance().getLabeledEnumByCode(getType(), getCode()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/enums/StaticLabeledEnumResolver.java b/spring-core/src/main/java/org/springframework/core/enums/StaticLabeledEnumResolver.java new file mode 100644 index 00000000..3d9f78a4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/enums/StaticLabeledEnumResolver.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2012 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.enums; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Set; +import java.util.TreeSet; + +import org.springframework.util.Assert; + +/** + * {@link LabeledEnumResolver} that resolves statically defined enumerations. + * Static implies all enum instances were defined within Java code, + * implementing the type-safe enum pattern. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + * @deprecated as of Spring 3.0, in favor of Java 5 enums. + */ +@Deprecated +public class StaticLabeledEnumResolver extends AbstractCachingLabeledEnumResolver { + + /** + * Shared {@code StaticLabeledEnumResolver} singleton instance. + */ + private static final StaticLabeledEnumResolver INSTANCE = new StaticLabeledEnumResolver(); + + + /** + * Return the shared {@code StaticLabeledEnumResolver} singleton instance. + * Mainly for resolving unique StaticLabeledEnum references on deserialization. + * @see StaticLabeledEnum + */ + public static StaticLabeledEnumResolver instance() { + return INSTANCE; + } + + + @Override + protected Set<LabeledEnum> findLabeledEnums(Class type) { + Set<LabeledEnum> typeEnums = new TreeSet<LabeledEnum>(); + for (Field field : type.getFields()) { + if (Modifier.isStatic(field.getModifiers()) && Modifier.isPublic(field.getModifiers())) { + if (type.isAssignableFrom(field.getType())) { + try { + Object value = field.get(null); + Assert.isTrue(value instanceof LabeledEnum, "Field value must be a LabeledEnum instance"); + typeEnums.add((LabeledEnum) value); + } + catch (IllegalAccessException ex) { + logger.warn("Unable to access field value: " + field, ex); + } + } + } + } + return typeEnums; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/enums/StringCodedLabeledEnum.java b/spring-core/src/main/java/org/springframework/core/enums/StringCodedLabeledEnum.java new file mode 100644 index 00000000..d0a936ef --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/enums/StringCodedLabeledEnum.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2012 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.enums; + +import org.springframework.util.Assert; + +/** + * Implementation of LabeledEnum which uses a String as the code type. + * + * <p>Should almost always be subclassed, but for some simple situations it may be + * used directly. Note that you will not be able to use unique type-based + * functionality like {@code LabeledEnumResolver.getLabeledEnumSet(type)} in this case. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + * @see org.springframework.core.enums.LabeledEnumResolver#getLabeledEnumSet(Class) + * @deprecated as of Spring 3.0, in favor of Java 5 enums. + */ +@Deprecated +@SuppressWarnings("serial") +public class StringCodedLabeledEnum extends AbstractGenericLabeledEnum { + + /** + * The unique code of this enum. + */ + private final String code; + + + /** + * Create a new StringCodedLabeledEnum instance. + * @param code the String code + * @param label the label (can be {@code null}) + */ + public StringCodedLabeledEnum(String code, String label) { + super(label); + Assert.notNull(code, "'code' must not be null"); + this.code = code; + } + + + public Comparable getCode() { + return this.code; + } + + /** + * Return the String code of this LabeledEnum instance. + */ + public String getStringCode() { + return (String) getCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/enums/package-info.java b/spring-core/src/main/java/org/springframework/core/enums/package-info.java new file mode 100644 index 00000000..ac720538 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/enums/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * Interfaces and classes for type-safe enum support on JDK >= 1.3. + * This enum abstraction support codes and labels. + * + */ +package org.springframework.core.enums; + 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 new file mode 100644 index 00000000..786443be --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java @@ -0,0 +1,535 @@ +/* + * Copyright 2002-2014 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.env; + +import java.security.AccessControlException; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.SpringProperties; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import static java.lang.String.*; +import static org.springframework.util.StringUtils.*; + +/** + * Abstract base class for {@link Environment} implementations. Supports the notion of + * reserved default profile names and enables specifying active and default profiles + * through the {@link #ACTIVE_PROFILES_PROPERTY_NAME} and + * {@link #DEFAULT_PROFILES_PROPERTY_NAME} properties. + * + * <p>Concrete subclasses differ primarily on which {@link PropertySource} objects they + * add by default. {@code AbstractEnvironment} adds none. Subclasses should contribute + * property sources through the protected {@link #customizePropertySources(MutablePropertySources)} + * hook, while clients should customize using {@link ConfigurableEnvironment#getPropertySources()} + * and working against the {@link MutablePropertySources} API. + * See {@link ConfigurableEnvironment} javadoc for usage examples. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see ConfigurableEnvironment + * @see StandardEnvironment + */ +public abstract class AbstractEnvironment implements ConfigurableEnvironment { + + /** + * System property that instructs Spring to ignore system environment variables, + * i.e. to never attempt to retrieve such a variable via {@link System#getenv()}. + * <p>The default is "false", falling back to system environment variable checks if a + * Spring environment property (e.g. a placeholder in a configuration String) isn't + * resolvable otherwise. Consider switching this flag to "true" if you experience + * log warnings from {@code getenv} calls coming from Spring, e.g. on WebSphere + * with strict SecurityManager settings and AccessControlExceptions warnings. + * @see #suppressGetenvAccess() + */ + public static final String IGNORE_GETENV_PROPERTY_NAME = "spring.getenv.ignore"; + + /** + * Name of property to set to specify active profiles: {@value}. Value may be comma + * delimited. + * <p>Note that certain shell environments such as Bash disallow the use of the period + * character in variable names. Assuming that Spring's {@link SystemEnvironmentPropertySource} + * is in use, this property may be specified as an environment variable as + * {@code SPRING_PROFILES_ACTIVE}. + * @see ConfigurableEnvironment#setActiveProfiles + */ + public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active"; + + /** + * Name of property to set to specify profiles active by default: {@value}. Value may + * be comma delimited. + * <p>Note that certain shell environments such as Bash disallow the use of the period + * character in variable names. Assuming that Spring's {@link SystemEnvironmentPropertySource} + * is in use, this property may be specified as an environment variable as + * {@code SPRING_PROFILES_DEFAULT}. + * @see ConfigurableEnvironment#setDefaultProfiles + */ + public static final String DEFAULT_PROFILES_PROPERTY_NAME = "spring.profiles.default"; + + /** + * Name of reserved default profile name: {@value}. If no default profile names are + * explicitly and no active profile names are explicitly set, this profile will + * automatically be activated by default. + * @see #getReservedDefaultProfiles + * @see ConfigurableEnvironment#setDefaultProfiles + * @see ConfigurableEnvironment#setActiveProfiles + * @see AbstractEnvironment#DEFAULT_PROFILES_PROPERTY_NAME + * @see AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME + */ + protected static final String RESERVED_DEFAULT_PROFILE_NAME = "default"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + private Set<String> activeProfiles = new LinkedHashSet<String>(); + + private Set<String> defaultProfiles = new LinkedHashSet<String>(getReservedDefaultProfiles()); + + private final MutablePropertySources propertySources = new MutablePropertySources(this.logger); + + private final ConfigurablePropertyResolver propertyResolver = + new PropertySourcesPropertyResolver(this.propertySources); + + + /** + * Create a new {@code Environment} instance, calling back to + * {@link #customizePropertySources(MutablePropertySources)} during construction to + * allow subclasses to contribute or manipulate {@link PropertySource} instances as + * appropriate. + * @see #customizePropertySources(MutablePropertySources) + */ + public AbstractEnvironment() { + customizePropertySources(this.propertySources); + if (this.logger.isDebugEnabled()) { + this.logger.debug(format( + "Initialized %s with PropertySources %s", getClass().getSimpleName(), this.propertySources)); + } + } + + + /** + * Customize the set of {@link PropertySource} objects to be searched by this + * {@code Environment} during calls to {@link #getProperty(String)} and related + * methods. + * + * <p>Subclasses that override this method are encouraged to add property + * sources using {@link MutablePropertySources#addLast(PropertySource)} such that + * further subclasses may call {@code super.customizePropertySources()} with + * predictable results. For example: + * <pre class="code"> + * public class Level1Environment extends AbstractEnvironment { + * @Override + * protected void customizePropertySources(MutablePropertySources propertySources) { + * super.customizePropertySources(propertySources); // no-op from base class + * propertySources.addLast(new PropertySourceA(...)); + * propertySources.addLast(new PropertySourceB(...)); + * } + * } + * + * public class Level2Environment extends Level1Environment { + * @Override + * protected void customizePropertySources(MutablePropertySources propertySources) { + * super.customizePropertySources(propertySources); // add all from superclass + * propertySources.addLast(new PropertySourceC(...)); + * propertySources.addLast(new PropertySourceD(...)); + * } + * } + * </pre> + * In this arrangement, properties will be resolved against sources A, B, C, D in that + * order. That is to say that property source "A" has precedence over property source + * "D". If the {@code Level2Environment} subclass wished to give property sources C + * and D higher precedence than A and B, it could simply call + * {@code super.customizePropertySources} after, rather than before adding its own: + * <pre class="code"> + * public class Level2Environment extends Level1Environment { + * @Override + * protected void customizePropertySources(MutablePropertySources propertySources) { + * propertySources.addLast(new PropertySourceC(...)); + * propertySources.addLast(new PropertySourceD(...)); + * super.customizePropertySources(propertySources); // add all from superclass + * } + * } + * </pre> + * The search order is now C, D, A, B as desired. + * + * <p>Beyond these recommendations, subclasses may use any of the {@code add*}, + * {@code remove}, or {@code replace} methods exposed by {@link MutablePropertySources} + * in order to create the exact arrangement of property sources desired. + * + * <p>The base implementation registers no property sources. + * + * <p>Note that clients of any {@link ConfigurableEnvironment} may further customize + * property sources via the {@link #getPropertySources()} accessor, typically within + * an {@link org.springframework.context.ApplicationContextInitializer + * ApplicationContextInitializer}. For example: + * <pre class="code"> + * ConfigurableEnvironment env = new StandardEnvironment(); + * env.getPropertySources().addLast(new PropertySourceX(...)); + * </pre> + * + * <h2>A warning about instance variable access</h2> + * Instance variables declared in subclasses and having default initial values should + * <em>not</em> be accessed from within this method. Due to Java object creation + * lifecycle constraints, any initial value will not yet be assigned when this + * callback is invoked by the {@link #AbstractEnvironment()} constructor, which may + * lead to a {@code NullPointerException} or other problems. If you need to access + * default values of instance variables, leave this method as a no-op and perform + * property source manipulation and instance variable access directly within the + * subclass constructor. Note that <em>assigning</em> values to instance variables is + * not problematic; it is only attempting to read default values that must be avoided. + * + * @see MutablePropertySources + * @see PropertySourcesPropertyResolver + * @see org.springframework.context.ApplicationContextInitializer + */ + protected void customizePropertySources(MutablePropertySources propertySources) { + } + + /** + * Return the set of reserved default profile names. This implementation returns + * {@value #RESERVED_DEFAULT_PROFILE_NAME}. Subclasses may override in order to + * customize the set of reserved names. + * @see #RESERVED_DEFAULT_PROFILE_NAME + * @see #doGetDefaultProfiles() + */ + protected Set<String> getReservedDefaultProfiles() { + return Collections.singleton(RESERVED_DEFAULT_PROFILE_NAME); + } + + + //--------------------------------------------------------------------- + // Implementation of ConfigurableEnvironment interface + //--------------------------------------------------------------------- + + public String[] getActiveProfiles() { + return StringUtils.toStringArray(doGetActiveProfiles()); + } + + /** + * Return the set of active profiles as explicitly set through + * {@link #setActiveProfiles} or if the current set of active profiles + * is empty, check for the presence of the {@value #ACTIVE_PROFILES_PROPERTY_NAME} + * property and assign its value to the set of active profiles. + * @see #getActiveProfiles() + * @see #ACTIVE_PROFILES_PROPERTY_NAME + */ + protected Set<String> doGetActiveProfiles() { + if (this.activeProfiles.isEmpty()) { + String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME); + if (StringUtils.hasText(profiles)) { + setActiveProfiles(commaDelimitedListToStringArray(trimAllWhitespace(profiles))); + } + } + return this.activeProfiles; + } + + public void setActiveProfiles(String... profiles) { + Assert.notNull(profiles, "Profile array must not be null"); + this.activeProfiles.clear(); + for (String profile : profiles) { + validateProfile(profile); + this.activeProfiles.add(profile); + } + } + + public void addActiveProfile(String profile) { + if (this.logger.isDebugEnabled()) { + this.logger.debug(format("Activating profile '%s'", profile)); + } + validateProfile(profile); + doGetActiveProfiles(); + this.activeProfiles.add(profile); + } + + + public String[] getDefaultProfiles() { + return StringUtils.toStringArray(doGetDefaultProfiles()); + } + + /** + * Return the set of default profiles explicitly set via + * {@link #setDefaultProfiles(String...)} or if the current set of default profiles + * consists only of {@linkplain #getReservedDefaultProfiles() reserved default + * profiles}, then check for the presence of the + * {@value #DEFAULT_PROFILES_PROPERTY_NAME} property and assign its value (if any) + * to the set of default profiles. + * @see #AbstractEnvironment() + * @see #getDefaultProfiles() + * @see #DEFAULT_PROFILES_PROPERTY_NAME + * @see #getReservedDefaultProfiles() + */ + protected Set<String> doGetDefaultProfiles() { + if (this.defaultProfiles.equals(getReservedDefaultProfiles())) { + String profiles = getProperty(DEFAULT_PROFILES_PROPERTY_NAME); + if (StringUtils.hasText(profiles)) { + setDefaultProfiles(commaDelimitedListToStringArray(trimAllWhitespace(profiles))); + } + } + return this.defaultProfiles; + } + + /** + * Specify the set of profiles to be made active by default if no other profiles + * are explicitly made active through {@link #setActiveProfiles}. + * <p>Calling this method removes overrides any reserved default profiles + * that may have been added during construction of the environment. + * @see #AbstractEnvironment() + * @see #getReservedDefaultProfiles() + */ + public void setDefaultProfiles(String... profiles) { + Assert.notNull(profiles, "Profile array must not be null"); + this.defaultProfiles.clear(); + for (String profile : profiles) { + validateProfile(profile); + this.defaultProfiles.add(profile); + } + } + + public boolean acceptsProfiles(String... profiles) { + Assert.notEmpty(profiles, "Must specify at least one profile"); + for (String profile : profiles) { + if (profile != null && profile.length() > 0 && profile.charAt(0) == '!') { + if (!isProfileActive(profile.substring(1))) { + return true; + } + } + else if (isProfileActive(profile)) { + return true; + } + } + return false; + } + + /** + * Return whether the given profile is active, or if active profiles are empty + * whether the profile should be active by default. + * @throws IllegalArgumentException per {@link #validateProfile(String)} + */ + protected boolean isProfileActive(String profile) { + validateProfile(profile); + return doGetActiveProfiles().contains(profile) || + (doGetActiveProfiles().isEmpty() && doGetDefaultProfiles().contains(profile)); + } + + /** + * Validate the given profile, called internally prior to adding to the set of + * active or default profiles. + * <p>Subclasses may override to impose further restrictions on profile syntax. + * @throws IllegalArgumentException if the profile is null, empty, whitespace-only or + * begins with the profile NOT operator (!). + * @see #acceptsProfiles + * @see #addActiveProfile + * @see #setDefaultProfiles + */ + protected void validateProfile(String profile) { + if (!StringUtils.hasText(profile)) { + throw new IllegalArgumentException("Invalid profile [" + profile + "]: must contain text"); + } + if (profile.charAt(0) == '!') { + throw new IllegalArgumentException("Invalid profile [" + profile + "]: must not begin with ! operator"); + } + } + + public MutablePropertySources getPropertySources() { + return this.propertySources; + } + + @SuppressWarnings("unchecked") + public Map<String, Object> getSystemEnvironment() { + if (suppressGetenvAccess()) { + return Collections.emptyMap(); + } + try { + return (Map) System.getenv(); + } + catch (AccessControlException ex) { + return (Map) new ReadOnlySystemAttributesMap() { + @Override + protected String getSystemAttribute(String attributeName) { + try { + return System.getenv(attributeName); + } + catch (AccessControlException ex) { + if (logger.isInfoEnabled()) { + logger.info(format("Caught AccessControlException when accessing system " + + "environment variable [%s]; its value will be returned [null]. Reason: %s", + attributeName, ex.getMessage())); + } + return null; + } + } + }; + } + } + + /** + * Determine whether to suppress {@link System#getenv()}/{@link System#getenv(String)} + * access for the purposes of {@link #getSystemEnvironment()}. + * <p>If this method returns {@code true}, an empty dummy Map will be used instead + * of the regular system environment Map, never even trying to call {@code getenv} + * and therefore avoiding security manager warnings (if any). + * <p>The default implementation checks for the "spring.getenv.ignore" system property, + * returning {@code true} if its value equals "true" in any case. + * @see #IGNORE_GETENV_PROPERTY_NAME + * @see SpringProperties#getFlag + */ + protected boolean suppressGetenvAccess() { + return SpringProperties.getFlag(IGNORE_GETENV_PROPERTY_NAME); + } + + @SuppressWarnings("unchecked") + public Map<String, Object> getSystemProperties() { + try { + return (Map) System.getProperties(); + } + catch (AccessControlException ex) { + return (Map) new ReadOnlySystemAttributesMap() { + @Override + protected String getSystemAttribute(String attributeName) { + try { + return System.getProperty(attributeName); + } + catch (AccessControlException ex) { + if (logger.isInfoEnabled()) { + logger.info(format("Caught AccessControlException when accessing system " + + "property [%s]; its value will be returned [null]. Reason: %s", + attributeName, ex.getMessage())); + } + return null; + } + } + }; + } + } + + public void merge(ConfigurableEnvironment parent) { + for (PropertySource<?> ps : parent.getPropertySources()) { + if (!this.propertySources.contains(ps.getName())) { + this.propertySources.addLast(ps); + } + } + for (String profile : parent.getActiveProfiles()) { + this.activeProfiles.add(profile); + } + if (parent.getDefaultProfiles().length > 0) { + this.defaultProfiles.remove(RESERVED_DEFAULT_PROFILE_NAME); + for (String profile : parent.getDefaultProfiles()) { + this.defaultProfiles.add(profile); + } + } + } + + + //--------------------------------------------------------------------- + // Implementation of ConfigurablePropertyResolver interface + //--------------------------------------------------------------------- + + public ConfigurableConversionService getConversionService() { + return this.propertyResolver.getConversionService(); + } + + public void setConversionService(ConfigurableConversionService conversionService) { + this.propertyResolver.setConversionService(conversionService); + } + + public void setPlaceholderPrefix(String placeholderPrefix) { + this.propertyResolver.setPlaceholderPrefix(placeholderPrefix); + } + + public void setPlaceholderSuffix(String placeholderSuffix) { + this.propertyResolver.setPlaceholderSuffix(placeholderSuffix); + } + + public void setValueSeparator(String valueSeparator) { + this.propertyResolver.setValueSeparator(valueSeparator); + } + + public void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders) { + this.propertyResolver.setIgnoreUnresolvableNestedPlaceholders(ignoreUnresolvableNestedPlaceholders); + } + + public void setRequiredProperties(String... requiredProperties) { + this.propertyResolver.setRequiredProperties(requiredProperties); + } + + public void validateRequiredProperties() throws MissingRequiredPropertiesException { + this.propertyResolver.validateRequiredProperties(); + } + + + //--------------------------------------------------------------------- + // Implementation of PropertyResolver interface + //--------------------------------------------------------------------- + + @Override + public boolean containsProperty(String key) { + return this.propertyResolver.containsProperty(key); + } + + public String getProperty(String key) { + return this.propertyResolver.getProperty(key); + } + + public String getProperty(String key, String defaultValue) { + return this.propertyResolver.getProperty(key, defaultValue); + } + + public <T> T getProperty(String key, Class<T> targetType) { + return this.propertyResolver.getProperty(key, targetType); + } + + public <T> T getProperty(String key, Class<T> targetType, T defaultValue) { + return this.propertyResolver.getProperty(key, targetType, defaultValue); + } + + public <T> Class<T> getPropertyAsClass(String key, Class<T> targetType) { + return this.propertyResolver.getPropertyAsClass(key, targetType); + } + + public String getRequiredProperty(String key) throws IllegalStateException { + return this.propertyResolver.getRequiredProperty(key); + } + + public <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException { + return this.propertyResolver.getRequiredProperty(key, targetType); + } + + public String resolvePlaceholders(String text) { + return this.propertyResolver.resolvePlaceholders(text); + } + + public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException { + return this.propertyResolver.resolveRequiredPlaceholders(text); + } + + + @Override + public String toString() { + return format("%s {activeProfiles=%s, defaultProfiles=%s, propertySources=%s}", + getClass().getSimpleName(), this.activeProfiles, this.defaultProfiles, + this.propertySources); + } + +} 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 new file mode 100644 index 00000000..4b02991a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java @@ -0,0 +1,206 @@ +/* + * Copyright 2002-2014 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.env; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.util.PropertyPlaceholderHelper; +import org.springframework.util.SystemPropertyUtils; + +/** + * Abstract base class for resolving properties against any underlying source. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver { + + protected final Log logger = LogFactory.getLog(getClass()); + + protected ConfigurableConversionService conversionService = new DefaultConversionService(); + + private PropertyPlaceholderHelper nonStrictHelper; + + private PropertyPlaceholderHelper strictHelper; + + private boolean ignoreUnresolvableNestedPlaceholders = false; + + private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX; + + private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX; + + private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR; + + private final Set<String> requiredProperties = new LinkedHashSet<String>(); + + + public ConfigurableConversionService getConversionService() { + return this.conversionService; + } + + public void setConversionService(ConfigurableConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Set the prefix that placeholders replaced by this resolver must begin with. + * <p>The default is "${". + * @see org.springframework.util.SystemPropertyUtils#PLACEHOLDER_PREFIX + */ + public void setPlaceholderPrefix(String placeholderPrefix) { + this.placeholderPrefix = placeholderPrefix; + } + + /** + * Set the suffix that placeholders replaced by this resolver must end with. + * <p>The default is "}". + * @see org.springframework.util.SystemPropertyUtils#PLACEHOLDER_SUFFIX + */ + public void setPlaceholderSuffix(String placeholderSuffix) { + this.placeholderSuffix = placeholderSuffix; + } + + /** + * Specify the separating character between the placeholders replaced by this + * resolver and their associated default value, or {@code null} if no such + * special character should be processed as a value separator. + * <p>The default is ":". + * @see org.springframework.util.SystemPropertyUtils#VALUE_SEPARATOR + */ + public void setValueSeparator(String valueSeparator) { + this.valueSeparator = valueSeparator; + } + + /** + * Set whether to throw an exception when encountering an unresolvable placeholder + * nested within the value of a given property. A {@code false} value indicates strict + * resolution, i.e. that an exception will be thrown. A {@code true} value indicates + * that unresolvable nested placeholders should be passed through in their unresolved + * ${...} form. + * <p>The default is {@code false}. + * @since 3.2 + */ + public void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders) { + this.ignoreUnresolvableNestedPlaceholders = ignoreUnresolvableNestedPlaceholders; + } + + public void setRequiredProperties(String... requiredProperties) { + for (String key : requiredProperties) { + this.requiredProperties.add(key); + } + } + + public void validateRequiredProperties() { + MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException(); + for (String key : this.requiredProperties) { + if (this.getProperty(key) == null) { + ex.addMissingRequiredProperty(key); + } + } + if (!ex.getMissingRequiredProperties().isEmpty()) { + throw ex; + } + } + + + public String getProperty(String key, String defaultValue) { + String value = getProperty(key); + return (value != null ? value : defaultValue); + } + + public <T> T getProperty(String key, Class<T> targetType, T defaultValue) { + T value = getProperty(key, targetType); + return (value != null ? value : defaultValue); + } + + public String getRequiredProperty(String key) throws IllegalStateException { + String value = getProperty(key); + if (value == null) { + throw new IllegalStateException(String.format("required key [%s] not found", key)); + } + return value; + } + + public <T> T getRequiredProperty(String key, Class<T> valueType) throws IllegalStateException { + T value = getProperty(key, valueType); + if (value == null) { + throw new IllegalStateException(String.format("required key [%s] not found", key)); + } + return value; + } + + public String resolvePlaceholders(String text) { + if (this.nonStrictHelper == null) { + this.nonStrictHelper = createPlaceholderHelper(true); + } + return doResolvePlaceholders(text, this.nonStrictHelper); + } + + public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException { + if (this.strictHelper == null) { + this.strictHelper = createPlaceholderHelper(false); + } + return doResolvePlaceholders(text, this.strictHelper); + } + + /** + * Resolve placeholders within the given string, deferring to the value of + * {@link #setIgnoreUnresolvableNestedPlaceholders} to determine whether any + * unresolvable placeholders should raise an exception or be ignored. + * <p>Invoked from {@link #getProperty} and its variants, implicitly resolving + * nested placeholders. In contrast, {@link #resolvePlaceholders} and + * {@link #resolveRequiredPlaceholders} do <emphasis>not</emphasis> delegate + * to this method but rather perform their own handling of unresolvable + * placeholders, as specified by each of those methods. + * @since 3.2 + * @see #setIgnoreUnresolvableNestedPlaceholders + */ + protected String resolveNestedPlaceholders(String value) { + return (this.ignoreUnresolvableNestedPlaceholders ? + resolvePlaceholders(value) : resolveRequiredPlaceholders(value)); + } + + private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) { + return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix, + this.valueSeparator, ignoreUnresolvablePlaceholders); + } + + private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) { + return helper.replacePlaceholders(text, new PropertyPlaceholderHelper.PlaceholderResolver() { + public String resolvePlaceholder(String placeholderName) { + return getPropertyAsRawString(placeholderName); + } + }); + } + + + /** + * Retrieve the specified property as a raw String, + * i.e. without resolution of nested placeholders. + * @param key the property name to resolve + * @return the property value or {@code null} if none found + */ + protected abstract String getPropertyAsRawString(String key); + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java b/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java new file mode 100644 index 00000000..189484a6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2011 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.env; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A simple representation of command line arguments, broken into "option arguments" and + * "non-option arguments". + * + * @author Chris Beams + * @since 3.1 + * @see SimpleCommandLineArgsParser + */ +class CommandLineArgs { + + private final Map<String, List<String>> optionArgs = new HashMap<String, List<String>>(); + private final List<String> nonOptionArgs = new ArrayList<String>(); + + /** + * Add an option argument for the given option name and add the given value to the + * list of values associated with this option (of which there may be zero or more). + * The given value may be {@code null}, indicating that the option was specified + * without an associated value (e.g. "--foo" vs. "--foo=bar"). + */ + public void addOptionArg(String optionName, String optionValue) { + if (!this.optionArgs.containsKey(optionName)) { + this.optionArgs.put(optionName, new ArrayList<String>()); + } + if (optionValue != null) { + this.optionArgs.get(optionName).add(optionValue); + } + } + + /** + * Return the set of all option arguments present on the command line. + */ + public Set<String> getOptionNames() { + return Collections.unmodifiableSet(this.optionArgs.keySet()); + } + + /** + * Return whether the option with the given name was present on the command line. + */ + public boolean containsOption(String optionName) { + return this.optionArgs.containsKey(optionName); + } + + /** + * Return the list of values associated with the given option. {@code null} signifies + * that the option was not present; empty list signifies that no values were associated + * with this option. + */ + public List<String> getOptionValues(String optionName) { + return this.optionArgs.get(optionName); + } + + /** + * Add the given value to the list of non-option arguments. + */ + public void addNonOptionArg(String value) { + this.nonOptionArgs.add(value); + } + + /** + * Return the list of non-option arguments specified on the command line. + */ + public List<String> getNonOptionArgs() { + return Collections.unmodifiableList(this.nonOptionArgs); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java new file mode 100644 index 00000000..575cd359 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java @@ -0,0 +1,298 @@ +/* + * Copyright 2002-2014 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.env; + +import java.util.Collection; +import java.util.List; + +import org.springframework.util.StringUtils; + +/** + * Abstract base class for {@link PropertySource} implementations backed by command line + * arguments. The parameterized type {@code T} represents the underlying source of command + * line options. This may be as simple as a String array in the case of + * {@link SimpleCommandLinePropertySource}, or specific to a particular API such as JOpt's + * {@code OptionSet} in the case of {@link JOptCommandLinePropertySource}. + * + * <h3>Purpose and General Usage</h3> + * For use in standalone Spring-based applications, i.e. those that are bootstrapped via + * a traditional {@code main} method accepting a {@code String[]} of arguments from the + * command line. In many cases, processing command-line arguments directly within the + * {@code main} method may be sufficient, but in other cases, it may be desirable to + * inject arguments as values into Spring beans. It is this latter set of cases in which + * a {@code CommandLinePropertySource} becomes useful. A {@code CommandLinePropertySource} + * will typically be added to the {@link Environment} of the Spring + * {@code ApplicationContext}, at which point all command line arguments become available + * through the {@link Environment#getProperty(String)} family of methods. For example: + * <pre class="code"> + * public static void main(String[] args) { + * CommandLinePropertySource clps = ...; + * AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + * ctx.getEnvironment().getPropertySources().addFirst(clps); + * ctx.register(AppConfig.class); + * ctx.refresh(); + * }</pre> + * With the bootstrap logic above, the {@code AppConfig} class may {@code @Inject} the + * Spring {@code Environment} and query it directly for properties: + * <pre class="code"> + * @Configuration + * public class AppConfig { + * @Inject Environment env; + * + * @Bean + * public void DataSource dataSource() { + * MyVendorDataSource dataSource = new MyVendorDataSource(); + * dataSource.setHostname(env.getProperty("db.hostname", "localhost")); + * dataSource.setUsername(env.getRequiredProperty("db.username")); + * dataSource.setPassword(env.getRequiredProperty("db.password")); + * // ... + * return dataSource; + * } + * }</pre> + * Because the {@code CommandLinePropertySource} was added to the {@code Environment}'s + * set of {@link MutablePropertySources} using the {@code #addFirst} method, it has + * highest search precedence, meaning that while "db.hostname" and other properties may + * exist in other property sources such as the system environment variables, it will be + * chosen from the command line property source first. This is a reasonable approach + * given that arguments specified on the command line are naturally more specific than + * those specified as environment variables. + * + * <p>As an alternative to injecting the {@code Environment}, Spring's {@code @Value} + * annotation may be used to inject these properties, given that a {@link + * PropertySourcesPropertyResolver} bean has been registered, either directly or through + * using the {@code <context:property-placeholder>} element. For example: + * <pre class="code"> + * @Component + * public class MyComponent { + * @Value("my.property:defaultVal") + * private String myProperty; + * + * public void getMyProperty() { + * return this.myProperty; + * } + * + * // ... + * }</pre> + * + * <h3>Working with option arguments</h3> + * + * <p>Individual command line arguments are represented as properties through the usual + * {@link PropertySource#getProperty(String)} and + * {@link PropertySource#containsProperty(String)} methods. For example, given the + * following command line: + * <pre class="code"> + * --o1=v1 --o2</pre> + * 'o1' and 'o2' are treated as "option arguments", and the following assertions would + * evaluate true: + * <pre class="code"> + * CommandLinePropertySource<?> ps = ... + * assert ps.containsProperty("o1") == true; + * assert ps.containsProperty("o2") == true; + * assert ps.containsProperty("o3") == false; + * assert ps.getProperty("o1").equals("v1"); + * assert ps.getProperty("o2").equals(""); + * assert ps.getProperty("o3") == null;</pre> + * + * Note that the 'o2' option has no argument, but {@code getProperty("o2")} resolves to + * empty string ({@code ""}) as opposed to {@code null}, while {@code getProperty("o3")} + * resolves to {@code null} because it was not specified. This behavior is consistent with + * the general contract to be followed by all {@code PropertySource} implementations. + * + * <p>Note also that while "--" was used in the examples above to denote an option + * argument, this syntax may vary across individual command line argument libraries. For + * example, a JOpt- or Commons CLI-based implementation may allow for single dash ("-") + * "short" option arguments, etc. + * + * <h3>Working with non-option arguments</h3> + * + * <p>Non-option arguments are also supported through this abstraction. Any arguments + * supplied without an option-style prefix such as "-" or "--" are considered "non-option + * arguments" and available through the special {@linkplain + * #DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME "nonOptionArgs"} property. If multiple + * non-option arguments are specified, the value of this property will be a + * comma-delimited string containing all of the arguments. This approach ensures a simple + * and consistent return type (String) for all properties from a {@code + * CommandLinePropertySource} and at the same time lends itself to conversion when used + * in conjunction with the Spring {@link Environment} and its built-in {@code + * ConversionService}. Consider the following example: + * <pre class="code"> + * --o1=v1 --o2=v2 /path/to/file1 /path/to/file2</pre> + * In this example, "o1" and "o2" would be considered "option arguments", while the two + * filesystem paths qualify as "non-option arguments". As such, the following assertions + * will evaluate true: + * <pre class="code"> + * CommandLinePropertySource<?> ps = ... + * assert ps.containsProperty("o1") == true; + * assert ps.containsProperty("o2") == true; + * assert ps.containsProperty("nonOptionArgs") == true; + * assert ps.getProperty("o1").equals("v1"); + * assert ps.getProperty("o2").equals("v2"); + * assert ps.getProperty("nonOptionArgs").equals("/path/to/file1,/path/to/file2");</pre> + * + * <p>As mentioned above, when used in conjunction with the Spring {@code Environment} + * abstraction, this comma-delimited string may easily be converted to a String array or + * list: + * <pre class="code"> + * Environment env = applicationContext.getEnvironment(); + * String[] nonOptionArgs = env.getProperty("nonOptionArgs", String[].class); + * assert nonOptionArgs[0].equals("/path/to/file1"); + * assert nonOptionArgs[1].equals("/path/to/file2");</pre> + * + * <p>The name of the special "non-option arguments" property may be customized through + * the {@link #setNonOptionArgsPropertyName(String)} method. Doing so is recommended as + * it gives proper semantic value to non-option arguments. For example, if filesystem + * paths are being specified as non-option arguments, it is likely preferable to refer to + * these as something like "file.locations" than the default of "nonOptionArgs": + * <pre class="code"> + * public static void main(String[] args) { + * CommandLinePropertySource clps = ...; + * clps.setNonOptionArgsPropertyName("file.locations"); + * + * AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + * ctx.getEnvironment().getPropertySources().addFirst(clps); + * ctx.register(AppConfig.class); + * ctx.refresh(); + * }</pre> + * + * <h3>Limitations</h3> + * This abstraction is not intended to expose the full power of underlying command line + * parsing APIs such as JOpt or Commons CLI. It's intent is rather just the opposite: to + * provide the simplest possible abstraction for accessing command line arguments + * <em>after</em> they have been parsed. So the typical case will involve fully configuring + * the underlying command line parsing API, parsing the {@code String[]} of arguments + * coming into the main method, and then simply providing the parsing results to an + * implementation of {@code CommandLinePropertySource}. At that point, all arguments can + * be considered either 'option' or 'non-option' arguments and as described above can be + * accessed through the normal {@code PropertySource} and {@code Environment} APIs. + * + * @author Chris Beams + * @since 3.1 + * @see PropertySource + * @see SimpleCommandLinePropertySource + * @see JOptCommandLinePropertySource + */ +public abstract class CommandLinePropertySource<T> extends PropertySource<T> { + + /** The default name given to {@link CommandLinePropertySource} instances: {@value} */ + public static final String COMMAND_LINE_PROPERTY_SOURCE_NAME = "commandLineArgs"; + + /** The default name of the property representing non-option arguments: {@value} */ + public static final String DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME = "nonOptionArgs"; + + + private String nonOptionArgsPropertyName = DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME; + + + /** + * Create a new {@code CommandLinePropertySource} having the default name + * {@value #COMMAND_LINE_PROPERTY_SOURCE_NAME} and backed by the given source object. + */ + public CommandLinePropertySource(T source) { + super(COMMAND_LINE_PROPERTY_SOURCE_NAME, source); + } + + /** + * Create a new {@link CommandLinePropertySource} having the given name + * and backed by the given source object. + */ + public CommandLinePropertySource(String name, T source) { + super(name, source); + } + + + /** + * Specify the name of the special "non-option arguments" property. + * The default is {@value #DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME}. + */ + public void setNonOptionArgsPropertyName(String nonOptionArgsPropertyName) { + this.nonOptionArgsPropertyName = nonOptionArgsPropertyName; + } + + /** + * This implementation first checks to see if the name specified is the special + * {@linkplain #setNonOptionArgsPropertyName(String) "non-option arguments" property}, + * and if so delegates to the abstract {@link #getNonOptionArgs()} method + * checking to see whether it returns an empty collection. Otherwise delegates to and + * returns the value of the abstract {@link #containsOption(String)} method. + */ + @Override + public final boolean containsProperty(String name) { + if (this.nonOptionArgsPropertyName.equals(name)) { + return !this.getNonOptionArgs().isEmpty(); + } + return this.containsOption(name); + } + + /** + * This implementation first checks to see if the name specified is the special + * {@linkplain #setNonOptionArgsPropertyName(String) "non-option arguments" property}, + * and if so delegates to the abstract {@link #getNonOptionArgs()} method. If so + * and the collection of non-option arguments is empty, this method returns {@code + * null}. If not empty, it returns a comma-separated String of all non-option + * arguments. Otherwise delegates to and returns the result of the abstract {@link + * #getOptionValues(String)} method. + */ + @Override + public final String getProperty(String name) { + if (this.nonOptionArgsPropertyName.equals(name)) { + Collection<String> nonOptionArguments = this.getNonOptionArgs(); + if (nonOptionArguments.isEmpty()) { + return null; + } + else { + return StringUtils.collectionToCommaDelimitedString(nonOptionArguments); + } + } + Collection<String> optionValues = this.getOptionValues(name); + if (optionValues == null) { + return null; + } + else { + return StringUtils.collectionToCommaDelimitedString(optionValues); + } + } + + + /** + * Return whether the set of option arguments parsed from the command line contains + * an option with the given name. + */ + protected abstract boolean containsOption(String name); + + /** + * Return the collection of values associated with the command line option having the + * given name. + * <ul> + * <li>if the option is present and has no argument (e.g.: "--foo"), return an empty + * collection ({@code []})</li> + * <li>if the option is present and has a single value (e.g. "--foo=bar"), return a + * collection having one element ({@code ["bar"]})</li> + * <li>if the option is present and the underlying command line parsing library + * supports multiple arguments (e.g. "--foo=bar --foo=baz"), return a collection + * having elements for each value ({@code ["bar", "baz"]})</li> + * <li>if the option is not present, return {@code null}</li> + * </ul> + */ + protected abstract List<String> getOptionValues(String name); + + /** + * Return the collection of non-option arguments parsed from the command line. + * Never {@code null}. + */ + protected abstract List<String> getNonOptionArgs(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java new file mode 100644 index 00000000..f05d0ce4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2014 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.env; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Composite {@link PropertySource} implementation that iterates over a set of + * {@link PropertySource} instances. Necessary in cases where multiple property sources + * share the same name, e.g. when multiple values are supplied to {@code @PropertySource}. + * + * @author Chris Beams + * @since 3.1.1 + */ +public class CompositePropertySource extends PropertySource<Object> { + + private final Set<PropertySource<?>> propertySources = new LinkedHashSet<PropertySource<?>>(); + + + /** + * Create a new {@code CompositePropertySource}. + * @param name the name of the property source + */ + public CompositePropertySource(String name) { + super(name); + } + + + @Override + public Object getProperty(String name) { + for (PropertySource<?> propertySource : this.propertySources) { + Object candidate = propertySource.getProperty(name); + if (candidate != null) { + return candidate; + } + } + return null; + } + + public void addPropertySource(PropertySource<?> propertySource) { + this.propertySources.add(propertySource); + } + + @Override + public String toString() { + return String.format("%s [name='%s', propertySources=%s]", + getClass().getSimpleName(), this.name, this.propertySources); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.java new file mode 100644 index 00000000..9e7cb58e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.java @@ -0,0 +1,172 @@ +/* + * Copyright 2002-2012 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.env; + +import java.util.Map; + +/** + * Configuration interface to be implemented by most if not all {@link Environment} types. + * Provides facilities for setting active and default profiles and manipulating underlying + * property sources. Allows clients to set and validate required properties, customize the + * conversion service and more through the {@link ConfigurablePropertyResolver} + * superinterface. + * + * <h2>Manipulating property sources</h2> + * <p>Property sources may be removed, reordered, or replaced; and additional + * property sources may be added using the {@link MutablePropertySources} + * instance returned from {@link #getPropertySources()}. The following examples + * are against the {@link StandardEnvironment} implementation of + * {@code ConfigurableEnvironment}, but are generally applicable to any implementation, + * though particular default property sources may differ. + * + * <h4>Example: adding a new property source with highest search priority</h4> + * <pre class="code"> + * ConfigurableEnvironment environment = new StandardEnvironment(); + * MutablePropertySources propertySources = environment.getPropertySources(); + * Map<String, String> myMap = new HashMap<String, String>(); + * myMap.put("xyz", "myValue"); + * propertySources.addFirst(new MapPropertySource("MY_MAP", myMap)); + * </pre> + * + * <h4>Example: removing the default system properties property source</h4> + * <pre class="code"> + * MutablePropertySources propertySources = environment.getPropertySources(); + * propertySources.remove(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME) + * </pre> + * + * <h4>Example: mocking the system environment for testing purposes</h4> + * <pre class="code"> + * MutablePropertySources propertySources = environment.getPropertySources(); + * MockPropertySource mockEnvVars = new MockPropertySource().withProperty("xyz", "myValue"); + * propertySources.replace(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, mockEnvVars); + * </pre> + * + * When an {@link Environment} is being used by an {@code ApplicationContext}, it is + * important that any such {@code PropertySource} manipulations be performed + * <em>before</em> the context's {@link + * org.springframework.context.support.AbstractApplicationContext#refresh() refresh()} + * method is called. This ensures that all property sources are available during the + * container bootstrap process, including use by {@linkplain + * org.springframework.context.support.PropertySourcesPlaceholderConfigurer property + * placeholder configurers}. + * + * + * @author Chris Beams + * @since 3.1 + * @see StandardEnvironment + * @see org.springframework.context.ConfigurableApplicationContext#getEnvironment + */ +public interface ConfigurableEnvironment extends Environment, ConfigurablePropertyResolver { + + /** + * Specify the set of profiles active for this {@code Environment}. Profiles are + * evaluated during container bootstrap to determine whether bean definitions + * should be registered with the container. + * <p>Any existing active profiles will be replaced with the given arguments; call + * with zero arguments to clear the current set of active profiles. Use + * {@link #addActiveProfile} to add a profile while preserving the existing set. + * @see #addActiveProfile + * @see #setDefaultProfiles + * @see org.springframework.context.annotation.Profile + * @see AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME + * @throws IllegalArgumentException if any profile is null, empty or whitespace-only + */ + void setActiveProfiles(String... profiles); + + /** + * Add a profile to the current set of active profiles. + * @see #setActiveProfiles + * @throws IllegalArgumentException if the profile is null, empty or whitespace-only + */ + void addActiveProfile(String profile); + + /** + * Specify the set of profiles to be made active by default if no other profiles + * are explicitly made active through {@link #setActiveProfiles}. + * @see AbstractEnvironment#DEFAULT_PROFILES_PROPERTY_NAME + * @throws IllegalArgumentException if any profile is null, empty or whitespace-only + */ + void setDefaultProfiles(String... profiles); + + /** + * Return the {@link PropertySources} for this {@code Environment} in mutable form, + * allowing for manipulation of the set of {@link PropertySource} objects that should + * be searched when resolving properties against this {@code Environment} object. + * The various {@link MutablePropertySources} methods such as + * {@link MutablePropertySources#addFirst addFirst}, + * {@link MutablePropertySources#addFirst addLast}, + * {@link MutablePropertySources#addFirst addBefore} and + * {@link MutablePropertySources#addFirst addAfter} allow for fine-grained control + * over property source ordering. This is useful, for example, in ensuring that + * certain user-defined property sources have search precedence over default property + * sources such as the set of system properties or the set of system environment + * variables. + * @see AbstractEnvironment#customizePropertySources + */ + MutablePropertySources getPropertySources(); + + /** + * Return the value of {@link System#getenv()} if allowed by the current + * {@link SecurityManager}, otherwise return a map implementation that will attempt + * to access individual keys using calls to {@link System#getenv(String)}. + * <p>Note that most {@link Environment} implementations will include this system + * environment map as a default {@link PropertySource} to be searched. Therefore, it + * is recommended that this method not be used directly unless bypassing other + * property sources is expressly intended. + * <p>Calls to {@link Map#get(Object)} on the Map returned will never throw + * {@link IllegalAccessException}; in cases where the SecurityManager forbids access + * to a property, {@code null} will be returned and an INFO-level log message will be + * issued noting the exception. + */ + Map<String, Object> getSystemEnvironment(); + + /** + * Return the value of {@link System#getProperties()} if allowed by the current + * {@link SecurityManager}, otherwise return a map implementation that will attempt + * to access individual keys using calls to {@link System#getProperty(String)}. + * <p>Note that most {@code Environment} implementations will include this system + * properties map as a default {@link PropertySource} to be searched. Therefore, it is + * recommended that this method not be used directly unless bypassing other property + * sources is expressly intended. + * <p>Calls to {@link Map#get(Object)} on the Map returned will never throw + * {@link IllegalAccessException}; in cases where the SecurityManager forbids access + * to a property, {@code null} will be returned and an INFO-level log message will be + * issued noting the exception. + */ + Map<String, Object> getSystemProperties(); + + /** + * Append the given parent environment's active profiles, default profiles and + * property sources to this (child) environment's respective collections of each. + * <p>For any identically-named {@code PropertySource} instance existing in both + * parent and child, the child instance is to be preserved and the parent instance + * discarded. This has the effect of allowing overriding of property sources by the + * child as well as avoiding redundant searches through common property source types, + * e.g. system environment and system properties. + * <p>Active and default profile names are also filtered for duplicates, to avoid + * confusion and redundant storage. + * <p>The parent environment remains unmodified in any case. Note that any changes to + * the parent environment occurring after the call to {@code merge} will not be + * reflected in the child. Therefore, care should be taken to configure parent + * property sources and profile information prior to calling {@code merge}. + * @param parent the environment to merge with + * @since 3.1.2 + * @see org.springframework.context.support.AbstractApplicationContext#setParent + */ + void merge(ConfigurableEnvironment parent); + +} 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 new file mode 100644 index 00000000..e1ec8a27 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2014 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.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. + * + * @author Chris Beams + * @since 3.1 + */ +public interface ConfigurablePropertyResolver extends PropertyResolver { + + /** + * @return the {@link ConfigurableConversionService} used when performing type + * conversions on properties. + * <p>The configurable nature of the returned conversion service allows for + * the convenient addition and removal of individual {@code Converter} instances: + * <pre class="code"> + * ConfigurableConversionService cs = env.getConversionService(); + * cs.addConverter(new FooConverter()); + * </pre> + * @see PropertyResolver#getProperty(String, Class) + * @see org.springframework.core.convert.converter.ConverterRegistry#addConverter + */ + ConfigurableConversionService getConversionService(); + + /** + * Set the {@link ConfigurableConversionService} to be used when performing type + * conversions on properties. + * <p><strong>Note:</strong> 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 + */ + void setConversionService(ConfigurableConversionService conversionService); + + /** + * Set the prefix that placeholders replaced by this resolver must begin with. + */ + void setPlaceholderPrefix(String placeholderPrefix); + + /** + * Set the suffix that placeholders replaced by this resolver must end with. + */ + void setPlaceholderSuffix(String placeholderSuffix); + + /** + * Specify the separating character between the placeholders replaced by this + * resolver and their associated default value, or {@code null} if no such + * special character should be processed as a value separator. + */ + void setValueSeparator(String valueSeparator); + + /** + * Set whether to throw an exception when encountering an unresolvable placeholder + * nested within the value of a given property. A {@code false} value indicates strict + * resolution, i.e. that an exception will be thrown. A {@code true} value indicates + * that unresolvable nested placeholders should be passed through in their unresolved + * ${...} form. + * <p>Implementations of {@link #getProperty(String)} and its variants must inspect + * the value set here to determine correct behavior when property values contain + * unresolvable placeholders. + * @since 3.2 + */ + void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders); + + /** + * Specify which properties must be present, to be verified by + * {@link #validateRequiredProperties()}. + */ + void setRequiredProperties(String... requiredProperties); + + /** + * Validate that each of the properties specified by + * {@link #setRequiredProperties} is present and resolves to a + * non-{@code null} value. + * @throws MissingRequiredPropertiesException if any of the required + * properties are not resolvable. + */ + void validateRequiredProperties() throws MissingRequiredPropertiesException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/EnumerablePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/EnumerablePropertySource.java new file mode 100644 index 00000000..c6742025 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/EnumerablePropertySource.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2014 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.env; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; + +/** + * A {@link PropertySource} implementation capable of interrogating its + * underlying source object to enumerate all possible property name/value + * pairs. Exposes the {@link #getPropertyNames()} method to allow callers + * to introspect available properties without having to access the underlying + * source object. This also facilitates a more efficient implementation of + * {@link #containsProperty(String)}, in that it can call {@link #getPropertyNames()} + * and iterate through the returned array rather than attempting a call to + * {@link #getProperty(String)} which may be more expensive. Implementations may + * consider caching the result of {@link #getPropertyNames()} to fully exploit this + * performance opportunity. + * + * Most framework-provided {@code PropertySource} implementations are enumerable; + * a counter-example would be {@code JndiPropertySource} where, due to the + * nature of JNDI it is not possible to determine all possible property names at + * any given time; rather it is only possible to try to access a property + * (via {@link #getProperty(String)}) in order to evaluate whether it is present + * or not. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public abstract class EnumerablePropertySource<T> extends PropertySource<T> { + + @Deprecated + protected static final String[] EMPTY_NAMES_ARRAY = new String[0]; + + protected final Log logger = LogFactory.getLog(getClass()); + + + public EnumerablePropertySource(String name, T source) { + super(name, source); + } + + + /** + * Return whether this {@code PropertySource} contains a property with the given name. + * <p>This implementation checks for the presence of the given name within the + * {@link #getPropertyNames()} array. + * @param name the name of the property to find + */ + public boolean containsProperty(String name) { + Assert.notNull(name, "Property name must not be null"); + for (String candidate : getPropertyNames()) { + if (candidate.equals(name)) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("PropertySource [%s] contains '%s'", getName(), name)); + } + return true; + } + } + if (logger.isTraceEnabled()) { + logger.trace(String.format("PropertySource [%s] does not contain '%s'", getName(), name)); + } + return false; + } + + /** + * Return the names of all properties contained by the + * {@linkplain #getSource() source} object (never {@code null}). + */ + public abstract String[] getPropertyNames(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/Environment.java b/spring-core/src/main/java/org/springframework/core/env/Environment.java new file mode 100644 index 00000000..17767732 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/Environment.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2013 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.env; + +/** + * Interface representing the environment in which the current application is running. + * Models two key aspects of the application environment: <em>profiles</em> and + * <em>properties</em>. Methods related to property access are exposed via the + * {@link PropertyResolver} superinterface. + * + * <p>A <em>profile</em> is a named, logical group of bean definitions to be registered + * with the container only if the given profile is <em>active</em>. Beans may be assigned + * to a profile whether defined in XML or via annotations; see the spring-beans 3.1 schema + * or the {@link org.springframework.context.annotation.Profile @Profile} annotation for + * syntax details. The role of the {@code Environment} object with relation to profiles is + * in determining which profiles (if any) are currently {@linkplain #getActiveProfiles + * active}, and which profiles (if any) should be {@linkplain #getDefaultProfiles active + * by default}. + * + * <p><em>Properties</em> play an important role in almost all applications, and may + * originate from a variety of sources: properties files, JVM system properties, system + * environment variables, JNDI, servlet context parameters, ad-hoc Properties objects, + * Maps, and so on. The role of the environment object with relation to properties is to + * provide the user with a convenient service interface for configuring property sources + * and resolving properties from them. + * + * <p>Beans managed within an {@code ApplicationContext} may register to be {@link + * org.springframework.context.EnvironmentAware EnvironmentAware} or {@code @Inject} the + * {@code Environment} in order to query profile state or resolve properties directly. + * + * <p>In most cases, however, application-level beans should not need to interact with the + * {@code Environment} directly but instead may have to have {@code ${...}} property + * values replaced by a property placeholder configurer such as + * {@link org.springframework.context.support.PropertySourcesPlaceholderConfigurer + * PropertySourcesPlaceholderConfigurer}, which itself is {@code EnvironmentAware} and + * as of Spring 3.1 is registered by default when using + * {@code <context:property-placeholder/>}. + * + * <p>Configuration of the environment object must be done through the + * {@code ConfigurableEnvironment} interface, returned from all + * {@code AbstractApplicationContext} subclass {@code getEnvironment()} methods. See + * {@link ConfigurableEnvironment} Javadoc for usage examples demonstrating manipulation + * of property sources prior to application context {@code refresh()}. + * + * @author Chris Beams + * @since 3.1 + * @see PropertyResolver + * @see EnvironmentCapable + * @see ConfigurableEnvironment + * @see AbstractEnvironment + * @see StandardEnvironment + * @see org.springframework.context.EnvironmentAware + * @see org.springframework.context.ConfigurableApplicationContext#getEnvironment + * @see org.springframework.context.ConfigurableApplicationContext#setEnvironment + * @see org.springframework.context.support.AbstractApplicationContext#createEnvironment + */ +public interface Environment extends PropertyResolver { + + /** + * Return the set of profiles explicitly made active for this environment. Profiles + * are used for creating logical groupings of bean definitions to be registered + * conditionally, for example based on deployment environment. Profiles can be + * activated by setting {@linkplain AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME + * "spring.profiles.active"} as a system property or by calling + * {@link ConfigurableEnvironment#setActiveProfiles(String...)}. + * <p>If no profiles have explicitly been specified as active, then any {@linkplain + * #getDefaultProfiles() default profiles} will automatically be activated. + * @see #getDefaultProfiles + * @see ConfigurableEnvironment#setActiveProfiles + * @see AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME + */ + String[] getActiveProfiles(); + + /** + * Return the set of profiles to be active by default when no active profiles have + * been set explicitly. + * @see #getActiveProfiles + * @see ConfigurableEnvironment#setDefaultProfiles + * @see AbstractEnvironment#DEFAULT_PROFILES_PROPERTY_NAME + */ + String[] getDefaultProfiles(); + + /** + * Return whether one or more of the given profiles is active or, in the case of no + * explicit active profiles, whether one or more of the given profiles is included in + * the set of default profiles. If a profile begins with '!' the logic is inverted, + * i.e. the method will return true if the given profile is <em>not</em> active. + * For example, <pre class="code">env.acceptsProfiles("p1", "!p2")</pre> will + * return {@code true} if profile 'p1' is active or 'p2' is not active. + * @throws IllegalArgumentException if called with zero arguments + * or if any profile is {@code null}, empty or whitespace-only + * @see #getActiveProfiles + * @see #getDefaultProfiles + */ + boolean acceptsProfiles(String... profiles); + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/EnvironmentCapable.java b/spring-core/src/main/java/org/springframework/core/env/EnvironmentCapable.java new file mode 100644 index 00000000..7d6ca387 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/EnvironmentCapable.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2013 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.env; + +/** + * Interface indicating a component that contains and exposes an {@link Environment} reference. + * + * <p>All Spring application contexts are EnvironmentCapable, and the interface is used primarily + * for performing {@code instanceof} checks in framework methods that accept BeanFactory + * instances that may or may not actually be ApplicationContext instances in order to interact + * with the environment if indeed it is available. + * + * <p>As mentioned, {@link org.springframework.context.ApplicationContext ApplicationContext} + * extends EnvironmentCapable, and thus exposes a {@link #getEnvironment()} method; however, + * {@link org.springframework.context.ConfigurableApplicationContext ConfigurableApplicationContext} + * redefines {@link org.springframework.context.ConfigurableApplicationContext#getEnvironment + * getEnvironment()} and narrows the signature to return a {@link ConfigurableEnvironment}. + * The effect is that an Environment object is 'read-only' until it is being accessed from + * a ConfigurableApplicationContext, at which point it too may be configured. + * + * @author Chris Beams + * @since 3.1 + * @see Environment + * @see ConfigurableEnvironment + * @see org.springframework.context.ConfigurableApplicationContext#getEnvironment() + */ +public interface EnvironmentCapable { + + /** + * Return the {@link Environment} associated with this component. + */ + Environment getEnvironment(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java new file mode 100644 index 00000000..61ed06a6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2014 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.env; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import joptsimple.OptionSet; + +import org.springframework.util.Assert; + +/** + * {@link CommandLinePropertySource} implementation backed by a JOpt {@link OptionSet}. + * + * <h2>Typical usage</h2> + * Configure and execute an {@code OptionParser} against the {@code String[]} of arguments + * supplied to the {@code main} method, and create a {@link JOptCommandLinePropertySource} + * using the resulting {@code OptionSet} object: + * <pre class="code"> + * public static void main(String[] args) { + * OptionParser parser = new OptionParser(); + * parser.accepts("option1"); + * parser.accepts("option2").withRequiredArg(); + * OptionSet options = parser.parse(args); + * PropertySource<?> ps = new JOptCommandLinePropertySource(options); + * // ... + * }</pre> + * + * See {@link CommandLinePropertySource} for complete general usage examples. + * + * <p>Requires JOpt version 3.0 or higher. Tested against JOpt up until 4.6. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see CommandLinePropertySource + * @see joptsimple.OptionParser + * @see joptsimple.OptionSet + */ +public class JOptCommandLinePropertySource extends CommandLinePropertySource<OptionSet> { + + /** + * Create a new {@code JOptCommandLinePropertySource} having the default name + * and backed by the given {@code OptionSet}. + * @see CommandLinePropertySource#COMMAND_LINE_PROPERTY_SOURCE_NAME + * @see CommandLinePropertySource#CommandLinePropertySource(Object) + */ + public JOptCommandLinePropertySource(OptionSet options) { + super(options); + } + + /** + * Create a new {@code JOptCommandLinePropertySource} having the given name + * and backed by the given {@code OptionSet}. + */ + public JOptCommandLinePropertySource(String name, OptionSet options) { + super(name, options); + } + + + @Override + protected boolean containsOption(String name) { + return this.source.has(name); + } + + @Override + public List<String> getOptionValues(String name) { + List<?> argValues = this.source.valuesOf(name); + List<String> stringArgValues = new ArrayList<String>(); + for (Object argValue : argValues) { + Assert.isInstanceOf(String.class, argValue, "Argument values must be of type String"); + stringArgValues.add((String) argValue); + } + if (stringArgValues.isEmpty()) { + return (this.source.has(name) ? Collections.<String>emptyList() : null); + } + return Collections.unmodifiableList(stringArgValues); + } + + @Override + protected List<String> getNonOptionArgs() { + List<?> argValues = this.source.nonOptionArguments(); + List<String> stringArgValues = new ArrayList<String>(); + for (Object argValue : argValues) { + Assert.isInstanceOf(String.class, argValue, "Argument values must be of type String"); + stringArgValues.add((String) argValue); + } + return (stringArgValues.isEmpty() ? Collections.<String>emptyList() : + Collections.unmodifiableList(stringArgValues)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java new file mode 100644 index 00000000..3bcd2fed --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2014 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.env; + +import java.util.Map; + +import org.springframework.util.StringUtils; + +/** + * {@link PropertySource} that reads keys and values from a {@code Map} object. + * + * @author Chris Beams + * @since 3.1 + * @see PropertiesPropertySource + */ +public class MapPropertySource extends EnumerablePropertySource<Map<String, Object>> { + + public MapPropertySource(String name, Map<String, Object> source) { + super(name, source); + } + + + @Override + public Object getProperty(String name) { + return this.source.get(name); + } + + @Override + public String[] getPropertyNames() { + return StringUtils.toStringArray(this.source.keySet()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/MissingRequiredPropertiesException.java b/spring-core/src/main/java/org/springframework/core/env/MissingRequiredPropertiesException.java new file mode 100644 index 00000000..8c4d338e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/MissingRequiredPropertiesException.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2011 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.env; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Exception thrown when required properties are not found. + * + * @author Chris Beams + * @since 3.1 + * @see ConfigurablePropertyResolver#setRequiredProperties(String...) + * @see ConfigurablePropertyResolver#validateRequiredProperties() + * @see org.springframework.context.support.AbstractApplicationContext#prepareRefresh() + */ +@SuppressWarnings("serial") +public class MissingRequiredPropertiesException extends IllegalStateException { + + private final Set<String> missingRequiredProperties = new LinkedHashSet<String>(); + + /** + * Return the set of properties marked as required but not present + * upon validation. + * @see ConfigurablePropertyResolver#setRequiredProperties(String...) + * @see ConfigurablePropertyResolver#validateRequiredProperties() + */ + public Set<String> getMissingRequiredProperties() { + return missingRequiredProperties; + } + + void addMissingRequiredProperty(String key) { + missingRequiredProperties.add(key); + } + + @Override + public String getMessage() { + return String.format( + "The following properties were declared as required but could " + + "not be resolved: %s", this.getMissingRequiredProperties()); + } +} diff --git a/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java b/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java new file mode 100644 index 00000000..507704bd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java @@ -0,0 +1,234 @@ +/* + * Copyright 2002-2014 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.env; + +import java.util.Iterator; +import java.util.LinkedList; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Default implementation of the {@link PropertySources} interface. + * Allows manipulation of contained property sources and provides a constructor + * for copying an existing {@code PropertySources} instance. + * + * <p>Where <em>precedence</em> is mentioned in methods such as {@link #addFirst} + * and {@link #addLast}, this is with regard to the order in which property sources + * will be searched when resolving a given property with a {@link PropertyResolver}. + * + * @author Chris Beams + * @since 3.1 + * @see PropertySourcesPropertyResolver + */ +public class MutablePropertySources implements PropertySources { + + static final String NON_EXISTENT_PROPERTY_SOURCE_MESSAGE = "PropertySource named [%s] does not exist"; + static final String ILLEGAL_RELATIVE_ADDITION_MESSAGE = "PropertySource named [%s] cannot be added relative to itself"; + + private final Log logger; + + private final LinkedList<PropertySource<?>> propertySourceList = new LinkedList<PropertySource<?>>(); + + + /** + * Create a new {@link MutablePropertySources} object. + */ + public MutablePropertySources() { + this.logger = LogFactory.getLog(this.getClass()); + } + + /** + * Create a new {@code MutablePropertySources} from the given propertySources + * object, preserving the original order of contained {@code PropertySource} objects. + */ + public MutablePropertySources(PropertySources propertySources) { + this(); + for (PropertySource<?> propertySource : propertySources) { + this.addLast(propertySource); + } + } + + /** + * Create a new {@link MutablePropertySources} object and inherit the given logger, + * usually from an enclosing {@link Environment}. + */ + MutablePropertySources(Log logger) { + this.logger = logger; + } + + + public boolean contains(String name) { + return this.propertySourceList.contains(PropertySource.named(name)); + } + + public PropertySource<?> get(String name) { + int index = this.propertySourceList.indexOf(PropertySource.named(name)); + return index == -1 ? null : this.propertySourceList.get(index); + } + + public Iterator<PropertySource<?>> iterator() { + return this.propertySourceList.iterator(); + } + + /** + * Add the given property source object with highest precedence. + */ + public void addFirst(PropertySource<?> propertySource) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Adding [%s] PropertySource with highest search precedence", + propertySource.getName())); + } + removeIfPresent(propertySource); + this.propertySourceList.addFirst(propertySource); + } + + /** + * Add the given property source object with lowest precedence. + */ + public void addLast(PropertySource<?> propertySource) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Adding [%s] PropertySource with lowest search precedence", + propertySource.getName())); + } + removeIfPresent(propertySource); + this.propertySourceList.addLast(propertySource); + } + + /** + * Add the given property source object with precedence immediately higher + * than the named relative property source. + */ + public void addBefore(String relativePropertySourceName, PropertySource<?> propertySource) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Adding [%s] PropertySource with search precedence immediately higher than [%s]", + propertySource.getName(), relativePropertySourceName)); + } + assertLegalRelativeAddition(relativePropertySourceName, propertySource); + removeIfPresent(propertySource); + int index = assertPresentAndGetIndex(relativePropertySourceName); + addAtIndex(index, propertySource); + } + + /** + * Add the given property source object with precedence immediately lower + * than the named relative property source. + */ + public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Adding [%s] PropertySource with search precedence immediately lower than [%s]", + propertySource.getName(), relativePropertySourceName)); + } + assertLegalRelativeAddition(relativePropertySourceName, propertySource); + removeIfPresent(propertySource); + int index = assertPresentAndGetIndex(relativePropertySourceName); + addAtIndex(index + 1, propertySource); + } + + /** + * Return the precedence of the given property source, {@code -1} if not found. + */ + public int precedenceOf(PropertySource<?> propertySource) { + return this.propertySourceList.indexOf(propertySource); + } + + /** + * Remove and return the property source with the given name, {@code null} if not found. + * @param name the name of the property source to find and remove + */ + public PropertySource<?> remove(String name) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Removing [%s] PropertySource", name)); + } + int index = this.propertySourceList.indexOf(PropertySource.named(name)); + return index == -1 ? null : this.propertySourceList.remove(index); + } + + /** + * Replace the property source with the given name with the given property source object. + * @param name the name of the property source to find and replace + * @param propertySource the replacement property source + * @throws IllegalArgumentException if no property source with the given name is present + * @see #contains + */ + public void replace(String name, PropertySource<?> propertySource) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Replacing [%s] PropertySource with [%s]", + name, propertySource.getName())); + } + int index = assertPresentAndGetIndex(name); + this.propertySourceList.set(index, propertySource); + } + + /** + * Return the number of {@link PropertySource} objects contained. + */ + public int size() { + return this.propertySourceList.size(); + } + + @Override + public String toString() { + String[] names = new String[this.size()]; + for (int i=0; i < size(); i++) { + names[i] = this.propertySourceList.get(i).getName(); + } + return String.format("[%s]", StringUtils.arrayToCommaDelimitedString(names)); + } + + /** + * Ensure that the given property source is not being added relative to itself. + */ + protected void assertLegalRelativeAddition(String relativePropertySourceName, PropertySource<?> propertySource) { + String newPropertySourceName = propertySource.getName(); + Assert.isTrue(!relativePropertySourceName.equals(newPropertySourceName), + String.format(ILLEGAL_RELATIVE_ADDITION_MESSAGE, newPropertySourceName)); + } + + /** + * Remove the given property source if it is present. + */ + protected void removeIfPresent(PropertySource<?> propertySource) { + if (this.propertySourceList.contains(propertySource)) { + this.propertySourceList.remove(propertySource); + } + } + + /** + * Add the given property source at a particular index in the list. + */ + private void addAtIndex(int index, PropertySource<?> propertySource) { + removeIfPresent(propertySource); + this.propertySourceList.add(index, propertySource); + } + + /** + * Assert that the named property source is present and return its index. + * @param name the {@linkplain PropertySource#getName() name of the property source} + * to find + * @throws IllegalArgumentException if the named property source is not present + */ + private int assertPresentAndGetIndex(String name) { + int index = this.propertySourceList.indexOf(PropertySource.named(name)); + Assert.isTrue(index >= 0, String.format(NON_EXISTENT_PROPERTY_SOURCE_MESSAGE, name)); + return index; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java new file mode 100644 index 00000000..492d142d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2013 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.env; + +import java.util.Map; +import java.util.Properties; + +/** + * {@link PropertySource} implementation that extracts properties from a + * {@link java.util.Properties} object. + * + * <p>Note that because a {@code Properties} object is technically an + * {@code <Object, Object>} {@link java.util.Hashtable Hashtable}, one may contain + * non-{@code String} keys or values. This implementation, however is restricted to + * accessing only {@code String}-based keys and values, in the same fashion as + * {@link Properties#getProperty} and {@link Properties#setProperty}. + * + * @author Chris Beams + * @since 3.1 + */ +public class PropertiesPropertySource extends MapPropertySource { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public PropertiesPropertySource(String name, Properties source) { + super(name, (Map) source); + } + +} 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 new file mode 100644 index 00000000..b3ce15b6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2013 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.env; + +/** + * Interface for resolving properties against any underlying source. + * + * @author Chris Beams + * @since 3.1 + * @see Environment + * @see PropertySourcesPropertyResolver + */ +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}. + */ + boolean containsProperty(String key); + + /** + * 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) + * @see #getRequiredProperty(String) + */ + String getProperty(String key); + + /** + * 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 defaultValue the default value to return if no value is found + * @see #getRequiredProperty(String) + * @see #getProperty(String, Class) + */ + String getProperty(String key, String defaultValue); + + /** + * 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) + */ + <T> T getProperty(String key, Class<T> targetType); + + /** + * 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 + * @see #getRequiredProperty(String, Class) + */ + <T> T getProperty(String key, Class<T> targetType, T defaultValue); + + /** + * 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 + * from class specified by property value + * @see #getProperty(String, Class) + */ + <T> Class<T> getPropertyAsClass(String key, Class<T> targetType); + + /** + * Return the property value associated with the given key, converted to the given + * targetType (never {@code null}). + * @throws IllegalStateException if the key cannot be resolved + * @see #getRequiredProperty(String, Class) + */ + String getRequiredProperty(String key) throws IllegalStateException; + + /** + * Return the property value associated with the given key, converted to the given + * targetType (never {@code null}). + * @throws IllegalStateException if the given key cannot be resolved + */ + <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException; + + /** + * Resolve ${...} placeholders in the given text, replacing them with corresponding + * property values as resolved by {@link #getProperty}. Unresolvable placeholders with + * no default value are ignored and passed through unchanged. + * @param text the String to resolve + * @return the resolved String (never {@code null}) + * @throws IllegalArgumentException if given text is {@code null} + * @see #resolveRequiredPlaceholders + * @see org.springframework.util.SystemPropertyUtils#resolvePlaceholders(String) + */ + String resolvePlaceholders(String text); + + /** + * Resolve ${...} placeholders in the given text, replacing them with corresponding + * property values as resolved by {@link #getProperty}. Unresolvable placeholders with + * no default value will cause an IllegalArgumentException to be thrown. + * @return the resolved String (never {@code null}) + * @throws IllegalArgumentException if given text is {@code null} + * or if any placeholders are unresolvable + * @see org.springframework.util.SystemPropertyUtils#resolvePlaceholders(String, boolean) + */ + String resolveRequiredPlaceholders(String text) throws IllegalArgumentException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertySource.java b/spring-core/src/main/java/org/springframework/core/env/PropertySource.java new file mode 100644 index 00000000..3fbdd045 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/PropertySource.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2014 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.env; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Abstract base class representing a source of name/value property pairs. The underlying + * {@linkplain #getSource() source object} may be of any type {@code T} that encapsulates + * properties. Examples include {@link java.util.Properties} objects, {@link java.util.Map} + * objects, {@code ServletContext} and {@code ServletConfig} objects (for access to init + * parameters). Explore the {@code PropertySource} type hierarchy to see provided + * implementations. + * + * <p>{@code PropertySource} objects are not typically used in isolation, but rather + * through a {@link PropertySources} object, which aggregates property sources and in + * conjunction with a {@link PropertyResolver} implementation that can perform + * precedence-based searches across the set of {@code PropertySources}. + * + * <p>{@code PropertySource} identity is determined not based on the content of + * encapsulated properties, but rather based on the {@link #getName() name} of the + * {@code PropertySource} alone. This is useful for manipulating {@code PropertySource} + * objects when in collection contexts. See operations in {@link MutablePropertySources} + * as well as the {@link #named(String)} and {@link #toString()} methods for details. + * + * <p>Note that when working with @{@link + * org.springframework.context.annotation.Configuration Configuration} classes that + * the @{@link org.springframework.context.annotation.PropertySource PropertySource} + * annotation provides a convenient and declarative way of adding property sources to the + * enclosing {@code Environment}. + * + * @author Chris Beams + * @since 3.1 + * @see PropertySources + * @see PropertyResolver + * @see PropertySourcesPropertyResolver + * @see MutablePropertySources + * @see org.springframework.context.annotation.PropertySource + */ +public abstract class PropertySource<T> { + + protected final Log logger = LogFactory.getLog(getClass()); + + protected final String name; + + protected final T source; + + + /** + * Create a new {@code PropertySource} with the given name and source object. + */ + public PropertySource(String name, T source) { + Assert.hasText(name, "Property source name must contain at least one character"); + Assert.notNull(source, "Property source must not be null"); + this.name = name; + this.source = source; + } + + /** + * Create a new {@code PropertySource} with the given name and with a new {@code Object} + * instance as the underlying source. + * <p>Often useful in testing scenarios when creating anonymous implementations that + * never query an actual source but rather return hard-coded values. + */ + @SuppressWarnings("unchecked") + public PropertySource(String name) { + this(name, (T) new Object()); + } + + + /** + * Return the name of this {@code PropertySource} + */ + public String getName() { + return this.name; + } + + /** + * Return the underlying source object for this {@code PropertySource}. + */ + public T getSource() { + return this.source; + } + + /** + * Return whether this {@code PropertySource} contains the given name. + * <p>This implementation simply checks for a {@code null} return value + * from {@link #getProperty(String)}. Subclasses may wish to implement + * a more efficient algorithm if possible. + * @param name the property name to find + */ + public boolean containsProperty(String name) { + return (getProperty(name) != null); + } + + /** + * Return the value associated with the given name, + * or {@code null} if not found. + * @param name the property to find + * @see PropertyResolver#getRequiredProperty(String) + */ + public abstract Object getProperty(String name); + + + /** + * This {@code PropertySource} object is equal to the given object if: + * <ul> + * <li>they are the same instance + * <li>the {@code name} properties for both objects are equal + * </ul> + * <p>No properties other than {@code name} are evaluated. + */ + @Override + public boolean equals(Object obj) { + return (this == obj || (obj instanceof PropertySource && + ObjectUtils.nullSafeEquals(this.name, ((PropertySource<?>) obj).name))); + } + + /** + * Return a hash code derived from the {@code name} property + * of this {@code PropertySource} object. + */ + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.name); + } + + /** + * Produce concise output (type and name) if the current log level does not include + * debug. If debug is enabled, produce verbose output including the hash code of the + * PropertySource instance and every name/value property pair. + * <p>This variable verbosity is useful as a property source such as system properties + * or environment variables may contain an arbitrary number of property pairs, + * potentially leading to difficult to read exception and log messages. + * @see Log#isDebugEnabled() + */ + @Override + public String toString() { + if (logger.isDebugEnabled()) { + return String.format("%s@%s [name='%s', properties=%s]", + getClass().getSimpleName(), System.identityHashCode(this), this.name, this.source); + } + else { + return String.format("%s [name='%s']", getClass().getSimpleName(), this.name); + } + } + + + /** + * Return a {@code PropertySource} implementation intended for collection comparison purposes only. + * <p>Primarily for internal use, but given a collection of {@code PropertySource} objects, may be + * used as follows: + * <pre class="code"> + * {@code List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>(); + * sources.add(new MapPropertySource("sourceA", mapA)); + * sources.add(new MapPropertySource("sourceB", mapB)); + * assert sources.contains(PropertySource.named("sourceA")); + * assert sources.contains(PropertySource.named("sourceB")); + * assert !sources.contains(PropertySource.named("sourceC")); + * }</pre> + * The returned {@code PropertySource} will throw {@code UnsupportedOperationException} + * if any methods other than {@code equals(Object)}, {@code hashCode()}, and {@code toString()} + * are called. + * @param name the name of the comparison {@code PropertySource} to be created and returned. + */ + public static PropertySource<?> named(String name) { + return new ComparisonPropertySource(name); + } + + + /** + * {@code PropertySource} to be used as a placeholder in cases where an actual + * property source cannot be eagerly initialized at application context + * creation time. For example, a {@code ServletContext}-based property source + * must wait until the {@code ServletContext} object is available to its enclosing + * {@code ApplicationContext}. In such cases, a stub should be used to hold the + * intended default position/order of the property source, then be replaced + * during context refresh. + * @see org.springframework.context.support.AbstractApplicationContext#initPropertySources() + * @see org.springframework.web.context.support.StandardServletEnvironment + * @see org.springframework.web.context.support.ServletContextPropertySource + */ + public static class StubPropertySource extends PropertySource<Object> { + + public StubPropertySource(String name) { + super(name, new Object()); + } + + /** + * Always returns {@code null}. + */ + @Override + public String getProperty(String name) { + return null; + } + } + + + /** + * @see PropertySource#named(String) + */ + static class ComparisonPropertySource extends StubPropertySource { + + private static final String USAGE_ERROR = + "ComparisonPropertySource instances are for use with collection comparison only"; + + public ComparisonPropertySource(String name) { + super(name); + } + + @Override + public Object getSource() { + throw new UnsupportedOperationException(USAGE_ERROR); + } + + @Override + public boolean containsProperty(String name) { + throw new UnsupportedOperationException(USAGE_ERROR); + } + + @Override + public String getProperty(String name) { + throw new UnsupportedOperationException(USAGE_ERROR); + } + + @Override + public String toString() { + return String.format("%s [name='%s']", getClass().getSimpleName(), this.name); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertySources.java b/spring-core/src/main/java/org/springframework/core/env/PropertySources.java new file mode 100644 index 00000000..90f99594 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/PropertySources.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2011 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.env; + +/** + * Holder containing one or more {@link PropertySource} objects. + * + * @author Chris Beams + * @since 3.1 + */ +public interface PropertySources extends Iterable<PropertySource<?>> { + + /** + * Return whether a property source with the given name is contained. + * @param name the {@linkplain PropertySource#getName() name of the property source} to find + */ + boolean contains(String name); + + /** + * Return the property source with the given name, {@code null} if not found. + * @param name the {@linkplain PropertySource#getName() name of the property source} to find + */ + PropertySource<?> get(String name); + +} 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 new file mode 100644 index 00000000..9e336284 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/PropertySourcesPropertyResolver.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2013 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.env; + +import org.springframework.core.convert.ConversionException; +import org.springframework.util.ClassUtils; + +/** + * {@link PropertyResolver} implementation that resolves property values against + * an underlying set of {@link PropertySources}. + * + * @author Chris Beams + * @since 3.1 + * @see PropertySource + * @see PropertySources + * @see AbstractEnvironment + */ +public class PropertySourcesPropertyResolver extends AbstractPropertyResolver { + + private final PropertySources propertySources; + + + /** + * Create a new resolver against the given property sources. + * @param propertySources the set of {@link PropertySource} objects to use + */ + public PropertySourcesPropertyResolver(PropertySources propertySources) { + this.propertySources = propertySources; + } + + + @Override + public boolean containsProperty(String key) { + if (this.propertySources != null) { + for (PropertySource<?> propertySource : this.propertySources) { + if (propertySource.containsProperty(key)) { + return true; + } + } + } + return false; + } + + @Override + public String getProperty(String key) { + return getProperty(key, String.class, true); + } + + @Override + public <T> T getProperty(String key, Class<T> targetValueType) { + return getProperty(key, targetValueType, true); + } + + @Override + protected String getPropertyAsRawString(String key) { + return getProperty(key, String.class, false); + } + + protected <T> T getProperty(String key, Class<T> 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())); + } + Object value; + if ((value = propertySource.getProperty(key)) != 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())); + } + return this.conversionService.convert(value, targetValueType); + } + } + } + if (debugEnabled) { + logger.debug(String.format("Could not find key '%s' in any property source. Returning [null]", key)); + } + return null; + } + + @Override + public <T> Class<T> getPropertyAsClass(String key, Class<T> 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())); + } + 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)); + } + Class<?> clazz; + if (value instanceof String) { + try { + clazz = ClassUtils.forName((String) value, null); + } + catch (Exception ex) { + throw new ClassConversionException((String) value, targetValueType, ex); + } + } + else if (value instanceof Class) { + clazz = (Class<?>)value; + } + else { + clazz = value.getClass(); + } + if (!targetValueType.isAssignableFrom(clazz)) { + throw new ClassConversionException(clazz, targetValueType); + } + @SuppressWarnings("unchecked") + Class<T> targetClass = (Class<T>) clazz; + return targetClass; + } + } + } + if (debugEnabled) { + logger.debug(String.format("Could not find key '%s' in any property source. Returning [null]", key)); + } + return null; + } + + + @SuppressWarnings("serial") + 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())); + } + + 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); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/ReadOnlySystemAttributesMap.java b/spring-core/src/main/java/org/springframework/core/env/ReadOnlySystemAttributesMap.java new file mode 100644 index 00000000..2f770277 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/ReadOnlySystemAttributesMap.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2013 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.env; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import org.springframework.util.Assert; + +/** + * Read-only {@code Map<String, String>} implementation that is backed by system + * properties or environment variables. + * + * <p>Used by {@link AbstractApplicationContext} when a {@link SecurityManager} prohibits + * access to {@link System#getProperties()} or {@link System#getenv()}. It is for this + * reason that the implementations of {@link #keySet()}, {@link #entrySet()}, and + * {@link #values()} always return empty even though {@link #get(Object)} may in fact + * return non-null if the current security manager allows access to individual keys. + * + * @author Arjen Poutsma + * @author Chris Beams + * @since 3.0 + */ +abstract class ReadOnlySystemAttributesMap implements Map<String, String> { + + public boolean containsKey(Object key) { + return (get(key) != null); + } + + /** + * @param key the name of the system attribute to retrieve + * @throws IllegalArgumentException if given key is non-String + */ + public String get(Object key) { + Assert.isInstanceOf(String.class, key, + String.format("Expected key [%s] to be of type String, got %s", key, key.getClass().getName())); + return this.getSystemAttribute((String) key); + } + + public boolean isEmpty() { + return false; + } + + /** + * Template method that returns the underlying system attribute. + * <p>Implementations typically call {@link System#getProperty(String)} or {@link System#getenv(String)} here. + */ + protected abstract String getSystemAttribute(String attributeName); + + + // Unsupported + + public int size() { + throw new UnsupportedOperationException(); + } + + public String put(String key, String value) { + throw new UnsupportedOperationException(); + } + + public boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + public String remove(Object key) { + throw new UnsupportedOperationException(); + } + + public void clear() { + throw new UnsupportedOperationException(); + } + + public Set<String> keySet() { + return Collections.emptySet(); + } + + public void putAll(Map<? extends String, ? extends String> map) { + throw new UnsupportedOperationException(); + } + + public Collection<String> values() { + return Collections.emptySet(); + } + + public Set<Entry<String, String>> entrySet() { + return Collections.emptySet(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java new file mode 100644 index 00000000..b249faf0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2013 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.env; + +/** + * Parses a {@code String[]} of command line arguments in order to populate a + * {@link CommandLineArgs} object. + * + * <h3>Working with option arguments</h3> + * Option arguments must adhere to the exact syntax: + * <pre class="code">--optName[=optValue]</pre> + * That is, options must be prefixed with "{@code --}", and may or may not specify a value. + * If a value is specified, the name and value must be separated <em>without spaces</em> + * by an equals sign ("="). + * + * <h4>Valid examples of option arguments</h4> + * <pre class="code"> + * --foo + * --foo=bar + * --foo="bar then baz" + * --foo=bar,baz,biz</pre> + * + * <h4>Invalid examples of option arguments</h4> + * <pre class="code"> + * -foo + * --foo bar + * --foo = bar + * --foo=bar --foo=baz --foo=biz</pre> + * + * <h3>Working with non-option arguments</h3> + * Any and all arguments specified at the command line without the "{@code --}" option + * prefix will be considered as "non-option arguments" and made available through the + * {@link CommandLineArgs#getNonOptionArgs()} method. + * + * @author Chris Beams + * @since 3.1 + */ +class SimpleCommandLineArgsParser { + + /** + * Parse the given {@code String} array based on the rules described + * {@linkplain SimpleCommandLineArgsParser above}, returning a + * fully-populated {@link CommandLineArgs} object. + * @param args command line arguments, typically from a {@code main()} method + */ + public CommandLineArgs parse(String... args) { + CommandLineArgs commandLineArgs = new CommandLineArgs(); + for (String arg : args) { + if (arg.startsWith("--")) { + String optionText = arg.substring(2, arg.length()); + String optionName; + String optionValue = null; + if (optionText.contains("=")) { + optionName = optionText.substring(0, optionText.indexOf('=')); + optionValue = optionText.substring(optionText.indexOf('=') + 1, optionText.length()); + } + else { + optionName = optionText; + } + if (optionName.length() == 0 || (optionValue != null && optionValue.length() == 0)) { + throw new IllegalArgumentException("Invalid argument syntax: " + arg); + } + commandLineArgs.addOptionArg(optionName, optionValue); + } + else { + commandLineArgs.addNonOptionArg(arg); + } + } + return commandLineArgs; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java new file mode 100644 index 00000000..d2022317 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2011 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.env; + +import java.util.List; + +/** + * {@link CommandLinePropertySource} implementation backed by a simple String array. + * + * <h3>Purpose</h3> + * This {@code CommandLinePropertySource} implementation aims to provide the simplest + * possible approach to parsing command line arguments. As with all {@code + * CommandLinePropertySource} implementations, command line arguments are broken into two + * distinct groups: <em>option arguments</em> and <em>non-option arguments</em>, as + * described below <em>(some sections copied from Javadoc for {@link SimpleCommandLineArgsParser})</em>: + * + * <h3>Working with option arguments</h3> + * Option arguments must adhere to the exact syntax: + * <pre class="code">--optName[=optValue]</pre> + * That is, options must be prefixed with "{@code --}", and may or may not specify a value. + * If a value is specified, the name and value must be separated <em>without spaces</em> + * by an equals sign ("="). + * + * <h4>Valid examples of option arguments</h4> + * <pre class="code"> + * --foo + * --foo=bar + * --foo="bar then baz" + * --foo=bar,baz,biz</pre> + * + * <h4>Invalid examples of option arguments</h4> + * <pre class="code"> + * -foo + * --foo bar + * --foo = bar + * --foo=bar --foo=baz --foo=biz</pre> + * + * <h3>Working with non-option arguments</h3> + * Any and all arguments specified at the command line without the "{@code --}" option + * prefix will be considered as "non-option arguments" and made available through the + * {@link #getNonOptionArgs()} method. + * + * <h2>Typical usage</h2> + * <pre class="code"> + * public static void main(String[] args) { + * PropertySource<?> ps = new SimpleCommandLinePropertySource(args); + * // ... + * }</pre> + * + * See {@link CommandLinePropertySource} for complete general usage examples. + * + * <h3>Beyond the basics</h3> + * + * <p>When more fully-featured command line parsing is necessary, consider using + * the provided {@link JOptCommandLinePropertySource}, or implement your own + * {@code CommandLinePropertySource} against the command line parsing library of your + * choice! + * + * @author Chris Beams + * @since 3.1 + * @see CommandLinePropertySource + * @see JOptCommandLinePropertySource + */ +public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> { + + /** + * Create a new {@code SimpleCommandLinePropertySource} having the default name + * and backed by the given {@code String[]} of command line arguments. + * @see CommandLinePropertySource#COMMAND_LINE_PROPERTY_SOURCE_NAME + * @see CommandLinePropertySource#CommandLinePropertySource(Object) + */ + public SimpleCommandLinePropertySource(String... args) { + super(new SimpleCommandLineArgsParser().parse(args)); + } + + /** + * Create a new {@code SimpleCommandLinePropertySource} having the given name + * and backed by the given {@code String[]} of command line arguments. + */ + public SimpleCommandLinePropertySource(String name, String[] args) { + super(name, new SimpleCommandLineArgsParser().parse(args)); + } + + @Override + protected boolean containsOption(String name) { + return this.source.containsOption(name); + } + + @Override + protected List<String> getOptionValues(String name) { + return this.source.getOptionValues(name); + } + + @Override + protected List<String> getNonOptionArgs() { + return this.source.getNonOptionArgs(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/StandardEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/StandardEnvironment.java new file mode 100644 index 00000000..9437134f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/StandardEnvironment.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2013 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.env; + +/** + * {@link Environment} implementation suitable for use in 'standard' (i.e. non-web) + * applications. + * + * <p>In addition to the usual functions of a {@link ConfigurableEnvironment} such as + * property resolution and profile-related operations, this implementation configures two + * default property sources, to be searched in the following order: + * <ul> + * <li>{@linkplain AbstractEnvironment#getSystemProperties() system properties} + * <li>{@linkplain AbstractEnvironment#getSystemEnvironment() system environment variables} + * </ul> + * + * That is, if the key "xyz" is present both in the JVM system properties as well as in + * the set of environment variables for the current process, the value of key "xyz" from + * system properties will return from a call to {@code environment.getProperty("xyz")}. + * This ordering is chosen by default because system properties are per-JVM, while + * environment variables may be the same across many JVMs on a given system. Giving + * system properties precedence allows for overriding of environment variables on a + * per-JVM basis. + * + * <p>These default property sources may be removed, reordered, or replaced; and + * additional property sources may be added using the {@link MutablePropertySources} + * instance available from {@link #getPropertySources()}. See + * {@link ConfigurableEnvironment} Javadoc for usage examples. + * + * <p>See {@link SystemEnvironmentPropertySource} javadoc for details on special handling + * of property names in shell environments (e.g. Bash) that disallow period characters in + * variable names. + * + * @author Chris Beams + * @since 3.1 + * @see ConfigurableEnvironment + * @see SystemEnvironmentPropertySource + * @see org.springframework.web.context.support.StandardServletEnvironment + */ +public class StandardEnvironment extends AbstractEnvironment { + + /** System environment property source name: {@value} */ + public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment"; + + /** JVM system properties property source name: {@value} */ + public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties"; + + + /** + * Customize the set of property sources with those appropriate for any standard + * Java environment: + * <ul> + * <li>{@value #SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME} + * <li>{@value #SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME} + * </ul> + * <p>Properties present in {@value #SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME} will + * take precedence over those in {@value #SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME}. + * @see AbstractEnvironment#customizePropertySources(MutablePropertySources) + * @see #getSystemProperties() + * @see #getSystemEnvironment() + */ + @Override + protected void customizePropertySources(MutablePropertySources propertySources) { + propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties())); + propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment())); + } + +} 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 new file mode 100644 index 00000000..b0ca4dc3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/SystemEnvironmentPropertySource.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2014 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.env; + +import java.util.Map; + +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. + * + * <p>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: + * <ul> + * <li>{@code foo.bar} - the original name</li> + * <li>{@code foo_bar} - with underscores for periods (if any)</li> + * <li>{@code FOO.BAR} - original, with upper case</li> + * <li>{@code FOO_BAR} - with underscores and upper case</li> + * </ul> + * + * The same applies for calls to {@link #containsProperty(String)}, which returns + * {@code true} if any of the above properties are present, otherwise {@code false}. + * + * <p>This feature is particularly useful when specifying active or default profiles as + * environment variables. The following is not allowable under Bash: + * + * <pre class="code">spring.profiles.active=p1 java -classpath ... MyApp</pre> + * + * However, the following syntax is permitted and is also more conventional: + * + * <pre class="code">SPRING_PROFILES_ACTIVE=p1 java -classpath ... MyApp</pre> + * + * <p>Enable debug- or trace-level logging for this class (or package) for messages + * explaining when these 'property name resolutions' occur. + * + * <p>This property source is included by default in {@link StandardEnvironment} + * and all its subclasses. + * + * @author Chris Beams + * @since 3.1 + * @see StandardEnvironment + * @see AbstractEnvironment#getSystemEnvironment() + * @see AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME + */ +public class SystemEnvironmentPropertySource extends MapPropertySource { + + /** + * Create a new {@code SystemEnvironmentPropertySource} with the given name and + * delegating to the given {@code MapPropertySource}. + */ + public SystemEnvironmentPropertySource(String name, Map<String, Object> source) { + super(name, source); + } + + + /** + * Return {@code true} if a property with the given name or any underscore/uppercase variant + * thereof exists in this property source. + */ + @Override + public boolean containsProperty(String name) { + return (getProperty(name) != null); + } + + /** + * This implementation returns {@code true} if a property with the given name or + * any underscore/uppercase variant thereof exists in this property source. + */ + @Override + public Object getProperty(String name) { + String actualName = resolvePropertyName(name); + if (logger.isDebugEnabled() && !name.equals(actualName)) { + logger.debug(String.format("PropertySource [%s] does not contain '%s', but found equivalent '%s'", + getName(), name, actualName)); + } + return super.getProperty(actualName); + } + + /** + * Check to see if this property source contains a property with the given name, or + * any underscore / uppercase variation thereof. Return the resolved name if one is + * found or otherwise the original name. Never returns {@code null}. + */ + private String resolvePropertyName(String name) { + Assert.notNull(name, "Property name must not be null"); + if (super.containsProperty(name)) { + return name; + } + + String usName = name.replace('.', '_'); + if (!name.equals(usName) && super.containsProperty(usName)) { + return usName; + } + + String ucName = name.toUpperCase(); + if (!name.equals(ucName)) { + if (super.containsProperty(ucName)) { + return ucName; + } + else { + String usUcName = ucName.replace('.', '_'); + if (!ucName.equals(usUcName) && super.containsProperty(usUcName)) { + return usUcName; + } + } + } + + return name; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/package-info.java b/spring-core/src/main/java/org/springframework/core/env/package-info.java new file mode 100644 index 00000000..bfaccbfd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/package-info.java @@ -0,0 +1,7 @@ +/** + * Spring's environment abstraction consisting of bean definition + * profile and hierarchical property source support. + * @author Chris Beams + * @since 3.1 + */ +package org.springframework.core.env; 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 new file mode 100644 index 00000000..c58ea9ab --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java @@ -0,0 +1,222 @@ +/* + * Copyright 2002-2013 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; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; + +import org.springframework.util.ResourceUtils; + +/** + * Abstract base class for resources which resolve URLs into File references, + * such as {@link UrlResource} or {@link ClassPathResource}. + * + * <p>Detects the "file" protocol as well as the JBoss "vfs" protocol in URLs, + * resolving file system references accordingly. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public abstract class AbstractFileResolvingResource extends AbstractResource { + + /** + * This implementation returns a File reference for the underlying class path + * resource, provided that it refers to a file in the file system. + * @see org.springframework.util.ResourceUtils#getFile(java.net.URL, String) + */ + @Override + public File getFile() throws IOException { + URL url = getURL(); + if (url.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { + return VfsResourceDelegate.getResource(url).getFile(); + } + return ResourceUtils.getFile(url, getDescription()); + } + + /** + * This implementation determines the underlying File + * (or jar file, in case of a resource in a jar/zip). + */ + @Override + protected File getFileForLastModifiedCheck() throws IOException { + URL url = getURL(); + if (ResourceUtils.isJarURL(url)) { + URL actualUrl = ResourceUtils.extractJarFileURL(url); + if (actualUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { + return VfsResourceDelegate.getResource(actualUrl).getFile(); + } + return ResourceUtils.getFile(actualUrl, "Jar URL"); + } + else { + return getFile(); + } + } + + /** + * This implementation returns a File reference for the underlying class path + * resource, provided that it refers to a file in the file system. + * @see org.springframework.util.ResourceUtils#getFile(java.net.URI, String) + */ + protected File getFile(URI uri) throws IOException { + if (uri.getScheme().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { + return VfsResourceDelegate.getResource(uri).getFile(); + } + return ResourceUtils.getFile(uri, getDescription()); + } + + + @Override + public boolean exists() { + try { + URL url = getURL(); + if (ResourceUtils.isFileURL(url)) { + // Proceed with file system resolution... + return getFile().exists(); + } + else { + // Try a URL connection content-length header... + URLConnection con = url.openConnection(); + customizeConnection(con); + HttpURLConnection httpCon = + (con instanceof HttpURLConnection ? (HttpURLConnection) con : null); + if (httpCon != null) { + int code = httpCon.getResponseCode(); + if (code == HttpURLConnection.HTTP_OK) { + return true; + } + else if (code == HttpURLConnection.HTTP_NOT_FOUND) { + return false; + } + } + if (con.getContentLength() >= 0) { + return true; + } + if (httpCon != null) { + // no HTTP OK status, and no content-length header: give up + httpCon.disconnect(); + return false; + } + else { + // Fall back to stream existence: can we open the stream? + InputStream is = getInputStream(); + is.close(); + return true; + } + } + } + catch (IOException ex) { + return false; + } + } + + @Override + public boolean isReadable() { + try { + URL url = getURL(); + if (ResourceUtils.isFileURL(url)) { + // Proceed with file system resolution... + File file = getFile(); + return (file.canRead() && !file.isDirectory()); + } + else { + return true; + } + } + catch (IOException ex) { + return false; + } + } + + @Override + public long contentLength() throws IOException { + URL url = getURL(); + if (ResourceUtils.isFileURL(url)) { + // Proceed with file system resolution... + return getFile().length(); + } + else { + // Try a URL connection content-length header... + URLConnection con = url.openConnection(); + customizeConnection(con); + return con.getContentLength(); + } + } + + @Override + public long lastModified() throws IOException { + URL url = getURL(); + if (ResourceUtils.isFileURL(url) || ResourceUtils.isJarURL(url)) { + // Proceed with file system resolution... + return super.lastModified(); + } + else { + // Try a URL connection last-modified header... + URLConnection con = url.openConnection(); + customizeConnection(con); + return con.getLastModified(); + } + } + + + /** + * Customize the given {@link URLConnection}, obtained in the course of an + * {@link #exists()}, {@link #contentLength()} or {@link #lastModified()} call. + * <p>Calls {@link ResourceUtils#useCachesIfNecessary(URLConnection)} and + * delegates to {@link #customizeConnection(HttpURLConnection)} if possible. + * Can be overridden in subclasses. + * @param con the URLConnection to customize + * @throws IOException if thrown from URLConnection methods + */ + protected void customizeConnection(URLConnection con) throws IOException { + ResourceUtils.useCachesIfNecessary(con); + if (con instanceof HttpURLConnection) { + customizeConnection((HttpURLConnection) con); + } + } + + /** + * Customize the given {@link HttpURLConnection}, obtained in the course of an + * {@link #exists()}, {@link #contentLength()} or {@link #lastModified()} call. + * <p>Sets request method "HEAD" by default. Can be overridden in subclasses. + * @param con the HttpURLConnection to customize + * @throws IOException if thrown from HttpURLConnection methods + */ + protected void customizeConnection(HttpURLConnection con) throws IOException { + con.setRequestMethod("HEAD"); + } + + + /** + * Inner delegate class, avoiding a hard JBoss VFS API dependency at runtime. + */ + private static class VfsResourceDelegate { + + public static Resource getResource(URL url) throws IOException { + return new VfsResource(VfsUtils.getRoot(url)); + } + + public static Resource getResource(URI uri) throws IOException { + return new VfsResource(VfsUtils.getRoot(uri)); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/AbstractResource.java b/spring-core/src/main/java/org/springframework/core/io/AbstractResource.java new file mode 100644 index 00000000..91ea1288 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/AbstractResource.java @@ -0,0 +1,209 @@ +/* + * Copyright 2002-2014 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; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import org.springframework.core.NestedIOException; +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; + +/** + * Convenience base class for {@link Resource} implementations, + * pre-implementing typical behavior. + * + * <p>The "exists" method will check whether a File or InputStream can + * be opened; "isOpen" will always return false; "getURL" and "getFile" + * throw an exception; and "toString" will return the description. + * + * @author Juergen Hoeller + * @since 28.12.2003 + */ +public abstract class AbstractResource implements Resource { + + /** + * This implementation checks whether a File can be opened, + * falling back to whether an InputStream can be opened. + * This will cover both directories and content resources. + */ + public boolean exists() { + // Try file existence: can we find the file in the file system? + try { + return getFile().exists(); + } + catch (IOException ex) { + // Fall back to stream existence: can we open the stream? + try { + InputStream is = getInputStream(); + is.close(); + return true; + } + catch (Throwable isEx) { + return false; + } + } + } + + /** + * This implementation always returns {@code true}. + */ + public boolean isReadable() { + return true; + } + + /** + * This implementation always returns {@code false}. + */ + public boolean isOpen() { + return false; + } + + /** + * This implementation throws a FileNotFoundException, assuming + * that the resource cannot be resolved to a URL. + */ + public URL getURL() throws IOException { + throw new FileNotFoundException(getDescription() + " cannot be resolved to URL"); + } + + /** + * This implementation builds a URI based on the URL returned + * by {@link #getURL()}. + */ + public URI getURI() throws IOException { + URL url = getURL(); + try { + return ResourceUtils.toURI(url); + } + catch (URISyntaxException ex) { + throw new NestedIOException("Invalid URI [" + url + "]", ex); + } + } + + /** + * This implementation throws a FileNotFoundException, assuming + * that the resource cannot be resolved to an absolute file path. + */ + public File getFile() throws IOException { + throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path"); + } + + /** + * This implementation reads the entire InputStream to calculate the + * content length. Subclasses will almost always be able to provide + * a more optimal version of this, e.g. checking a File length. + * @see #getInputStream() + * @throws IllegalStateException if {@link #getInputStream()} returns null. + */ + public long contentLength() throws IOException { + InputStream is = this.getInputStream(); + Assert.state(is != null, "resource input stream must not be null"); + try { + long size = 0; + byte[] buf = new byte[255]; + int read; + while ((read = is.read(buf)) != -1) { + size += read; + } + return size; + } + finally { + try { + is.close(); + } + catch (IOException ex) { + } + } + } + + /** + * This implementation checks the timestamp of the underlying File, + * if available. + * @see #getFileForLastModifiedCheck() + */ + public long lastModified() throws IOException { + long lastModified = getFileForLastModifiedCheck().lastModified(); + if (lastModified == 0L) { + throw new FileNotFoundException(getDescription() + + " cannot be resolved in the file system for resolving its last-modified timestamp"); + } + return lastModified; + } + + /** + * Determine the File to use for timestamp checking. + * <p>The default implementation delegates to {@link #getFile()}. + * @return the File to use for timestamp checking (never {@code null}) + * @throws IOException if the resource cannot be resolved as absolute + * file path, i.e. if the resource is not available in a file system + */ + protected File getFileForLastModifiedCheck() throws IOException { + return getFile(); + } + + /** + * This implementation throws a FileNotFoundException, assuming + * that relative resources cannot be created for this resource. + */ + public Resource createRelative(String relativePath) throws IOException { + throw new FileNotFoundException("Cannot create a relative resource for " + getDescription()); + } + + /** + * This implementation always returns {@code null}, + * assuming that this resource type does not have a filename. + */ + public String getFilename() { + return null; + } + + + /** + * This implementation returns the description of this resource. + * @see #getDescription() + */ + @Override + public String toString() { + return getDescription(); + } + + /** + * This implementation compares description strings. + * @see #getDescription() + */ + @Override + public boolean equals(Object obj) { + return (obj == this || + (obj instanceof Resource && ((Resource) obj).getDescription().equals(getDescription()))); + } + + /** + * This implementation returns the description's hash code. + * @see #getDescription() + */ + @Override + public int hashCode() { + return getDescription().hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ByteArrayResource.java b/spring-core/src/main/java/org/springframework/core/io/ByteArrayResource.java new file mode 100644 index 00000000..d195def9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ByteArrayResource.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2012 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; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +/** + * {@link Resource} implementation for a given byte array. + * Creates a ByteArrayInputStreams for the given byte array. + * + * <p>Useful for loading content from any given byte array, + * without having to resort to a single-use {@link InputStreamResource}. + * Particularly useful for creating mail attachments from local content, + * where JavaMail needs to be able to read the stream multiple times. + * + * @author Juergen Hoeller + * @since 1.2.3 + * @see java.io.ByteArrayInputStream + * @see InputStreamResource + * @see org.springframework.mail.javamail.MimeMessageHelper#addAttachment(String, InputStreamSource) + */ +public class ByteArrayResource extends AbstractResource { + + private final byte[] byteArray; + + private final String description; + + + /** + * Create a new ByteArrayResource. + * @param byteArray the byte array to wrap + */ + public ByteArrayResource(byte[] byteArray) { + this(byteArray, "resource loaded from byte array"); + } + + /** + * Create a new ByteArrayResource. + * @param byteArray the byte array to wrap + * @param description where the byte array comes from + */ + public ByteArrayResource(byte[] byteArray, String description) { + if (byteArray == null) { + throw new IllegalArgumentException("Byte array must not be null"); + } + this.byteArray = byteArray; + this.description = (description != null ? description : ""); + } + + /** + * Return the underlying byte array. + */ + public final byte[] getByteArray() { + return this.byteArray; + } + + + /** + * This implementation always returns {@code true}. + */ + @Override + public boolean exists() { + return true; + } + + /** + * This implementation returns the length of the underlying byte array. + */ + @Override + public long contentLength() { + return this.byteArray.length; + } + + /** + * This implementation returns a ByteArrayInputStream for the + * underlying byte array. + * @see java.io.ByteArrayInputStream + */ + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(this.byteArray); + } + + /** + * This implementation returns the passed-in description, if any. + */ + public String getDescription() { + return this.description; + } + + + /** + * This implementation compares the underlying byte array. + * @see java.util.Arrays#equals(byte[], byte[]) + */ + @Override + public boolean equals(Object obj) { + return (obj == this || + (obj instanceof ByteArrayResource && Arrays.equals(((ByteArrayResource) obj).byteArray, this.byteArray))); + } + + /** + * This implementation returns the hash code based on the + * underlying byte array. + */ + @Override + public int hashCode() { + return (byte[].class.hashCode() * 29 * this.byteArray.length); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java b/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java new file mode 100644 index 00000000..b1d2998e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java @@ -0,0 +1,256 @@ +/* + * Copyright 2002-2014 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; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * {@link Resource} implementation for class path resources. + * Uses either a given ClassLoader or a given Class for loading resources. + * + * <p>Supports resolution as {@code java.io.File} if the class path + * resource resides in the file system, but not for resources in a JAR. + * Always supports resolution as URL. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 28.12.2003 + * @see ClassLoader#getResourceAsStream(String) + * @see Class#getResourceAsStream(String) + */ +public class ClassPathResource extends AbstractFileResolvingResource { + + private final String path; + + private ClassLoader classLoader; + + private Class<?> clazz; + + + /** + * Create a new {@code ClassPathResource} for {@code ClassLoader} usage. + * A leading slash will be removed, as the ClassLoader resource access + * methods will not accept it. + * <p>The thread context class loader will be used for + * loading the resource. + * @param path the absolute path within the class path + * @see java.lang.ClassLoader#getResourceAsStream(String) + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + */ + public ClassPathResource(String path) { + this(path, (ClassLoader) null); + } + + /** + * Create a new {@code ClassPathResource} for {@code ClassLoader} usage. + * A leading slash will be removed, as the ClassLoader resource access + * methods will not accept it. + * @param path the absolute path within the classpath + * @param classLoader the class loader to load the resource with, + * or {@code null} for the thread context class loader + * @see ClassLoader#getResourceAsStream(String) + */ + public ClassPathResource(String path, ClassLoader classLoader) { + Assert.notNull(path, "Path must not be null"); + String pathToUse = StringUtils.cleanPath(path); + if (pathToUse.startsWith("/")) { + pathToUse = pathToUse.substring(1); + } + this.path = pathToUse; + this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new {@code ClassPathResource} for {@code Class} usage. + * The path can be relative to the given class, or absolute within + * the classpath via a leading slash. + * @param path relative or absolute path within the class path + * @param clazz the class to load resources with + * @see java.lang.Class#getResourceAsStream + */ + public ClassPathResource(String path, Class<?> clazz) { + Assert.notNull(path, "Path must not be null"); + this.path = StringUtils.cleanPath(path); + this.clazz = clazz; + } + + /** + * Create a new {@code ClassPathResource} with optional {@code ClassLoader} + * and {@code Class}. Only for internal usage. + * @param path relative or absolute path within the classpath + * @param classLoader the class loader to load the resource with, if any + * @param clazz the class to load resources with, if any + */ + protected ClassPathResource(String path, ClassLoader classLoader, Class<?> clazz) { + this.path = StringUtils.cleanPath(path); + this.classLoader = classLoader; + this.clazz = clazz; + } + + + /** + * Return the path for this resource (as resource path within the class path). + */ + public final String getPath() { + return this.path; + } + + /** + * Return the ClassLoader that this resource will be obtained from. + */ + public final ClassLoader getClassLoader() { + return (this.clazz != null ? this.clazz.getClassLoader() : this.classLoader); + } + + + /** + * This implementation checks for the resolution of a resource URL. + * @see java.lang.ClassLoader#getResource(String) + * @see java.lang.Class#getResource(String) + */ + @Override + public boolean exists() { + return (resolveURL() != null); + } + + /** + * Resolves a URL for the underlying class path resource. + * @return the resolved URL, or {@code null} if not resolvable + */ + protected URL resolveURL() { + if (this.clazz != null) { + return this.clazz.getResource(this.path); + } + else if (this.classLoader != null) { + return this.classLoader.getResource(this.path); + } + else { + return ClassLoader.getSystemResource(this.path); + } + } + + /** + * This implementation opens an InputStream for the given class path resource. + * @see java.lang.ClassLoader#getResourceAsStream(String) + * @see java.lang.Class#getResourceAsStream(String) + */ + public InputStream getInputStream() throws IOException { + InputStream is; + if (this.clazz != null) { + is = this.clazz.getResourceAsStream(this.path); + } + else if (this.classLoader != null) { + is = this.classLoader.getResourceAsStream(this.path); + } + else { + is = ClassLoader.getSystemResourceAsStream(this.path); + } + if (is == null) { + throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist"); + } + return is; + } + + /** + * This implementation returns a URL for the underlying class path resource, + * if available. + * @see java.lang.ClassLoader#getResource(String) + * @see java.lang.Class#getResource(String) + */ + @Override + public URL getURL() throws IOException { + URL url = resolveURL(); + if (url == null) { + throw new FileNotFoundException(getDescription() + " cannot be resolved to URL because it does not exist"); + } + return url; + } + + /** + * This implementation creates a ClassPathResource, applying the given path + * relative to the path of the underlying resource of this descriptor. + * @see org.springframework.util.StringUtils#applyRelativePath(String, String) + */ + @Override + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(this.path, relativePath); + return new ClassPathResource(pathToUse, this.classLoader, this.clazz); + } + + /** + * This implementation returns the name of the file that this class path + * resource refers to. + * @see org.springframework.util.StringUtils#getFilename(String) + */ + @Override + public String getFilename() { + return StringUtils.getFilename(this.path); + } + + /** + * This implementation returns a description that includes the class path location. + */ + public String getDescription() { + StringBuilder builder = new StringBuilder("class path resource ["); + String pathToUse = path; + if (this.clazz != null && !pathToUse.startsWith("/")) { + builder.append(ClassUtils.classPackageAsResourcePath(this.clazz)); + builder.append('/'); + } + if (pathToUse.startsWith("/")) { + pathToUse = pathToUse.substring(1); + } + builder.append(pathToUse); + builder.append(']'); + return builder.toString(); + } + + /** + * This implementation compares the underlying class path locations. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof ClassPathResource) { + ClassPathResource otherRes = (ClassPathResource) obj; + return (this.path.equals(otherRes.path) && + ObjectUtils.nullSafeEquals(this.classLoader, otherRes.classLoader) && + ObjectUtils.nullSafeEquals(this.clazz, otherRes.clazz)); + } + return false; + } + + /** + * This implementation returns the hash code of the underlying + * class path location. + */ + @Override + public int hashCode() { + return this.path.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ClassRelativeResourceLoader.java b/spring-core/src/main/java/org/springframework/core/io/ClassRelativeResourceLoader.java new file mode 100644 index 00000000..00f267e4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ClassRelativeResourceLoader.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2012 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; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link ResourceLoader} implementation that interprets plain resource paths + * as relative to a given {@code java.lang.Class}. + * + * @author Juergen Hoeller + * @since 3.0 + * @see Class#getResource(String) + * @see ClassPathResource#ClassPathResource(String, Class) + */ +public class ClassRelativeResourceLoader extends DefaultResourceLoader { + + private final Class clazz; + + + /** + * Create a new ClassRelativeResourceLoader for the given class. + * @param clazz the class to load resources through + */ + public ClassRelativeResourceLoader(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + this.clazz = clazz; + setClassLoader(clazz.getClassLoader()); + } + + protected Resource getResourceByPath(String path) { + return new ClassRelativeContextResource(path, this.clazz); + } + + + /** + * ClassPathResource that explicitly expresses a context-relative path + * through implementing the ContextResource interface. + */ + private static class ClassRelativeContextResource extends ClassPathResource implements ContextResource { + + private final Class clazz; + + public ClassRelativeContextResource(String path, Class clazz) { + super(path, clazz); + this.clazz = clazz; + } + + public String getPathWithinContext() { + return getPath(); + } + + @Override + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath); + return new ClassRelativeContextResource(pathToUse, this.clazz); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ContextResource.java b/spring-core/src/main/java/org/springframework/core/io/ContextResource.java new file mode 100644 index 00000000..0493cfa7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ContextResource.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2007 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; + +/** + * Extended interface for a resource that is loaded from an enclosing + * 'context', e.g. from a {@link javax.servlet.ServletContext} or a + * {@link javax.portlet.PortletContext} but also from plain classpath paths + * or relative file system paths (specified without an explicit prefix, + * hence applying relative to the local {@link ResourceLoader}'s context). + * + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.web.context.support.ServletContextResource + * @see org.springframework.web.portlet.context.PortletContextResource + */ +public interface ContextResource extends Resource { + + /** + * Return the path within the enclosing 'context'. + * <p>This is typically path relative to a context-specific root directory, + * e.g. a ServletContext root or a PortletContext root. + */ + String getPathWithinContext(); + +} 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 new file mode 100644 index 00000000..1a390f04 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2012 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; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Default implementation of the {@link ResourceLoader} interface. + * Used by {@link ResourceEditor}, and serves as base class for + * {@link org.springframework.context.support.AbstractApplicationContext}. + * Can also be used standalone. + * + * <p>Will return a {@link UrlResource} if the location value is a URL, + * and a {@link ClassPathResource} if it is a non-URL path or a + * "classpath:" pseudo-URL. + * + * @author Juergen Hoeller + * @since 10.03.2004 + * @see FileSystemResourceLoader + * @see org.springframework.context.support.ClassPathXmlApplicationContext + */ +public class DefaultResourceLoader implements ResourceLoader { + + private ClassLoader classLoader; + + + /** + * Create a new DefaultResourceLoader. + * <p>ClassLoader access will happen using the thread context class loader + * at the time of this ResourceLoader's initialization. + * @see java.lang.Thread#getContextClassLoader() + */ + public DefaultResourceLoader() { + this.classLoader = ClassUtils.getDefaultClassLoader(); + } + + /** + * Create a new DefaultResourceLoader. + * @param classLoader the ClassLoader to load class path resources with, or {@code null} + * for using the thread context class loader at the time of actual resource access + */ + public DefaultResourceLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + + /** + * Specify the ClassLoader to load class path resources with, or {@code null} + * for using the thread context class loader at the time of actual resource access. + * <p>The default is that ClassLoader access will happen using the thread context + * class loader at the time of this ResourceLoader's initialization. + */ + public void setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + /** + * Return the ClassLoader to load class path resources with. + * <p>Will get passed to ClassPathResource's constructor for all + * ClassPathResource objects created by this resource loader. + * @see ClassPathResource + */ + public ClassLoader getClassLoader() { + return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader()); + } + + + public Resource getResource(String location) { + Assert.notNull(location, "Location must not be null"); + if (location.startsWith(CLASSPATH_URL_PREFIX)) { + return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); + } + else { + try { + // Try to parse the location as a URL... + URL url = new URL(location); + return new UrlResource(url); + } + catch (MalformedURLException ex) { + // No URL -> resolve as resource path. + return getResourceByPath(location); + } + } + } + + /** + * Return a Resource handle for the resource at the given path. + * <p>The default implementation supports class path locations. This should + * be appropriate for standalone implementations but can be overridden, + * e.g. for implementations targeted at a Servlet container. + * @param path the path to the resource + * @return the corresponding Resource handle + * @see ClassPathResource + * @see org.springframework.context.support.FileSystemXmlApplicationContext#getResourceByPath + * @see org.springframework.web.context.support.XmlWebApplicationContext#getResourceByPath + */ + protected Resource getResourceByPath(String path) { + return new ClassPathContextResource(path, getClassLoader()); + } + + + /** + * ClassPathResource that explicitly expresses a context-relative path + * through implementing the ContextResource interface. + */ + private static class ClassPathContextResource extends ClassPathResource implements ContextResource { + + public ClassPathContextResource(String path, ClassLoader classLoader) { + super(path, classLoader); + } + + public String getPathWithinContext() { + return getPath(); + } + + @Override + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath); + return new ClassPathContextResource(pathToUse, getClassLoader()); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/DescriptiveResource.java b/spring-core/src/main/java/org/springframework/core/io/DescriptiveResource.java new file mode 100644 index 00000000..8385891e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/DescriptiveResource.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2012 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; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * Simple {@link Resource} implementation that holds a resource description + * but does not point to an actually readable resource. + * + * <p>To be used as placeholder if a {@code Resource} argument is + * expected by an API but not necessarily used for actual reading. + * + * @author Juergen Hoeller + * @since 1.2.6 + */ +public class DescriptiveResource extends AbstractResource { + + private final String description; + + + /** + * Create a new DescriptiveResource. + * @param description the resource description + */ + public DescriptiveResource(String description) { + this.description = (description != null ? description : ""); + } + + + @Override + public boolean exists() { + return false; + } + + @Override + public boolean isReadable() { + return false; + } + + public InputStream getInputStream() throws IOException { + throw new FileNotFoundException( + getDescription() + " cannot be opened because it does not point to a readable resource"); + } + + public String getDescription() { + return this.description; + } + + + /** + * This implementation compares the underlying description String. + */ + @Override + public boolean equals(Object obj) { + return (obj == this || + (obj instanceof DescriptiveResource && ((DescriptiveResource) obj).description.equals(this.description))); + } + + /** + * This implementation returns the hash code of the underlying description String. + */ + @Override + public int hashCode() { + return this.description.hashCode(); + } + +} 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 new file mode 100644 index 00000000..05202a3b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java @@ -0,0 +1,219 @@ +/* + * Copyright 2002-2014 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; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URL; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link Resource} implementation for {@code java.io.File} handles. + * Obviously supports resolution as File, and also as URL. + * Implements the extended {@link WritableResource} interface. + * + * @author Juergen Hoeller + * @since 28.12.2003 + * @see java.io.File + */ +public class FileSystemResource extends AbstractResource implements WritableResource { + + private final File file; + + private final String path; + + + /** + * Create a new {@code FileSystemResource} from a {@link File} handle. + * <p>Note: When building relative resources via {@link #createRelative}, + * the relative path will apply <i>at the same directory level</i>: + * e.g. new File("C:/dir1"), relative path "dir2" -> "C:/dir2"! + * If you prefer to have relative paths built underneath the given root + * directory, use the {@link #FileSystemResource(String) constructor with a file path} + * to append a trailing slash to the root path: "C:/dir1/", which + * indicates this directory as root for all relative paths. + * @param file a File handle + */ + public FileSystemResource(File file) { + Assert.notNull(file, "File must not be null"); + this.file = file; + this.path = StringUtils.cleanPath(file.getPath()); + } + + /** + * Create a new {@code FileSystemResource} from a file path. + * <p>Note: When building relative resources via {@link #createRelative}, + * it makes a difference whether the specified resource base path here + * ends with a slash or not. In the case of "C:/dir1/", relative paths + * will be built underneath that root: e.g. relative path "dir2" -> + * "C:/dir1/dir2". In the case of "C:/dir1", relative paths will apply + * at the same directory level: relative path "dir2" -> "C:/dir2". + * @param path a file path + */ + public FileSystemResource(String path) { + Assert.notNull(path, "Path must not be null"); + this.file = new File(path); + this.path = StringUtils.cleanPath(path); + } + + + /** + * Return the file path for this resource. + */ + public final String getPath() { + return this.path; + } + + + /** + * This implementation returns whether the underlying file exists. + * @see java.io.File#exists() + */ + @Override + public boolean exists() { + return this.file.exists(); + } + + /** + * This implementation checks whether the underlying file is marked as readable + * (and corresponds to an actual file with content, not to a directory). + * @see java.io.File#canRead() + * @see java.io.File#isDirectory() + */ + @Override + public boolean isReadable() { + return (this.file.canRead() && !this.file.isDirectory()); + } + + /** + * This implementation opens a FileInputStream for the underlying file. + * @see java.io.FileInputStream + */ + public InputStream getInputStream() throws IOException { + return new FileInputStream(this.file); + } + + /** + * This implementation returns a URL for the underlying file. + * @see java.io.File#toURI() + */ + @Override + public URL getURL() throws IOException { + return this.file.toURI().toURL(); + } + + /** + * This implementation returns a URI for the underlying file. + * @see java.io.File#toURI() + */ + @Override + public URI getURI() throws IOException { + return this.file.toURI(); + } + + /** + * This implementation returns the underlying File reference. + */ + @Override + public File getFile() { + return this.file; + } + + /** + * This implementation returns the underlying File's length. + */ + @Override + public long contentLength() throws IOException { + return this.file.length(); + } + + /** + * This implementation creates a FileSystemResource, applying the given path + * relative to the path of the underlying file of this resource descriptor. + * @see org.springframework.util.StringUtils#applyRelativePath(String, String) + */ + @Override + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(this.path, relativePath); + return new FileSystemResource(pathToUse); + } + + /** + * This implementation returns the name of the file. + * @see java.io.File#getName() + */ + @Override + public String getFilename() { + return this.file.getName(); + } + + /** + * This implementation returns a description that includes the absolute + * path of the file. + * @see java.io.File#getAbsolutePath() + */ + public String getDescription() { + return "file [" + this.file.getAbsolutePath() + "]"; + } + + + // implementation of WritableResource + + /** + * This implementation checks whether the underlying file is marked as writable + * (and corresponds to an actual file with content, not to a directory). + * @see java.io.File#canWrite() + * @see java.io.File#isDirectory() + */ + public boolean isWritable() { + return (this.file.canWrite() && !this.file.isDirectory()); + } + + /** + * This implementation opens a FileOutputStream for the underlying file. + * @see java.io.FileOutputStream + */ + public OutputStream getOutputStream() throws IOException { + return new FileOutputStream(this.file); + } + + + /** + * This implementation compares the underlying File references. + */ + @Override + public boolean equals(Object obj) { + return (obj == this || + (obj instanceof FileSystemResource && this.path.equals(((FileSystemResource) obj).path))); + } + + /** + * This implementation returns the hash code of the underlying File reference. + */ + @Override + public int hashCode() { + return this.path.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/FileSystemResourceLoader.java b/spring-core/src/main/java/org/springframework/core/io/FileSystemResourceLoader.java new file mode 100644 index 00000000..d811d91f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/FileSystemResourceLoader.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2007 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; + +/** + * {@link ResourceLoader} implementation that resolves plain paths as + * file system resources rather than as class path resources + * (the latter is {@link DefaultResourceLoader}'s default strategy). + * + * <p><b>NOTE:</b> Plain paths will always be interpreted as relative + * to the current VM working directory, even if they start with a slash. + * (This is consistent with the semantics in a Servlet container.) + * <b>Use an explicit "file:" prefix to enforce an absolute file path.</b> + * + * <p>{@link org.springframework.context.support.FileSystemXmlApplicationContext} + * is a full-fledged ApplicationContext implementation that provides + * the same resource path resolution strategy. + * + * @author Juergen Hoeller + * @since 1.1.3 + * @see DefaultResourceLoader + * @see org.springframework.context.support.FileSystemXmlApplicationContext + */ +public class FileSystemResourceLoader extends DefaultResourceLoader { + + /** + * Resolve resource paths as file system paths. + * <p>Note: Even if a given path starts with a slash, it will get + * interpreted as relative to the current VM working directory. + * @param path the path to the resource + * @return the corresponding Resource handle + * @see FileSystemResource + * @see org.springframework.web.context.support.ServletContextResourceLoader#getResourceByPath + */ + @Override + protected Resource getResourceByPath(String path) { + if (path != null && path.startsWith("/")) { + path = path.substring(1); + } + return new FileSystemContextResource(path); + } + + + /** + * FileSystemResource that explicitly expresses a context-relative path + * through implementing the ContextResource interface. + */ + private static class FileSystemContextResource extends FileSystemResource implements ContextResource { + + public FileSystemContextResource(String path) { + super(path); + } + + public String getPathWithinContext() { + return getPath(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java b/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java new file mode 100644 index 00000000..d9b2405c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2012 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; + +import java.io.IOException; +import java.io.InputStream; + +/** + * {@link Resource} implementation for a given InputStream. Should only + * be used if no specific Resource implementation is applicable. + * In particular, prefer {@link ByteArrayResource} or any of the + * file-based Resource implementations where possible. + * + * <p>In contrast to other Resource implementations, this is a descriptor + * for an <i>already opened</i> resource - therefore returning "true" from + * {@code isOpen()}. Do not use it if you need to keep the resource + * descriptor somewhere, or if you need to read a stream multiple times. + * + * @author Juergen Hoeller + * @since 28.12.2003 + * @see ByteArrayResource + * @see ClassPathResource + * @see FileSystemResource + * @see UrlResource + */ +public class InputStreamResource extends AbstractResource { + + private final InputStream inputStream; + + private final String description; + + private boolean read = false; + + + /** + * Create a new InputStreamResource. + * @param inputStream the InputStream to use + */ + public InputStreamResource(InputStream inputStream) { + this(inputStream, "resource loaded through InputStream"); + } + + /** + * Create a new InputStreamResource. + * @param inputStream the InputStream to use + * @param description where the InputStream comes from + */ + public InputStreamResource(InputStream inputStream, String description) { + if (inputStream == null) { + throw new IllegalArgumentException("InputStream must not be null"); + } + this.inputStream = inputStream; + this.description = (description != null ? description : ""); + } + + + /** + * This implementation always returns {@code true}. + */ + @Override + public boolean exists() { + return true; + } + + /** + * This implementation always returns {@code true}. + */ + @Override + public boolean isOpen() { + return true; + } + + /** + * This implementation throws IllegalStateException if attempting to + * read the underlying stream multiple times. + */ + public InputStream getInputStream() throws IOException, IllegalStateException { + if (this.read) { + throw new IllegalStateException("InputStream has already been read - " + + "do not use InputStreamResource if a stream needs to be read multiple times"); + } + this.read = true; + return this.inputStream; + } + + /** + * This implementation returns the passed-in description, if any. + */ + public String getDescription() { + return this.description; + } + + + /** + * This implementation compares the underlying InputStream. + */ + @Override + public boolean equals(Object obj) { + return (obj == this || + (obj instanceof InputStreamResource && ((InputStreamResource) obj).inputStream.equals(this.inputStream))); + } + + /** + * This implementation returns the hash code of the underlying InputStream. + */ + @Override + public int hashCode() { + return this.inputStream.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java b/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java new file mode 100644 index 00000000..f31e6ef9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2012 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; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Simple interface for objects that are sources for an {@link InputStream}. + * + * <p>This is the base interface for Spring's more extensive {@link Resource} interface. + * + * <p>For single-use streams, {@link InputStreamResource} can be used for any + * given {@code InputStream}. Spring's {@link ByteArrayResource} or any + * file-based {@code Resource} implementation can be used as a concrete + * instance, allowing one to read the underlying content stream multiple times. + * This makes this interface useful as an abstract content source for mail + * attachments, for example. + * + * @author Juergen Hoeller + * @since 20.01.2004 + * @see java.io.InputStream + * @see Resource + * @see InputStreamResource + * @see ByteArrayResource + */ +public interface InputStreamSource { + + /** + * Return an {@link InputStream}. + * <p>It is expected that each call creates a <i>fresh</i> stream. + * <p>This requirement is particularly important when you consider an API such + * as JavaMail, which needs to be able to read the stream multiple times when + * creating mail attachments. For such a use case, it is <i>required</i> + * that each {@code getInputStream()} call returns a fresh stream. + * @return the input stream for the underlying resource (must not be {@code null}) + * @throws IOException if the stream could not be opened + * @see org.springframework.mail.javamail.MimeMessageHelper#addAttachment(String, InputStreamSource) + */ + InputStream getInputStream() throws IOException; + +} 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 new file mode 100644 index 00000000..cc6eef30 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/Resource.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2012 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; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; + +/** + * Interface for a resource descriptor that abstracts from the actual + * type of underlying resource, such as a file or class path resource. + * + * <p>An InputStream can be opened for every resource if it exists in + * physical form, but a URL or File handle can just be returned for + * certain resources. The actual behavior is implementation-specific. + * + * @author Juergen Hoeller + * @since 28.12.2003 + * @see #getInputStream() + * @see #getURL() + * @see #getURI() + * @see #getFile() + * @see WritableResource + * @see ContextResource + * @see FileSystemResource + * @see ClassPathResource + * @see UrlResource + * @see ByteArrayResource + * @see InputStreamResource + */ +public interface Resource extends InputStreamSource { + + /** + * Return whether this resource actually exists in physical form. + * <p>This method performs a definitive existence check, whereas the + * 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()}. + * <p>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 + * that the resource content cannot be read. + * @see #getInputStream() + */ + boolean isReadable(); + + /** + * Return whether this resource represents a handle with an open + * stream. If true, the InputStream cannot be read multiple times, + * and must be read and closed to avoid resource leaks. + * <p>Will be {@code false} for typical resource descriptors. + */ + boolean isOpen(); + + /** + * Return a URL handle for this resource. + * @throws IOException if the resource cannot be resolved as URL, + * i.e. if the resource is not available as descriptor + */ + URL getURL() throws IOException; + + /** + * 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 + */ + URI getURI() throws IOException; + + /** + * Return a File handle for this resource. + * @throws IOException if the resource cannot be resolved as absolute + * file path, i.e. if the resource is not available in a file system + */ + File getFile() throws IOException; + + /** + * Determine the content length for this resource. + * @throws IOException if the resource cannot be resolved + * (in the file system or as some other known physical resource type) + */ + long contentLength() throws IOException; + + /** + * Determine the last-modified timestamp for this resource. + * @throws IOException if the resource cannot be resolved + * (in the file system or as some other known physical resource type) + */ + long lastModified() throws IOException; + + /** + * Create a resource relative to this resource. + * @param relativePath the relative path (relative to this resource) + * @return the resource handle for the relative resource + * @throws IOException if the relative resource cannot be determined + */ + Resource createRelative(String relativePath) throws IOException; + + /** + * Determine a filename for this resource, i.e. typically the last + * part of the path: for example, "myfile.txt". + * <p>Returns {@code null} if this type of resource does not + * have a filename. + */ + String getFilename(); + + /** + * Return a description for this resource, + * to be used for error output when working with the resource. + * <p>Implementations are also encouraged to return this value + * from their {@code toString} method. + * @see Object#toString() + */ + String getDescription(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ResourceEditor.java b/spring-core/src/main/java/org/springframework/core/io/ResourceEditor.java new file mode 100644 index 00000000..a01d5f7f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ResourceEditor.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2013 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; + +import java.beans.PropertyEditorSupport; +import java.io.IOException; + +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link java.beans.PropertyEditor Editor} for {@link Resource} + * descriptors, to automatically convert {@code String} locations + * e.g. {@code file:C:/myfile.txt} or {@code classpath:myfile.txt} to + * {@code Resource} properties instead of using a {@code String} location property. + * + * <p>The path may contain {@code ${...}} placeholders, to be + * resolved as {@link org.springframework.core.env.Environment} properties: + * e.g. {@code ${user.dir}}. Unresolvable placeholders are ignored by default. + * + * <p>Delegates to a {@link ResourceLoader} to do the heavy lifting, + * by default using a {@link DefaultResourceLoader}. + * + * @author Juergen Hoeller + * @author Dave Syer + * @author Chris Beams + * @since 28.12.2003 + * @see Resource + * @see ResourceLoader + * @see DefaultResourceLoader + * @see PropertyResolver#resolvePlaceholders + */ +public class ResourceEditor extends PropertyEditorSupport { + + private final ResourceLoader resourceLoader; + + private PropertyResolver propertyResolver; + + private final boolean ignoreUnresolvablePlaceholders; + + + /** + * Create a new instance of the {@link ResourceEditor} class + * using a {@link DefaultResourceLoader} and {@link StandardEnvironment}. + */ + public ResourceEditor() { + this(new DefaultResourceLoader(), null); + } + + /** + * Create a new instance of the {@link ResourceEditor} class + * using the given {@link ResourceLoader} and a {@link StandardEnvironment}. + * @param resourceLoader the {@code ResourceLoader} to use + * @deprecated as of Spring 3.1 in favor of + * {@link #ResourceEditor(ResourceLoader, PropertyResolver)} + */ + @Deprecated + public ResourceEditor(ResourceLoader resourceLoader) { + this(resourceLoader, null, true); + } + + /** + * Create a new instance of the {@link ResourceEditor} class + * using the given {@link ResourceLoader}. + * @param resourceLoader the {@code ResourceLoader} to use + * @param ignoreUnresolvablePlaceholders whether to ignore unresolvable placeholders + * if no corresponding property could be found + * @deprecated as of Spring 3.1 in favor of + * {@link #ResourceEditor(ResourceLoader, PropertyResolver, boolean)} + */ + @Deprecated + public ResourceEditor(ResourceLoader resourceLoader, boolean ignoreUnresolvablePlaceholders) { + this(resourceLoader, null, ignoreUnresolvablePlaceholders); + } + + /** + * Create a new instance of the {@link ResourceEditor} class + * using the given {@link ResourceLoader} and {@link PropertyResolver}. + * @param resourceLoader the {@code ResourceLoader} to use + * @param propertyResolver the {@code PropertyResolver} to use + */ + public ResourceEditor(ResourceLoader resourceLoader, PropertyResolver propertyResolver) { + this(resourceLoader, propertyResolver, true); + } + + /** + * Create a new instance of the {@link ResourceEditor} class + * using the given {@link ResourceLoader}. + * @param resourceLoader the {@code ResourceLoader} to use + * @param propertyResolver the {@code PropertyResolver} to use + * @param ignoreUnresolvablePlaceholders whether to ignore unresolvable placeholders + * if no corresponding property could be found in the given {@code propertyResolver} + */ + public ResourceEditor(ResourceLoader resourceLoader, PropertyResolver propertyResolver, boolean ignoreUnresolvablePlaceholders) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + this.resourceLoader = resourceLoader; + this.propertyResolver = propertyResolver; + this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + } + + + @Override + public void setAsText(String text) { + if (StringUtils.hasText(text)) { + String locationToUse = resolvePath(text).trim(); + setValue(this.resourceLoader.getResource(locationToUse)); + } + else { + setValue(null); + } + } + + /** + * Resolve the given path, replacing placeholders with corresponding + * property values from the {@code environment} if necessary. + * @param path the original file path + * @return the resolved file path + * @see PropertyResolver#resolvePlaceholders + * @see PropertyResolver#resolveRequiredPlaceholders + */ + protected String resolvePath(String path) { + if (this.propertyResolver == null) { + this.propertyResolver = new StandardEnvironment(); + } + return (this.ignoreUnresolvablePlaceholders ? this.propertyResolver.resolvePlaceholders(path) : + this.propertyResolver.resolveRequiredPlaceholders(path)); + } + + + @Override + public String getAsText() { + Resource value = (Resource) getValue(); + try { + // Try to determine URL for resource. + return (value != null ? value.getURL().toExternalForm() : ""); + } + catch (IOException ex) { + // Couldn't determine resource URL - return null to indicate + // that there is no appropriate text representation. + return null; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ResourceLoader.java b/spring-core/src/main/java/org/springframework/core/io/ResourceLoader.java new file mode 100644 index 00000000..b753535a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ResourceLoader.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2014 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; + +import org.springframework.util.ResourceUtils; + +/** + * Strategy interface for loading resources (e.. class path or file system + * resources). An {@link org.springframework.context.ApplicationContext} + * is required to provide this functionality, plus extended + * {@link org.springframework.core.io.support.ResourcePatternResolver} support. + * + * <p>{@link DefaultResourceLoader} is a standalone implementation that is + * usable outside an ApplicationContext, also used by {@link ResourceEditor}. + * + * <p>Bean properties of type Resource and Resource array can be populated + * from Strings when running in an ApplicationContext, using the particular + * context's resource loading strategy. + * + * @author Juergen Hoeller + * @since 10.03.2004 + * @see Resource + * @see org.springframework.core.io.support.ResourcePatternResolver + * @see org.springframework.context.ApplicationContext + * @see org.springframework.context.ResourceLoaderAware + */ +public interface ResourceLoader { + + /** Pseudo URL prefix for loading from the class path: "classpath:" */ + String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; + + + /** + * Return a Resource handle for the specified resource. + * The handle should always be a reusable resource descriptor, + * allowing for multiple {@link Resource#getInputStream()} calls. + * <p><ul> + * <li>Must support fully qualified URLs, e.g. "file:C:/test.dat". + * <li>Must support classpath pseudo-URLs, e.g. "classpath:test.dat". + * <li>Should support relative file paths, e.g. "WEB-INF/test.dat". + * (This will be implementation-specific, typically provided by an + * ApplicationContext implementation.) + * </ul> + * <p>Note that a Resource handle does not imply an existing resource; + * you need to invoke {@link Resource#exists} to check for existence. + * @param location the resource location + * @return a corresponding Resource handle + * @see #CLASSPATH_URL_PREFIX + * @see org.springframework.core.io.Resource#exists + * @see org.springframework.core.io.Resource#getInputStream + */ + Resource getResource(String location); + + /** + * Expose the ClassLoader used by this ResourceLoader. + * <p>Clients which need to access the ClassLoader directly can do so + * in a uniform manner with the ResourceLoader, rather than relying + * on the thread context ClassLoader. + * @return the ClassLoader (only {@code null} if even the system + * ClassLoader isn't accessible) + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + */ + ClassLoader getClassLoader(); + +} 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 new file mode 100644 index 00000000..626f8f70 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/UrlResource.java @@ -0,0 +1,263 @@ +/* + * Copyright 2002-2013 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; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; + +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * {@link Resource} implementation for {@code java.net.URL} locators. + * Obviously supports resolution as URL, and also as File in case of + * the "file:" protocol. + * + * @author Juergen Hoeller + * @since 28.12.2003 + * @see java.net.URL + */ +public class UrlResource extends AbstractFileResolvingResource { + + /** + * Original URI, if available; used for URI and File access. + */ + private final URI uri; + + /** + * Original URL, used for actual access. + */ + private final URL url; + + /** + * Cleaned URL (with normalized path), used for comparisons. + */ + private final URL cleanedUrl; + + + /** + * Create a new UrlResource based on the given URI object. + * @param uri a URI + * @throws MalformedURLException if the given URL path is not valid + */ + public UrlResource(URI uri) throws MalformedURLException { + Assert.notNull(uri, "URI must not be null"); + this.uri = uri; + this.url = uri.toURL(); + this.cleanedUrl = getCleanedUrl(this.url, uri.toString()); + } + + /** + * Create a new UrlResource based on the given URL object. + * @param url a URL + */ + public UrlResource(URL url) { + Assert.notNull(url, "URL must not be null"); + this.url = url; + this.cleanedUrl = getCleanedUrl(this.url, url.toString()); + this.uri = null; + } + + /** + * Create a new UrlResource based on a URL path. + * <p>Note: The given path needs to be pre-encoded if necessary. + * @param path a URL path + * @throws MalformedURLException if the given URL path is not valid + * @see java.net.URL#URL(String) + */ + public UrlResource(String path) throws MalformedURLException { + Assert.notNull(path, "Path must not be null"); + this.uri = null; + this.url = new URL(path); + this.cleanedUrl = getCleanedUrl(this.url, path); + } + + /** + * Create a new UrlResource based on a URI specification. + * <p>The given parts will automatically get encoded if necessary. + * @param protocol the URL protocol to use (e.g. "jar" or "file" - without colon); + * also known as "scheme" + * @param location the location (e.g. the file path within that protocol); + * also known as "scheme-specific part" + * @throws MalformedURLException if the given URL specification is not valid + * @see java.net.URI#URI(String, String, String) + */ + public UrlResource(String protocol, String location) throws MalformedURLException { + this(protocol, location, null); + } + + /** + * Create a new UrlResource based on a URI specification. + * <p>The given parts will automatically get encoded if necessary. + * @param protocol the URL protocol to use (e.g. "jar" or "file" - without colon); + * also known as "scheme" + * @param location the location (e.g. the file path within that protocol); + * also known as "scheme-specific part" + * @param fragment the fragment within that location (e.g. anchor on an HTML page, + * as following after a "#" separator) + * @throws MalformedURLException if the given URL specification is not valid + * @see java.net.URI#URI(String, String, String) + */ + public UrlResource(String protocol, String location, String fragment) throws MalformedURLException { + try { + this.uri = new URI(protocol, location, fragment); + this.url = this.uri.toURL(); + this.cleanedUrl = getCleanedUrl(this.url, this.uri.toString()); + } + catch (URISyntaxException ex) { + MalformedURLException exToThrow = new MalformedURLException(ex.getMessage()); + exToThrow.initCause(ex); + throw exToThrow; + } + } + + /** + * Determine a cleaned URL for the given original URL. + * @param originalUrl the original URL + * @param originalPath the original URL path + * @return the cleaned URL + * @see org.springframework.util.StringUtils#cleanPath + */ + private URL getCleanedUrl(URL originalUrl, String originalPath) { + try { + return new URL(StringUtils.cleanPath(originalPath)); + } + catch (MalformedURLException ex) { + // Cleaned URL path cannot be converted to URL + // -> take original URL. + return originalUrl; + } + } + + + /** + * This implementation opens an InputStream for the given URL. + * It sets the "UseCaches" flag to {@code false}, + * mainly to avoid jar file locking on Windows. + * @see java.net.URL#openConnection() + * @see java.net.URLConnection#setUseCaches(boolean) + * @see java.net.URLConnection#getInputStream() + */ + public InputStream getInputStream() throws IOException { + URLConnection con = this.url.openConnection(); + ResourceUtils.useCachesIfNecessary(con); + try { + return con.getInputStream(); + } + catch (IOException ex) { + // Close the HTTP connection (if applicable). + if (con instanceof HttpURLConnection) { + ((HttpURLConnection) con).disconnect(); + } + throw ex; + } + } + + /** + * This implementation returns the underlying URL reference. + */ + @Override + public URL getURL() throws IOException { + return this.url; + } + + /** + * This implementation returns the underlying URI directly, + * if possible. + */ + @Override + public URI getURI() throws IOException { + if (this.uri != null) { + return this.uri; + } + else { + return super.getURI(); + } + } + + /** + * This implementation returns a File reference for the underlying URL/URI, + * provided that it refers to a file in the file system. + * @see org.springframework.util.ResourceUtils#getFile(java.net.URL, String) + */ + @Override + public File getFile() throws IOException { + if (this.uri != null) { + return super.getFile(this.uri); + } + else { + return super.getFile(); + } + } + + /** + * This implementation creates a UrlResource, applying the given path + * relative to the path of the underlying URL of this resource descriptor. + * @see java.net.URL#URL(java.net.URL, String) + */ + @Override + public Resource createRelative(String relativePath) throws MalformedURLException { + if (relativePath.startsWith("/")) { + relativePath = relativePath.substring(1); + } + return new UrlResource(new URL(this.url, relativePath)); + } + + /** + * This implementation returns the name of the file that this URL refers to. + * @see java.net.URL#getFile() + * @see java.io.File#getName() + */ + @Override + public String getFilename() { + return new File(this.url.getFile()).getName(); + } + + /** + * This implementation returns a description that includes the URL. + */ + public String getDescription() { + return "URL [" + this.url + "]"; + } + + + /** + * This implementation compares the underlying URL references. + */ + @Override + public boolean equals(Object obj) { + return (obj == this || + (obj instanceof UrlResource && this.cleanedUrl.equals(((UrlResource) obj).cleanedUrl))); + } + + /** + * This implementation returns the hash code of the underlying URL reference. + */ + @Override + public int hashCode() { + return this.cleanedUrl.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/VfsResource.java b/spring-core/src/main/java/org/springframework/core/io/VfsResource.java new file mode 100644 index 00000000..4877d0ad --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/VfsResource.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2012 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; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; + +import org.springframework.core.NestedIOException; +import org.springframework.util.Assert; + +/** + * VFS based {@link Resource} implementation. + * Supports the corresponding VFS API versions on JBoss AS 5.x as well as 6.x and 7.x. + * + * @author Ales Justin + * @author Juergen Hoeller + * @author Costin Leau + * @since 3.0 + * @see org.jboss.vfs.VirtualFile + */ +public class VfsResource extends AbstractResource { + + private final Object resource; + + + public VfsResource(Object resources) { + Assert.notNull(resources, "VirtualFile must not be null"); + this.resource = resources; + } + + + public InputStream getInputStream() throws IOException { + return VfsUtils.getInputStream(this.resource); + } + + @Override + public boolean exists() { + return VfsUtils.exists(this.resource); + } + + @Override + public boolean isReadable() { + return VfsUtils.isReadable(this.resource); + } + + @Override + public URL getURL() throws IOException { + try { + return VfsUtils.getURL(this.resource); + } + catch (Exception ex) { + throw new NestedIOException("Failed to obtain URL for file " + this.resource, ex); + } + } + + @Override + public URI getURI() throws IOException { + try { + return VfsUtils.getURI(this.resource); + } + catch (Exception ex) { + throw new NestedIOException("Failed to obtain URI for " + this.resource, ex); + } + } + + @Override + public File getFile() throws IOException { + return VfsUtils.getFile(this.resource); + } + + @Override + public long contentLength() throws IOException { + return VfsUtils.getSize(this.resource); + } + + @Override + public long lastModified() throws IOException { + return VfsUtils.getLastModified(this.resource); + } + + @Override + public Resource createRelative(String relativePath) throws IOException { + if (!relativePath.startsWith(".") && relativePath.contains("/")) { + try { + return new VfsResource(VfsUtils.getChild(this.resource, relativePath)); + } + catch (IOException ex) { + // fall back to getRelative + } + } + + return new VfsResource(VfsUtils.getRelative(new URL(getURL(), relativePath))); + } + + @Override + public String getFilename() { + return VfsUtils.getName(this.resource); + } + + public String getDescription() { + return this.resource.toString(); + } + + @Override + public boolean equals(Object obj) { + return (obj == this || (obj instanceof VfsResource && this.resource.equals(((VfsResource) obj).resource))); + } + + @Override + public int hashCode() { + return this.resource.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/VfsUtils.java b/spring-core/src/main/java/org/springframework/core/io/VfsUtils.java new file mode 100644 index 00000000..8c9a7aa1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/VfsUtils.java @@ -0,0 +1,260 @@ +/* + * Copyright 2002-2012 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; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URL; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.NestedIOException; +import org.springframework.util.ReflectionUtils; + +/** + * Utility for detecting the JBoss VFS version available in the classpath. + * JBoss AS 5+ uses VFS 2.x (package {@code org.jboss.virtual}) while + * JBoss AS 6+ uses VFS 3.x (package {@code org.jboss.vfs}). + * + * <p>Thanks go to Marius Bogoevici for the initial patch. + * + * <b>Note:</b> This is an internal class and should not be used outside the framework. + * + * @author Costin Leau + * @since 3.0.3 + */ +public abstract class VfsUtils { + + private static final Log logger = LogFactory.getLog(VfsUtils.class); + + private static final String VFS2_PKG = "org.jboss.virtual."; + private static final String VFS3_PKG = "org.jboss.vfs."; + private static final String VFS_NAME = "VFS"; + + private static enum VFS_VER { V2, V3 } + + private static VFS_VER version; + + private static Method VFS_METHOD_GET_ROOT_URL = null; + private static Method VFS_METHOD_GET_ROOT_URI = null; + + private static Method VIRTUAL_FILE_METHOD_EXISTS = null; + private static Method VIRTUAL_FILE_METHOD_GET_INPUT_STREAM; + private static Method VIRTUAL_FILE_METHOD_GET_SIZE; + private static Method VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED; + private static Method VIRTUAL_FILE_METHOD_TO_URL; + private static Method VIRTUAL_FILE_METHOD_TO_URI; + private static Method VIRTUAL_FILE_METHOD_GET_NAME; + private static Method VIRTUAL_FILE_METHOD_GET_PATH_NAME; + private static Method VIRTUAL_FILE_METHOD_GET_CHILD; + + protected static Class<?> VIRTUAL_FILE_VISITOR_INTERFACE; + protected static Method VIRTUAL_FILE_METHOD_VISIT; + + private static Method VFS_UTILS_METHOD_IS_NESTED_FILE = null; + private static Method VFS_UTILS_METHOD_GET_COMPATIBLE_URI = null; + private static Field VISITOR_ATTRIBUTES_FIELD_RECURSE = null; + private static Method GET_PHYSICAL_FILE = null; + + static { + ClassLoader loader = VfsUtils.class.getClassLoader(); + String pkg; + Class<?> vfsClass; + + // check for JBoss 6 + try { + vfsClass = loader.loadClass(VFS3_PKG + VFS_NAME); + version = VFS_VER.V3; + pkg = VFS3_PKG; + + if (logger.isDebugEnabled()) { + logger.debug("JBoss VFS packages for JBoss AS 6 found"); + } + } + catch (ClassNotFoundException ex) { + // fallback to JBoss 5 + if (logger.isDebugEnabled()) + logger.debug("JBoss VFS packages for JBoss AS 6 not found; falling back to JBoss AS 5 packages"); + try { + vfsClass = loader.loadClass(VFS2_PKG + VFS_NAME); + + version = VFS_VER.V2; + pkg = VFS2_PKG; + + if (logger.isDebugEnabled()) + logger.debug("JBoss VFS packages for JBoss AS 5 found"); + } + catch (ClassNotFoundException ex2) { + logger.error("JBoss VFS packages (for both JBoss AS 5 and 6) were not found - JBoss VFS support disabled"); + throw new IllegalStateException("Cannot detect JBoss VFS packages", ex2); + } + } + + // cache reflective information + try { + String methodName = (VFS_VER.V3.equals(version) ? "getChild" : "getRoot"); + + VFS_METHOD_GET_ROOT_URL = ReflectionUtils.findMethod(vfsClass, methodName, URL.class); + VFS_METHOD_GET_ROOT_URI = ReflectionUtils.findMethod(vfsClass, methodName, URI.class); + + Class<?> virtualFile = loader.loadClass(pkg + "VirtualFile"); + + VIRTUAL_FILE_METHOD_EXISTS = ReflectionUtils.findMethod(virtualFile, "exists"); + VIRTUAL_FILE_METHOD_GET_INPUT_STREAM = ReflectionUtils.findMethod(virtualFile, "openStream"); + VIRTUAL_FILE_METHOD_GET_SIZE = ReflectionUtils.findMethod(virtualFile, "getSize"); + VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED = ReflectionUtils.findMethod(virtualFile, "getLastModified"); + VIRTUAL_FILE_METHOD_TO_URI = ReflectionUtils.findMethod(virtualFile, "toURI"); + VIRTUAL_FILE_METHOD_TO_URL = ReflectionUtils.findMethod(virtualFile, "toURL"); + VIRTUAL_FILE_METHOD_GET_NAME = ReflectionUtils.findMethod(virtualFile, "getName"); + VIRTUAL_FILE_METHOD_GET_PATH_NAME = ReflectionUtils.findMethod(virtualFile, "getPathName"); + GET_PHYSICAL_FILE = ReflectionUtils.findMethod(virtualFile, "getPhysicalFile"); + + methodName = (VFS_VER.V3.equals(version) ? "getChild" : "findChild"); + + VIRTUAL_FILE_METHOD_GET_CHILD = ReflectionUtils.findMethod(virtualFile, methodName, String.class); + + Class<?> utilsClass = loader.loadClass(pkg + "VFSUtils"); + + VFS_UTILS_METHOD_GET_COMPATIBLE_URI = ReflectionUtils.findMethod(utilsClass, "getCompatibleURI", + virtualFile); + VFS_UTILS_METHOD_IS_NESTED_FILE = ReflectionUtils.findMethod(utilsClass, "isNestedFile", virtualFile); + + VIRTUAL_FILE_VISITOR_INTERFACE = loader.loadClass(pkg + "VirtualFileVisitor"); + VIRTUAL_FILE_METHOD_VISIT = ReflectionUtils.findMethod(virtualFile, "visit", VIRTUAL_FILE_VISITOR_INTERFACE); + + Class<?> visitorAttributesClass = loader.loadClass(pkg + "VisitorAttributes"); + VISITOR_ATTRIBUTES_FIELD_RECURSE = ReflectionUtils.findField(visitorAttributesClass, "RECURSE"); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Could not detect the JBoss VFS infrastructure", ex); + } + } + + protected static Object invokeVfsMethod(Method method, Object target, Object... args) throws IOException { + try { + return method.invoke(target, args); + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (targetEx instanceof IOException) { + throw (IOException) targetEx; + } + ReflectionUtils.handleInvocationTargetException(ex); + } + catch (Exception ex) { + ReflectionUtils.handleReflectionException(ex); + } + + throw new IllegalStateException("Invalid code path reached"); + } + + static boolean exists(Object vfsResource) { + try { + return (Boolean) invokeVfsMethod(VIRTUAL_FILE_METHOD_EXISTS, vfsResource); + } + catch (IOException ex) { + return false; + } + } + + static boolean isReadable(Object vfsResource) { + try { + return ((Long) invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_SIZE, vfsResource) > 0); + } + catch (IOException ex) { + return false; + } + } + + static long getSize(Object vfsResource) throws IOException { + return (Long) invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_SIZE, vfsResource); + } + + static long getLastModified(Object vfsResource) throws IOException { + return (Long) invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED, vfsResource); + } + + static InputStream getInputStream(Object vfsResource) throws IOException { + return (InputStream) invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_INPUT_STREAM, vfsResource); + } + + static URL getURL(Object vfsResource) throws IOException { + return (URL) invokeVfsMethod(VIRTUAL_FILE_METHOD_TO_URL, vfsResource); + } + + static URI getURI(Object vfsResource) throws IOException { + return (URI) invokeVfsMethod(VIRTUAL_FILE_METHOD_TO_URI, vfsResource); + } + + static String getName(Object vfsResource) { + try { + return (String) invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_NAME, vfsResource); + } + catch (IOException ex) { + throw new IllegalStateException("Cannot get resource name", ex); + } + } + + static Object getRelative(URL url) throws IOException { + return invokeVfsMethod(VFS_METHOD_GET_ROOT_URL, null, url); + } + + static Object getChild(Object vfsResource, String path) throws IOException { + return invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_CHILD, vfsResource, path); + } + + static File getFile(Object vfsResource) throws IOException { + if (VFS_VER.V2.equals(version)) { + if ((Boolean) invokeVfsMethod(VFS_UTILS_METHOD_IS_NESTED_FILE, null, vfsResource)) { + throw new IOException("File resolution not supported for nested resource: " + vfsResource); + } + try { + return new File((URI) invokeVfsMethod(VFS_UTILS_METHOD_GET_COMPATIBLE_URI, null, vfsResource)); + } + catch (Exception ex) { + throw new NestedIOException("Failed to obtain File reference for " + vfsResource, ex); + } + } + else { + return (File) invokeVfsMethod(GET_PHYSICAL_FILE, vfsResource); + } + } + + static Object getRoot(URI url) throws IOException { + return invokeVfsMethod(VFS_METHOD_GET_ROOT_URI, null, url); + } + + // protected methods used by the support sub-package + + protected static Object getRoot(URL url) throws IOException { + return invokeVfsMethod(VFS_METHOD_GET_ROOT_URL, null, url); + } + + protected static Object doGetVisitorAttribute() { + return ReflectionUtils.getField(VISITOR_ATTRIBUTES_FIELD_RECURSE, null); + } + + protected static String doGetPath(Object resource) { + return (String) ReflectionUtils.invokeMethod(VIRTUAL_FILE_METHOD_GET_PATH_NAME, resource); + } +} diff --git a/spring-core/src/main/java/org/springframework/core/io/WritableResource.java b/spring-core/src/main/java/org/springframework/core/io/WritableResource.java new file mode 100644 index 00000000..72d4fe34 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/WritableResource.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2012 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; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Extended interface for a resource that supports writing to it. + * Provides an {@link #getOutputStream() OutputStream accessor}. + * + * @author Juergen Hoeller + * @since 3.1 + * @see java.io.OutputStream + */ +public interface WritableResource extends Resource { + + /** + * Return whether the contents of this resource can be modified, + * e.g. via {@link #getOutputStream()} or {@link #getFile()}. + * <p>Will be {@code true} for typical resource descriptors; + * note that actual content writing may still fail when attempted. + * However, a value of {@code false} is a definitive indication + * that the resource content cannot be modified. + * @see #getOutputStream() + * @see #isReadable() + */ + boolean isWritable(); + + /** + * Return an {@link OutputStream} for the underlying resource, + * allowing to (over-)write its content. + * @throws IOException if the stream could not be opened + * @see #getInputStream() + */ + OutputStream getOutputStream() throws IOException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/package-info.java b/spring-core/src/main/java/org/springframework/core/io/package-info.java new file mode 100644 index 00000000..4631d05a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * Generic abstraction for (file-based) resources, used throughout the framework. + * + */ +package org.springframework.core.io; + diff --git a/spring-core/src/main/java/org/springframework/core/io/support/EncodedResource.java b/spring-core/src/main/java/org/springframework/core/io/support/EncodedResource.java new file mode 100644 index 00000000..04a17383 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/EncodedResource.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2013 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 java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Holder that combines a {@link org.springframework.core.io.Resource} + * with a specific encoding to be used for reading from the resource. + * + * <p>Used as argument for operations that support to read content with + * a specific encoding (usually through a {@code java.io.Reader}. + * + * @author Juergen Hoeller + * @since 1.2.6 + * @see java.io.Reader + */ +public class EncodedResource { + + private final Resource resource; + + private String encoding; + + private Charset charset; + + + /** + * Create a new EncodedResource for the given Resource, + * not specifying a specific encoding. + * @param resource the Resource to hold + */ + public EncodedResource(Resource resource) { + Assert.notNull(resource, "Resource must not be null"); + this.resource = resource; + } + + /** + * Create a new EncodedResource for the given Resource, + * using the specified encoding. + * @param resource the Resource to hold + * @param encoding the encoding to use for reading from the resource + */ + public EncodedResource(Resource resource, String encoding) { + Assert.notNull(resource, "Resource must not be null"); + this.resource = resource; + this.encoding = encoding; + } + + /** + * Create a new EncodedResource for the given Resource, + * using the specified encoding. + * @param resource the Resource to hold + * @param charset the charset to use for reading from the resource + */ + public EncodedResource(Resource resource, Charset charset) { + Assert.notNull(resource, "Resource must not be null"); + this.resource = resource; + this.charset = charset; + } + + + /** + * Return the Resource held. + */ + public final Resource getResource() { + return this.resource; + } + + /** + * Return the encoding to use for reading from the resource, + * or {@code null} if none specified. + */ + public final String getEncoding() { + return this.encoding; + } + + /** + * Return the charset to use for reading from the resource, + * or {@code null} if none specified. + */ + public final Charset getCharset() { + return this.charset; + } + + + /** + * Determine whether a {@link Reader} is required as opposed to an {@link InputStream}, + * i.e. whether an encoding or a charset has been specified. + * @see #getReader() + * @see #getInputStream() + */ + public boolean requiresReader() { + return (this.encoding != null || this.charset != null); + } + + /** + * Open a {@code java.io.Reader} for the specified resource, + * using the specified encoding (if any). + * @throws IOException if opening the Reader failed + * @see #requiresReader() + */ + public Reader getReader() throws IOException { + if (this.charset != null) { + return new InputStreamReader(this.resource.getInputStream(), this.charset); + } + else if (this.encoding != null) { + return new InputStreamReader(this.resource.getInputStream(), this.encoding); + } + else { + return new InputStreamReader(this.resource.getInputStream()); + } + } + + /** + * Open an {@code java.io.InputStream} for the specified resource, + * typically assuming that there is no specific encoding to use. + * @throws IOException if opening the InputStream failed + * @see #requiresReader() + */ + public InputStream getInputStream() throws IOException { + return this.resource.getInputStream(); + } + + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof EncodedResource) { + EncodedResource otherRes = (EncodedResource) obj; + return (this.resource.equals(otherRes.resource) && + ObjectUtils.nullSafeEquals(this.encoding, otherRes.encoding)); + } + return false; + } + + @Override + public int hashCode() { + return this.resource.hashCode(); + } + + @Override + public String toString() { + return this.resource.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/LocalizedResourceHelper.java b/spring-core/src/main/java/org/springframework/core/io/support/LocalizedResourceHelper.java new file mode 100644 index 00000000..57f2645a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/LocalizedResourceHelper.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2012 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.util.Locale; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; + +/** + * Helper class for loading a localized resource, + * specified through name, extension and current locale. + * + * @author Juergen Hoeller + * @since 1.2.5 + */ +public class LocalizedResourceHelper { + + /** The default separator to use inbetween file name parts: an underscore */ + public static final String DEFAULT_SEPARATOR = "_"; + + + private final ResourceLoader resourceLoader; + + private String separator = DEFAULT_SEPARATOR; + + + /** + * Create a new LocalizedResourceHelper with a DefaultResourceLoader. + * @see org.springframework.core.io.DefaultResourceLoader + */ + public LocalizedResourceHelper() { + this.resourceLoader = new DefaultResourceLoader(); + } + + /** + * Create a new LocalizedResourceHelper with the given ResourceLoader. + * @param resourceLoader the ResourceLoader to use + */ + public LocalizedResourceHelper(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + this.resourceLoader = resourceLoader; + } + + /** + * Set the separator to use inbetween file name parts. + * Default is an underscore ("_"). + */ + public void setSeparator(String separator) { + this.separator = (separator != null ? separator : DEFAULT_SEPARATOR); + } + + + /** + * Find the most specific localized resource for the given name, + * extension and locale: + * <p>The file will be searched with locations in the following order, + * similar to {@code java.util.ResourceBundle}'s search order: + * <ul> + * <li>[name]_[language]_[country]_[variant][extension] + * <li>[name]_[language]_[country][extension] + * <li>[name]_[language][extension] + * <li>[name][extension] + * </ul> + * <p>If none of the specific files can be found, a resource + * descriptor for the default location will be returned. + * @param name the name of the file, without localization part nor extension + * @param extension the file extension (e.g. ".xls") + * @param locale the current locale (may be {@code null}) + * @return the most specific localized resource found + * @see java.util.ResourceBundle + */ + public Resource findLocalizedResource(String name, String extension, Locale locale) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(extension, "Extension must not be null"); + + Resource resource = null; + + if (locale != null) { + String lang = locale.getLanguage(); + String country = locale.getCountry(); + String variant = locale.getVariant(); + + // Check for file with language, country and variant localization. + if (variant.length() > 0) { + String location = + name + this.separator + lang + this.separator + country + this.separator + variant + extension; + resource = this.resourceLoader.getResource(location); + } + + // Check for file with language and country localization. + if ((resource == null || !resource.exists()) && country.length() > 0) { + String location = name + this.separator + lang + this.separator + country + extension; + resource = this.resourceLoader.getResource(location); + } + + // Check for document with language localization. + if ((resource == null || !resource.exists()) && lang.length() > 0) { + String location = name + this.separator + lang + extension; + resource = this.resourceLoader.getResource(location); + } + } + + // Check for document without localization. + if (resource == null || !resource.exists()) { + String location = name + extension; + resource = this.resourceLoader.getResource(location); + } + + return 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 new file mode 100644 index 00000000..72b0c481 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -0,0 +1,733 @@ +/* + * Copyright 2002-2014 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.File; +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.net.JarURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.UrlResource; +import org.springframework.core.io.VfsResource; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.PathMatcher; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * A {@link ResourcePatternResolver} implementation that is able to resolve a + * specified resource location path into one or more matching Resources. + * The source path may be a simple path which has a one-to-one mapping to a + * target {@link org.springframework.core.io.Resource}, or alternatively + * may contain the special "{@code classpath*:}" prefix and/or + * internal Ant-style regular expressions (matched using Spring's + * {@link org.springframework.util.AntPathMatcher} utility). + * Both of the latter are effectively wildcards. + * + * <p><b>No Wildcards:</b> + * + * <p>In the simple case, if the specified location path does not start with the + * {@code "classpath*:}" prefix, and does not contain a PathMatcher pattern, + * this resolver will simply return a single resource via a + * {@code getResource()} call on the underlying {@code ResourceLoader}. + * Examples are real URLs such as "{@code file:C:/context.xml}", pseudo-URLs + * such as "{@code classpath:/context.xml}", and simple unprefixed paths + * such as "{@code /WEB-INF/context.xml}". The latter will resolve in a + * fashion specific to the underlying {@code ResourceLoader} (e.g. + * {@code ServletContextResource} for a {@code WebApplicationContext}). + * + * <p><b>Ant-style Patterns:</b> + * + * <p>When the path location contains an Ant-style pattern, e.g.: + * <pre class="code"> + * /WEB-INF/*-context.xml + * com/mycompany/**/applicationContext.xml + * file:C:/some/path/*-context.xml + * classpath:com/mycompany/**/applicationContext.xml</pre> + * the resolver follows a more complex but defined procedure to try to resolve + * the wildcard. It produces a {@code Resource} for the path up to the last + * non-wildcard segment and obtains a {@code URL} from it. If this URL is + * not a "{@code jar:}" URL or container-specific variant (e.g. + * "{@code zip:}" in WebLogic, "{@code wsjar}" in WebSphere", etc.), + * then a {@code java.io.File} is obtained from it, and used to resolve the + * wildcard by walking the filesystem. In the case of a jar URL, the resolver + * either gets a {@code java.net.JarURLConnection} from it, or manually parses + * the jar URL, and then traverses the contents of the jar file, to resolve the + * wildcards. + * + * <p><b>Implications on portability:</b> + * + * <p>If the specified path is already a file URL (either explicitly, or + * implicitly because the base {@code ResourceLoader} is a filesystem one, + * then wildcarding is guaranteed to work in a completely portable fashion. + * + * <p>If the specified path is a classpath location, then the resolver must + * obtain the last non-wildcard path segment URL via a + * {@code Classloader.getResource()} call. Since this is just a + * node of the path (not the file at the end) it is actually undefined + * (in the ClassLoader Javadocs) exactly what sort of a URL is returned in + * this case. In practice, it is usually a {@code java.io.File} representing + * the directory, where the classpath resource resolves to a filesystem + * location, or a jar URL of some sort, where the classpath resource resolves + * to a jar location. Still, there is a portability concern on this operation. + * + * <p>If a jar URL is obtained for the last non-wildcard segment, the resolver + * must be able to get a {@code java.net.JarURLConnection} from it, or + * manually parse the jar URL, to be able to walk the contents of the jar, + * and resolve the wildcard. This will work in most environments, but will + * fail in others, and it is strongly recommended that the wildcard + * resolution of resources coming from jars be thoroughly tested in your + * specific environment before you rely on it. + * + * <p><b>{@code classpath*:} Prefix:</b> + * + * <p>There is special support for retrieving multiple class path resources with + * the same name, via the "{@code classpath*:}" prefix. For example, + * "{@code classpath*:META-INF/beans.xml}" will find all "beans.xml" + * files in the class path, be it in "classes" directories or in JAR files. + * This is particularly useful for autodetecting config files of the same name + * at the same location within each jar file. Internally, this happens via a + * {@code ClassLoader.getResources()} call, and is completely portable. + * + * <p>The "classpath*:" prefix can also be combined with a PathMatcher pattern in + * the rest of the location path, for example "classpath*:META-INF/*-beans.xml". + * In this case, the resolution strategy is fairly simple: a + * {@code ClassLoader.getResources()} call is used on the last non-wildcard + * path segment to get all the matching resources in the class loader hierarchy, + * and then off each resource the same PathMatcher resolution strategy described + * above is used for the wildcard subpath. + * + * <p><b>Other notes:</b> + * + * <p><b>WARNING:</b> Note that "{@code classpath*:}" when combined with + * Ant-style patterns will only work reliably with at least one root directory + * before the pattern starts, unless the actual target files reside in the file + * system. This means that a pattern like "{@code classpath*:*.xml}" will + * <i>not</i> retrieve files from the root of jar files but rather only from the + * 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). + * + * <p><b>WARNING:</b> Ant-style patterns with "classpath:" resources are not + * guaranteed to find matching resources if the root package to search is available + * in multiple class path locations. This is because a resource such as + * <pre class="code"> + * com/mycompany/package1/service-context.xml + * </pre> + * may be in only one location, but when a path such as + * <pre class="code"> + * classpath:com/mycompany/**/service-context.xml + * </pre> + * is used to try to resolve it, the resolver will work off the (first) URL + * returned by {@code getResource("com/mycompany");}. If this base package + * node exists in multiple classloader locations, the actual end resource may + * not be underneath. Therefore, preferably, use "{@code classpath*:}" with the same + * Ant-style pattern in such a case, which will search <i>all</i> class path + * locations that contain the root package. + * + * @author Juergen Hoeller + * @author Colin Sampaleanu + * @author Marius Bogoevici + * @author Costin Leau + * @since 1.0.2 + * @see #CLASSPATH_ALL_URL_PREFIX + * @see org.springframework.util.AntPathMatcher + * @see org.springframework.core.io.ResourceLoader#getResource(String) + * @see ClassLoader#getResources(String) + */ +public class PathMatchingResourcePatternResolver implements ResourcePatternResolver { + + private static final Log logger = LogFactory.getLog(PathMatchingResourcePatternResolver.class); + + private static Method equinoxResolveMethod; + + static { + try { + // Detect Equinox OSGi (e.g. on WebSphere 6.1) + Class<?> fileLocatorClass = ClassUtils.forName("org.eclipse.core.runtime.FileLocator", + PathMatchingResourcePatternResolver.class.getClassLoader()); + equinoxResolveMethod = fileLocatorClass.getMethod("resolve", URL.class); + logger.debug("Found Equinox FileLocator for OSGi bundle URL resolution"); + } + catch (Throwable ex) { + equinoxResolveMethod = null; + } + } + + + private final ResourceLoader resourceLoader; + + private PathMatcher pathMatcher = new AntPathMatcher(); + + + /** + * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader. + * <p>ClassLoader access will happen via the thread context class loader. + * @see org.springframework.core.io.DefaultResourceLoader + */ + public PathMatchingResourcePatternResolver() { + this.resourceLoader = new DefaultResourceLoader(); + } + + /** + * Create a new PathMatchingResourcePatternResolver. + * <p>ClassLoader access will happen via the thread context class loader. + * @param resourceLoader the ResourceLoader to load root directories and + * actual resources with + */ + public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + this.resourceLoader = resourceLoader; + } + + /** + * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader. + * @param classLoader the ClassLoader to load classpath resources with, + * or {@code null} for using the thread context class loader + * at the time of actual resource access + * @see org.springframework.core.io.DefaultResourceLoader + */ + public PathMatchingResourcePatternResolver(ClassLoader classLoader) { + this.resourceLoader = new DefaultResourceLoader(classLoader); + } + + + /** + * Return the ResourceLoader that this pattern resolver works with. + */ + public ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + + public ClassLoader getClassLoader() { + return getResourceLoader().getClassLoader(); + } + + /** + * Set the PathMatcher implementation to use for this + * resource pattern resolver. Default is AntPathMatcher. + * @see org.springframework.util.AntPathMatcher + */ + public void setPathMatcher(PathMatcher pathMatcher) { + Assert.notNull(pathMatcher, "PathMatcher must not be null"); + this.pathMatcher = pathMatcher; + } + + /** + * Return the PathMatcher that this resource pattern resolver uses. + */ + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + + + public Resource getResource(String location) { + return getResourceLoader().getResource(location); + } + + public Resource[] getResources(String locationPattern) throws IOException { + Assert.notNull(locationPattern, "Location pattern must not be null"); + if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { + // a class path resource (multiple resources for same name possible) + if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) { + // a class path resource pattern + return findPathMatchingResources(locationPattern); + } + else { + // all class path resources with the given name + return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); + } + } + else { + // Only look for a pattern after a prefix here + // (to not get fooled by a pattern symbol in a strange prefix). + int prefixEnd = locationPattern.indexOf(":") + 1; + if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) { + // a file pattern + return findPathMatchingResources(locationPattern); + } + else { + // a single resource with the given name + return new Resource[] {getResourceLoader().getResource(locationPattern)}; + } + } + } + + /** + * Find all class location resources with the given location via the ClassLoader. + * @param location the absolute path within the classpath + * @return the result as Resource array + * @throws IOException in case of I/O errors + * @see java.lang.ClassLoader#getResources + * @see #convertClassLoaderURL + */ + protected Resource[] findAllClassPathResources(String location) throws IOException { + String path = location; + if (path.startsWith("/")) { + path = path.substring(1); + } + ClassLoader cl = getClassLoader(); + Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path)); + Set<Resource> result = new LinkedHashSet<Resource>(16); + while (resourceUrls.hasMoreElements()) { + URL url = resourceUrls.nextElement(); + result.add(convertClassLoaderURL(url)); + } + return result.toArray(new Resource[result.size()]); + } + + /** + * Convert the given URL as returned from the ClassLoader into a Resource object. + * <p>The default implementation simply creates a UrlResource instance. + * @param url a URL as returned from the ClassLoader + * @return the corresponding Resource object + * @see java.lang.ClassLoader#getResources + * @see org.springframework.core.io.Resource + */ + protected Resource convertClassLoaderURL(URL url) { + return new UrlResource(url); + } + + /** + * Find all resources that match the given location pattern via the + * Ant-style PathMatcher. Supports resources in jar files and zip files + * and in the file system. + * @param locationPattern the location pattern to match + * @return the result as Resource array + * @throws IOException in case of I/O errors + * @see #doFindPathMatchingJarResources + * @see #doFindPathMatchingFileResources + * @see org.springframework.util.PathMatcher + */ + protected Resource[] findPathMatchingResources(String locationPattern) throws IOException { + String rootDirPath = determineRootDir(locationPattern); + String subPattern = locationPattern.substring(rootDirPath.length()); + Resource[] rootDirResources = getResources(rootDirPath); + Set<Resource> result = new LinkedHashSet<Resource>(16); + for (Resource rootDirResource : rootDirResources) { + rootDirResource = resolveRootDirResource(rootDirResource); + if (rootDirResource.getURL().getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { + result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirResource, subPattern, getPathMatcher())); + } + else if (isJarResource(rootDirResource)) { + result.addAll(doFindPathMatchingJarResources(rootDirResource, subPattern)); + } + else { + result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern)); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result); + } + return result.toArray(new Resource[result.size()]); + } + + /** + * Determine the root directory for the given location. + * <p>Used for determining the starting point for file matching, + * resolving the root directory location to a {@code java.io.File} + * and passing it into {@code retrieveMatchingFiles}, with the + * remainder of the location as pattern. + * <p>Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml", + * for example. + * @param location the location to check + * @return the part of the location that denotes the root directory + * @see #retrieveMatchingFiles + */ + protected String determineRootDir(String location) { + int prefixEnd = location.indexOf(":") + 1; + int rootDirEnd = location.length(); + while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) { + rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1; + } + if (rootDirEnd == 0) { + rootDirEnd = prefixEnd; + } + return location.substring(0, rootDirEnd); + } + + /** + * Resolve the specified resource for path matching. + * <p>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. + * @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. + * <p>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). + * @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()); + } + + /** + * 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 the Set of matching Resource instances + * @throws IOException in case of I/O errors + * @see java.net.JarURLConnection + * @see org.springframework.util.PathMatcher + */ + protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, String subPattern) + throws IOException { + + URLConnection con = rootDirResource.getURL().openConnection(); + JarFile jarFile; + String jarFileUrl; + String rootEntryPath; + boolean newJarFile = false; + + if (con instanceof JarURLConnection) { + // Should usually be the case for traditional JAR files. + JarURLConnection jarCon = (JarURLConnection) con; + ResourceUtils.useCachesIfNecessary(jarCon); + jarFile = jarCon.getJarFile(); + jarFileUrl = jarCon.getJarFileURL().toExternalForm(); + JarEntry jarEntry = jarCon.getJarEntry(); + rootEntryPath = (jarEntry != null ? jarEntry.getName() : ""); + } + 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(); + int separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR); + if (separatorIndex != -1) { + jarFileUrl = urlFile.substring(0, separatorIndex); + rootEntryPath = urlFile.substring(separatorIndex + ResourceUtils.JAR_URL_SEPARATOR.length()); + jarFile = getJarFile(jarFileUrl); + } + else { + jarFile = new JarFile(urlFile); + jarFileUrl = urlFile; + rootEntryPath = ""; + } + newJarFile = true; + } + + try { + if (logger.isDebugEnabled()) { + logger.debug("Looking for matching resources in jar file [" + jarFileUrl + "]"); + } + if (!"".equals(rootEntryPath) && !rootEntryPath.endsWith("/")) { + // Root entry path must end with slash to allow for proper matching. + // The Sun JRE does not return a slash here, but BEA JRockit does. + rootEntryPath = rootEntryPath + "/"; + } + Set<Resource> result = new LinkedHashSet<Resource>(8); + for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) { + JarEntry entry = entries.nextElement(); + String entryPath = entry.getName(); + if (entryPath.startsWith(rootEntryPath)) { + String relativePath = entryPath.substring(rootEntryPath.length()); + if (getPathMatcher().match(subPattern, relativePath)) { + result.add(rootDirResource.createRelative(relativePath)); + } + } + } + return result; + } + finally { + // Close jar file, but only if freshly obtained - + // not from JarURLConnection, which might cache the file reference. + if (newJarFile) { + jarFile.close(); + } + } + } + + /** + * Resolve the given jar file URL into a JarFile object. + */ + protected JarFile getJarFile(String jarFileUrl) throws IOException { + if (jarFileUrl.startsWith(ResourceUtils.FILE_URL_PREFIX)) { + try { + return new JarFile(ResourceUtils.toURI(jarFileUrl).getSchemeSpecificPart()); + } + catch (URISyntaxException ex) { + // Fallback for URLs that are not valid URIs (should hardly ever happen). + return new JarFile(jarFileUrl.substring(ResourceUtils.FILE_URL_PREFIX.length())); + } + } + else { + return new JarFile(jarFileUrl); + } + } + + /** + * Find all resources in the file system 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 the Set of matching Resource instances + * @throws IOException in case of I/O errors + * @see #retrieveMatchingFiles + * @see org.springframework.util.PathMatcher + */ + protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) + throws IOException { + + File rootDir; + try { + rootDir = rootDirResource.getFile().getAbsoluteFile(); + } + catch (IOException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Cannot search for matching files underneath " + rootDirResource + + " because it does not correspond to a directory in the file system", ex); + } + return Collections.emptySet(); + } + return doFindMatchingFileSystemResources(rootDir, subPattern); + } + + /** + * Find all resources in the file system that match the given location pattern + * via the Ant-style PathMatcher. + * @param rootDir the root directory in the file system + * @param subPattern the sub pattern to match (below the root directory) + * @return the Set of matching Resource instances + * @throws IOException in case of I/O errors + * @see #retrieveMatchingFiles + * @see org.springframework.util.PathMatcher + */ + protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException { + if (logger.isDebugEnabled()) { + logger.debug("Looking for matching resources in directory tree [" + rootDir.getPath() + "]"); + } + Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern); + Set<Resource> result = new LinkedHashSet<Resource>(matchingFiles.size()); + for (File file : matchingFiles) { + result.add(new FileSystemResource(file)); + } + return result; + } + + /** + * Retrieve files that match the given path pattern, + * checking the given directory and its subdirectories. + * @param rootDir the directory to start from + * @param pattern the pattern to match against, + * relative to the root directory + * @return the Set of matching File instances + * @throws IOException if directory contents could not be retrieved + */ + protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException { + if (!rootDir.exists()) { + // Silently skip non-existing directories. + if (logger.isDebugEnabled()) { + logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist"); + } + return Collections.emptySet(); + } + if (!rootDir.isDirectory()) { + // Complain louder if it exists but is no directory. + if (logger.isWarnEnabled()) { + logger.warn("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory"); + } + return Collections.emptySet(); + } + if (!rootDir.canRead()) { + if (logger.isWarnEnabled()) { + logger.warn("Cannot search for matching files underneath directory [" + rootDir.getAbsolutePath() + + "] because the application is not allowed to read the directory"); + } + return Collections.emptySet(); + } + String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/"); + if (!pattern.startsWith("/")) { + fullPattern += "/"; + } + fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/"); + Set<File> result = new LinkedHashSet<File>(8); + doRetrieveMatchingFiles(fullPattern, rootDir, result); + return result; + } + + /** + * Recursively retrieve files that match the given pattern, + * adding them to the given result list. + * @param fullPattern the pattern to match against, + * with prepended root directory path + * @param dir the current directory + * @param result the Set of matching File instances to add to + * @throws IOException if directory contents could not be retrieved + */ + protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException { + if (logger.isDebugEnabled()) { + logger.debug("Searching directory [" + dir.getAbsolutePath() + + "] for files matching pattern [" + fullPattern + "]"); + } + File[] dirContents = dir.listFiles(); + if (dirContents == null) { + if (logger.isWarnEnabled()) { + logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]"); + } + return; + } + for (File content : dirContents) { + String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/"); + if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) { + if (!content.canRead()) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() + + "] because the application is not allowed to read the directory"); + } + } + else { + doRetrieveMatchingFiles(fullPattern, content, result); + } + } + if (getPathMatcher().match(fullPattern, currPath)) { + result.add(content); + } + } + } + + + /** + * Inner delegate class, avoiding a hard JBoss VFS API dependency at runtime. + */ + private static class VfsResourceMatchingDelegate { + + public static Set<Resource> findMatchingResources( + Resource rootResource, String locationPattern, PathMatcher pathMatcher) throws IOException { + Object root = VfsPatternUtils.findRoot(rootResource.getURL()); + PatternVirtualFileVisitor visitor = + new PatternVirtualFileVisitor(VfsPatternUtils.getPath(root), locationPattern, pathMatcher); + VfsPatternUtils.visit(root, visitor); + return visitor.getResources(); + } + } + + + /** + * VFS visitor for path matching purposes. + */ + @SuppressWarnings("unused") + private static class PatternVirtualFileVisitor implements InvocationHandler { + + private final String subPattern; + + private final PathMatcher pathMatcher; + + private final String rootPath; + + private final Set<Resource> resources = new LinkedHashSet<Resource>(); + + public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) { + this.subPattern = subPattern; + this.pathMatcher = pathMatcher; + this.rootPath = (rootPath.length() == 0 || rootPath.endsWith("/") ? rootPath : rootPath + "/"); + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + if (Object.class.equals(method.getDeclaringClass())) { + if (methodName.equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0]); + } + else if (methodName.equals("hashCode")) { + return System.identityHashCode(proxy); + } + } + else if ("getAttributes".equals(methodName)) { + return getAttributes(); + } + else if ("visit".equals(methodName)) { + visit(args[0]); + return null; + } + else if ("toString".equals(methodName)) { + return toString(); + } + + throw new IllegalStateException("Unexpected method invocation: " + method); + } + + public void visit(Object vfsResource) { + if (this.pathMatcher.match(this.subPattern, + VfsPatternUtils.getPath(vfsResource).substring(this.rootPath.length()))) { + this.resources.add(new VfsResource(vfsResource)); + } + } + + public Object getAttributes() { + return VfsPatternUtils.getVisitorAttribute(); + } + + public Set<Resource> getResources() { + return this.resources; + } + + public int size() { + return this.resources.size(); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("sub-pattern: ").append(this.subPattern); + sb.append(", resources: ").append(this.resources); + return sb.toString(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java new file mode 100644 index 00000000..fb950839 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java @@ -0,0 +1,192 @@ +/* + * Copyright 2002-2014 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 java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.Resource; +import org.springframework.util.CollectionUtils; +import org.springframework.util.DefaultPropertiesPersister; +import org.springframework.util.PropertiesPersister; + +/** + * Base class for JavaBean-style components that need to load properties + * from one or more resources. Supports local properties as well, with + * configurable overriding. + * + * @author Juergen Hoeller + * @since 1.2.2 + */ +public abstract class PropertiesLoaderSupport { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + protected Properties[] localProperties; + + protected boolean localOverride = false; + + private Resource[] locations; + + private boolean ignoreResourceNotFound = false; + + private String fileEncoding; + + private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister(); + + + /** + * Set local properties, e.g. via the "props" tag in XML bean definitions. + * These can be considered defaults, to be overridden by properties + * loaded from files. + */ + public void setProperties(Properties properties) { + this.localProperties = new Properties[] {properties}; + } + + /** + * Set local properties, e.g. via the "props" tag in XML bean definitions, + * allowing for merging multiple properties sets into one. + */ + public void setPropertiesArray(Properties... propertiesArray) { + this.localProperties = propertiesArray; + } + + /** + * Set a location of a properties file to be loaded. + * <p>Can point to a classic properties file or to an XML file + * that follows JDK 1.5's properties XML format. + */ + public void setLocation(Resource location) { + this.locations = new Resource[] {location}; + } + + /** + * Set locations of properties files to be loaded. + * <p>Can point to classic properties files or to XML files + * that follow JDK 1.5's properties XML format. + * <p>Note: Properties defined in later files will override + * properties defined earlier files, in case of overlapping keys. + * Hence, make sure that the most specific files are the last + * ones in the given list of locations. + */ + public void setLocations(Resource... locations) { + this.locations = locations; + } + + /** + * Set whether local properties override properties from files. + * <p>Default is "false": Properties from files override local defaults. + * Can be switched to "true" to let local properties override defaults + * from files. + */ + public void setLocalOverride(boolean localOverride) { + this.localOverride = localOverride; + } + + /** + * Set if failure to find the property resource should be ignored. + * <p>"true" is appropriate if the properties file is completely optional. + * Default is "false". + */ + public void setIgnoreResourceNotFound(boolean ignoreResourceNotFound) { + this.ignoreResourceNotFound = ignoreResourceNotFound; + } + + /** + * Set the encoding to use for parsing properties files. + * <p>Default is none, using the {@code java.util.Properties} + * default encoding. + * <p>Only applies to classic properties files, not to XML files. + * @see org.springframework.util.PropertiesPersister#load + */ + public void setFileEncoding(String encoding) { + this.fileEncoding = encoding; + } + + /** + * Set the PropertiesPersister to use for parsing properties files. + * The default is DefaultPropertiesPersister. + * @see org.springframework.util.DefaultPropertiesPersister + */ + public void setPropertiesPersister(PropertiesPersister propertiesPersister) { + this.propertiesPersister = + (propertiesPersister != null ? propertiesPersister : new DefaultPropertiesPersister()); + } + + + /** + * Return a merged Properties instance containing both the + * loaded properties and properties set on this FactoryBean. + */ + protected Properties mergeProperties() throws IOException { + Properties result = new Properties(); + + if (this.localOverride) { + // Load properties from file upfront, to let local properties override. + loadProperties(result); + } + + if (this.localProperties != null) { + for (Properties localProp : this.localProperties) { + CollectionUtils.mergePropertiesIntoMap(localProp, result); + } + } + + if (!this.localOverride) { + // Load properties from file afterwards, to let those properties override. + loadProperties(result); + } + + return result; + } + + /** + * Load properties into the given instance. + * @param props the Properties instance to load into + * @throws IOException in case of I/O errors + * @see #setLocations + */ + protected void loadProperties(Properties props) throws IOException { + if (this.locations != null) { + for (Resource location : this.locations) { + if (logger.isInfoEnabled()) { + logger.info("Loading properties file from " + location); + } + try { + PropertiesLoaderUtils.fillProperties( + props, new EncodedResource(location, this.fileEncoding), this.propertiesPersister); + } + catch (IOException ex) { + if (this.ignoreResourceNotFound) { + if (logger.isWarnEnabled()) { + logger.warn("Could not load properties from " + location + ": " + ex.getMessage()); + } + } + else { + throw ex; + } + } + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderUtils.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderUtils.java new file mode 100644 index 00000000..c501a52f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderUtils.java @@ -0,0 +1,200 @@ +/* + * Copyright 2002-2014 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 java.io.InputStream; +import java.io.Reader; +import java.net.URL; +import java.net.URLConnection; +import java.util.Enumeration; +import java.util.Properties; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.DefaultPropertiesPersister; +import org.springframework.util.PropertiesPersister; +import org.springframework.util.ResourceUtils; + +/** + * Convenient utility methods for loading of {@code java.util.Properties}, + * performing standard handling of input streams. + * + * <p>For more configurable properties loading, including the option of a + * customized encoding, consider using the PropertiesLoaderSupport class. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0 + * @see PropertiesLoaderSupport + */ +public abstract class PropertiesLoaderUtils { + + private static final String XML_FILE_EXTENSION = ".xml"; + + + /** + * Load properties from the given EncodedResource, + * potentially defining a specific encoding for the properties file. + * @see #fillProperties(java.util.Properties, EncodedResource) + */ + public static Properties loadProperties(EncodedResource resource) throws IOException { + Properties props = new Properties(); + fillProperties(props, resource); + return props; + } + + /** + * Fill the given properties from the given EncodedResource, + * potentially defining a specific encoding for the properties file. + * @param props the Properties instance to load into + * @param resource the resource to load from + * @throws IOException in case of I/O errors + */ + public static void fillProperties(Properties props, EncodedResource resource) + throws IOException { + + fillProperties(props, resource, new DefaultPropertiesPersister()); + } + + /** + * Actually load properties from the given EncodedResource into the given Properties instance. + * @param props the Properties instance to load into + * @param resource the resource to load from + * @param persister the PropertiesPersister to use + * @throws IOException in case of I/O errors + */ + static void fillProperties(Properties props, EncodedResource resource, PropertiesPersister persister) + throws IOException { + + InputStream stream = null; + Reader reader = null; + try { + String filename = resource.getResource().getFilename(); + if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) { + stream = resource.getInputStream(); + persister.loadFromXml(props, stream); + } + else if (resource.requiresReader()) { + reader = resource.getReader(); + persister.load(props, reader); + } + else { + stream = resource.getInputStream(); + persister.load(props, stream); + } + } + finally { + if (stream != null) { + stream.close(); + } + if (reader != null) { + reader.close(); + } + } + } + + /** + * Load properties from the given resource (in ISO-8859-1 encoding). + * @param resource the resource to load from + * @return the populated Properties instance + * @throws IOException if loading failed + * @see #fillProperties(java.util.Properties, Resource) + */ + public static Properties loadProperties(Resource resource) throws IOException { + Properties props = new Properties(); + fillProperties(props, resource); + return props; + } + + /** + * Fill the given properties from the given resource (in ISO-8859-1 encoding). + * @param props the Properties instance to fill + * @param resource the resource to load from + * @throws IOException if loading failed + */ + public static void fillProperties(Properties props, Resource resource) throws IOException { + InputStream is = resource.getInputStream(); + try { + String filename = resource.getFilename(); + if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) { + props.loadFromXML(is); + } + else { + props.load(is); + } + } + finally { + is.close(); + } + } + + /** + * Load all properties from the specified class path resource + * (in ISO-8859-1 encoding), using the default class loader. + * <p>Merges properties if more than one resource of the same name + * found in the class path. + * @param resourceName the name of the class path resource + * @return the populated Properties instance + * @throws IOException if loading failed + */ + public static Properties loadAllProperties(String resourceName) throws IOException { + return loadAllProperties(resourceName, null); + } + + /** + * Load all properties from the specified class path resource + * (in ISO-8859-1 encoding), using the given class loader. + * <p>Merges properties if more than one resource of the same name + * found in the class path. + * @param resourceName the name of the class path resource + * @param classLoader the ClassLoader to use for loading + * (or {@code null} to use the default class loader) + * @return the populated Properties instance + * @throws IOException if loading failed + */ + public static Properties loadAllProperties(String resourceName, ClassLoader classLoader) throws IOException { + Assert.notNull(resourceName, "Resource name must not be null"); + ClassLoader classLoaderToUse = classLoader; + if (classLoaderToUse == null) { + classLoaderToUse = ClassUtils.getDefaultClassLoader(); + } + Enumeration<URL> urls = (classLoaderToUse != null ? classLoaderToUse.getResources(resourceName) : + ClassLoader.getSystemResources(resourceName)); + Properties props = new Properties(); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + URLConnection con = url.openConnection(); + ResourceUtils.useCachesIfNecessary(con); + InputStream is = con.getInputStream(); + try { + if (resourceName != null && resourceName.endsWith(XML_FILE_EXTENSION)) { + props.loadFromXML(is); + } + else { + props.load(is); + } + } + finally { + is.close(); + } + } + return props; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java new file mode 100644 index 00000000..e37a21f0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2013 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.beans.PropertyEditorSupport; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * Editor for {@link org.springframework.core.io.Resource} arrays, to + * automatically convert {@code String} location patterns + * (e.g. {@code "file:C:/my*.txt"} or {@code "classpath*:myfile.txt"}) + * to {@code Resource} array properties. Can also translate a collection + * or array of location patterns into a merged Resource array. + * + * <p>A path may contain {@code ${...}} placeholders, to be + * resolved as {@link org.springframework.core.env.Environment} properties: + * e.g. {@code ${user.dir}}. Unresolvable placeholders are ignored by default. + * + * <p>Delegates to a {@link ResourcePatternResolver}, + * by default using a {@link PathMatchingResourcePatternResolver}. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 1.1.2 + * @see org.springframework.core.io.Resource + * @see ResourcePatternResolver + * @see PathMatchingResourcePatternResolver + */ +public class ResourceArrayPropertyEditor extends PropertyEditorSupport { + + private static final Log logger = LogFactory.getLog(ResourceArrayPropertyEditor.class); + + private final ResourcePatternResolver resourcePatternResolver; + + private PropertyResolver propertyResolver; + + private final boolean ignoreUnresolvablePlaceholders; + + + /** + * Create a new ResourceArrayPropertyEditor with a default + * {@link PathMatchingResourcePatternResolver} and {@link StandardEnvironment}. + * @see PathMatchingResourcePatternResolver + * @see Environment + */ + public ResourceArrayPropertyEditor() { + this(new PathMatchingResourcePatternResolver(), null, true); + } + + /** + * Create a new ResourceArrayPropertyEditor with the given {@link ResourcePatternResolver} + * and a {@link StandardEnvironment}. + * @param resourcePatternResolver the ResourcePatternResolver to use + * @deprecated as of 3.1 in favor of {@link #ResourceArrayPropertyEditor(ResourcePatternResolver, PropertyResolver)} + */ + @Deprecated + public ResourceArrayPropertyEditor(ResourcePatternResolver resourcePatternResolver) { + this(resourcePatternResolver, null, true); + } + + /** + * Create a new ResourceArrayPropertyEditor with the given {@link ResourcePatternResolver} + * and a {@link StandardEnvironment}. + * @param resourcePatternResolver the ResourcePatternResolver to use + * @param ignoreUnresolvablePlaceholders whether to ignore unresolvable placeholders + * if no corresponding system property could be found + * @deprecated as of 3.1 in favor of {@link #ResourceArrayPropertyEditor(ResourcePatternResolver, PropertyResolver, boolean)} + */ + @Deprecated + public ResourceArrayPropertyEditor(ResourcePatternResolver resourcePatternResolver, boolean ignoreUnresolvablePlaceholders) { + this(resourcePatternResolver, null, ignoreUnresolvablePlaceholders); + } + + /** + * Create a new ResourceArrayPropertyEditor with the given {@link ResourcePatternResolver} + * and {@link PropertyResolver} (typically an {@link Environment}). + * @param resourcePatternResolver the ResourcePatternResolver to use + * @param propertyResolver the PropertyResolver to use + */ + public ResourceArrayPropertyEditor(ResourcePatternResolver resourcePatternResolver, PropertyResolver propertyResolver) { + this(resourcePatternResolver, propertyResolver, true); + } + + /** + * Create a new ResourceArrayPropertyEditor with the given {@link ResourcePatternResolver} + * and {@link PropertyResolver} (typically an {@link Environment}). + * @param resourcePatternResolver the ResourcePatternResolver to use + * @param propertyResolver the PropertyResolver to use + * @param ignoreUnresolvablePlaceholders whether to ignore unresolvable placeholders + * if no corresponding system property could be found + */ + public ResourceArrayPropertyEditor(ResourcePatternResolver resourcePatternResolver, + PropertyResolver propertyResolver, boolean ignoreUnresolvablePlaceholders) { + + Assert.notNull(resourcePatternResolver, "ResourcePatternResolver must not be null"); + this.resourcePatternResolver = resourcePatternResolver; + this.propertyResolver = propertyResolver; + this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + } + + + /** + * Treat the given text as a location pattern and convert it to a Resource array. + */ + @Override + public void setAsText(String text) { + String pattern = resolvePath(text).trim(); + try { + setValue(this.resourcePatternResolver.getResources(pattern)); + } + catch (IOException ex) { + throw new IllegalArgumentException( + "Could not resolve resource location pattern [" + pattern + "]: " + ex.getMessage()); + } + } + + /** + * Treat the given value as a collection or array and convert it to a Resource array. + * Considers String elements as location patterns and takes Resource elements as-is. + */ + @Override + public void setValue(Object value) throws IllegalArgumentException { + if (value instanceof Collection || (value instanceof Object[] && !(value instanceof Resource[]))) { + Collection<?> input = (value instanceof Collection ? (Collection<?>) value : Arrays.asList((Object[]) value)); + List<Resource> merged = new ArrayList<Resource>(); + for (Object element : input) { + if (element instanceof String) { + // A location pattern: resolve it into a Resource array. + // Might point to a single resource or to multiple resources. + String pattern = resolvePath((String) element).trim(); + try { + Resource[] resources = this.resourcePatternResolver.getResources(pattern); + for (Resource resource : resources) { + if (!merged.contains(resource)) { + merged.add(resource); + } + } + } + catch (IOException ex) { + // ignore - might be an unresolved placeholder or non-existing base directory + if (logger.isDebugEnabled()) { + logger.debug("Could not retrieve resources for pattern '" + pattern + "'", ex); + } + } + } + else if (element instanceof Resource) { + // A Resource object: add it to the result. + Resource resource = (Resource) element; + if (!merged.contains(resource)) { + merged.add(resource); + } + } + else { + throw new IllegalArgumentException("Cannot convert element [" + element + "] to [" + + Resource.class.getName() + "]: only location String and Resource object supported"); + } + } + super.setValue(merged.toArray(new Resource[merged.size()])); + } + + else { + // An arbitrary value: probably a String or a Resource array. + // setAsText will be called for a String; a Resource array will be used as-is. + super.setValue(value); + } + } + + /** + * Resolve the given path, replacing placeholders with + * corresponding system property values if necessary. + * @param path the original file path + * @return the resolved file path + * @see PropertyResolver#resolvePlaceholders + * @see PropertyResolver#resolveRequiredPlaceholders(String) + */ + protected String resolvePath(String path) { + if (this.propertyResolver == null) { + this.propertyResolver = new StandardEnvironment(); + } + return (this.ignoreUnresolvablePlaceholders ? this.propertyResolver.resolvePlaceholders(path) : + this.propertyResolver.resolveRequiredPlaceholders(path)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternResolver.java new file mode 100644 index 00000000..2bbe19cc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternResolver.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2007 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.io.Resource; +import org.springframework.core.io.ResourceLoader; + +/** + * Strategy interface for resolving a location pattern (for example, + * an Ant-style path pattern) into Resource objects. + * + * <p>This is an extension to the {@link org.springframework.core.io.ResourceLoader} + * interface. A passed-in ResourceLoader (for example, an + * {@link org.springframework.context.ApplicationContext} passed in via + * {@link org.springframework.context.ResourceLoaderAware} when running in a context) + * can be checked whether it implements this extended interface too. + * + * <p>{@link PathMatchingResourcePatternResolver} is a standalone implementation + * that is usable outside an ApplicationContext, also used by + * {@link ResourceArrayPropertyEditor} for populating Resource array bean properties. + * + * <p>Can be used with any sort of location pattern (e.g. "/WEB-INF/*-context.xml"): + * Input patterns have to match the strategy implementation. This interface just + * specifies the conversion method rather than a specific pattern format. + * + * <p>This interface also suggests a new resource prefix "classpath*:" for all + * matching resources from the class path. Note that the resource location is + * expected to be a path without placeholders in this case (e.g. "/beans.xml"); + * JAR files or classes directories can contain multiple files of the same name. + * + * @author Juergen Hoeller + * @since 1.0.2 + * @see org.springframework.core.io.Resource + * @see org.springframework.core.io.ResourceLoader + * @see org.springframework.context.ApplicationContext + * @see org.springframework.context.ResourceLoaderAware + */ +public interface ResourcePatternResolver extends ResourceLoader { + + /** + * Pseudo URL prefix for all matching resources from the class path: "classpath*:" + * This differs from ResourceLoader's classpath URL prefix in that it + * retrieves all matching resources for a given name (e.g. "/beans.xml"), + * for example in the root of all deployed JAR files. + * @see org.springframework.core.io.ResourceLoader#CLASSPATH_URL_PREFIX + */ + String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; + + /** + * Resolve the given location pattern into Resource objects. + * <p>Overlapping resource entries that point to the same physical + * resource should be avoided, as far as possible. The result should + * have set semantics. + * @param locationPattern the location pattern to resolve + * @return the corresponding Resource objects + * @throws IOException in case of I/O errors + */ + Resource[] getResources(String locationPattern) 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 new file mode 100644 index 00000000..9c0c12ee --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternUtils.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2012 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.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. + * + * <p>Callers will usually assume that a location is a relative path + * if the {@link #isUrl(String)} method returns {@code false}. + * + * @author Juergen Hoeller + * @since 1.2.3 + */ +public abstract class ResourcePatternUtils { + + /** + * Return whether the given resource location is a URL: either a + * special "classpath" or "classpath*" pseudo URL or a standard URL. + * @param resourceLocation the location String to check + * @return whether the location qualifies as a URL + * @see ResourcePatternResolver#CLASSPATH_ALL_URL_PREFIX + * @see org.springframework.util.ResourceUtils#CLASSPATH_URL_PREFIX + * @see org.springframework.util.ResourceUtils#isUrl(String) + * @see java.net.URL + */ + public static boolean isUrl(String resourceLocation) { + return (resourceLocation != null && + (resourceLocation.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX) || + ResourceUtils.isUrl(resourceLocation))); + } + + /** + * Return a default ResourcePatternResolver for the given ResourceLoader. + * <p>This might be the ResourceLoader itself, if it implements the + * ResourcePatternResolver extension, or a PathMatchingResourcePatternResolver + * built on the given ResourceLoader. + * @param resourceLoader the ResourceLoader to build a pattern resolver for + * (may be {@code null} to indicate a default ResourceLoader) + * @return the ResourcePatternResolver + * @see PathMatchingResourcePatternResolver + */ + public static ResourcePatternResolver getResourcePatternResolver(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + if (resourceLoader instanceof ResourcePatternResolver) { + return (ResourcePatternResolver) resourceLoader; + } + else if (resourceLoader != null) { + return new PathMatchingResourcePatternResolver(resourceLoader); + } + else { + return new PathMatchingResourcePatternResolver(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertySource.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertySource.java new file mode 100644 index 00000000..d0795545 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertySource.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2013 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 java.util.Properties; + +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; + +/** + * Subclass of {@link PropertiesPropertySource} that loads a {@link Properties} + * object from a given {@link org.springframework.core.io.Resource} or resource location such as + * {@code "classpath:/com/myco/foo.properties"} or {@code "file:/path/to/file.xml"}. + * Both traditional and XML-based properties file formats are supported, however in order + * for XML processing to take effect, the underlying {@code Resource}'s + * {@link org.springframework.core.io.Resource#getFilename() getFilename()} method must + * return non-{@code null} and end in ".xml". + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public class ResourcePropertySource extends PropertiesPropertySource { + + /** + * Create a PropertySource having the given name based on Properties + * loaded from the given encoded resource. + */ + public ResourcePropertySource(String name, EncodedResource resource) throws IOException { + super(name, PropertiesLoaderUtils.loadProperties(resource)); + } + + /** + * Create a PropertySource based on Properties loaded from the given resource. + * The name of the PropertySource will be generated based on the + * {@link Resource#getDescription() description} of the given resource. + */ + public ResourcePropertySource(EncodedResource resource) throws IOException { + this(getNameForResource(resource.getResource()), resource); + } + + /** + * Create a PropertySource having the given name based on Properties + * loaded from the given encoded resource. + */ + public ResourcePropertySource(String name, Resource resource) throws IOException { + super(name, PropertiesLoaderUtils.loadProperties(new EncodedResource(resource))); + } + + /** + * Create a PropertySource based on Properties loaded from the given resource. + * The name of the PropertySource will be generated based on the + * {@link Resource#getDescription() description} of the given resource. + */ + public ResourcePropertySource(Resource resource) throws IOException { + this(getNameForResource(resource), resource); + } + + /** + * Create a PropertySource having the given name based on Properties loaded from + * the given resource location and using the given class loader to load the + * resource (assuming it is prefixed with {@code classpath:}). + */ + public ResourcePropertySource(String name, String location, ClassLoader classLoader) throws IOException { + this(name, new DefaultResourceLoader(classLoader).getResource(location)); + } + + /** + * Create a PropertySource based on Properties loaded from the given resource + * location and use the given class loader to load the resource, assuming it is + * prefixed with {@code classpath:}. The name of the PropertySource will be + * generated based on the {@link Resource#getDescription() description} of the + * resource. + */ + public ResourcePropertySource(String location, ClassLoader classLoader) throws IOException { + this(new DefaultResourceLoader(classLoader).getResource(location)); + } + + /** + * Create a PropertySource having the given name based on Properties loaded from + * the given resource location. The default thread context class loader will be + * used to load the resource (assuming the location string is prefixed with + * {@code classpath:}. + */ + public ResourcePropertySource(String name, String location) throws IOException { + this(name, new DefaultResourceLoader().getResource(location)); + } + + /** + * Create a PropertySource based on Properties loaded from the given resource + * location. The name of the PropertySource will be generated based on the + * {@link Resource#getDescription() description} of the resource. + */ + public ResourcePropertySource(String location) throws IOException { + this(new DefaultResourceLoader().getResource(location)); + } + + + /** + * Return the description string for the resource, and if empty returns + * the class name of the resource plus its identity hash code. + */ + private static String getNameForResource(Resource resource) { + String name = resource.getDescription(); + if (!StringUtils.hasText(name)) { + name = resource.getClass().getSimpleName() + "@" + System.identityHashCode(resource); + } + return name; + } + +} 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 new file mode 100644 index 00000000..91257165 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2014 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 java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.OrderComparator; +import org.springframework.core.io.UrlResource; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * General purpose factory loading mechanism for internal use within the framework. + * + * <p>The {@code SpringFactoriesLoader} loads and instantiates factories of a given type + * from "META-INF/spring.factories" files. The file should be in {@link Properties} format, + * where the key is the fully qualified interface or abstract class name, and the value + * is a comma-separated list of implementation class names. For instance: + * + * <pre class="code">example.MyService=example.MyServiceImpl1,example.MyServiceImpl2</pre> + * + * where {@code MyService} is the name of the interface, and {@code MyServiceImpl1} and + * {@code MyServiceImpl2} are the two implementations. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.2 + */ +public abstract class SpringFactoriesLoader { + + /** The location to look for the factories. Can be present in multiple JAR files. */ + private static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; + + private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class); + + + /** + * Load the factory implementations of the given type from the default location, + * using the given class loader. + * <p>The returned factories are ordered in accordance with the {@link OrderComparator}. + * @param factoryClass the interface or abstract class representing the factory + * @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default) + */ + public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) { + Assert.notNull(factoryClass, "'factoryClass' must not be null"); + ClassLoader classLoaderToUse = classLoader; + if (classLoaderToUse == null) { + classLoaderToUse = SpringFactoriesLoader.class.getClassLoader(); + } + List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse); + if (logger.isTraceEnabled()) { + logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames); + } + List<T> result = new ArrayList<T>(factoryNames.size()); + for (String factoryName : factoryNames) { + result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse)); + } + OrderComparator.sort(result); + return result; + } + + public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) { + String factoryClassName = factoryClass.getName(); + try { + Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : + ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); + List<String> result = new ArrayList<String>(); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); + String factoryClassNames = properties.getProperty(factoryClassName); + result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); + } + return result; + } + catch (IOException ex) { + throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + + "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); + } + } + + @SuppressWarnings("unchecked") + private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) { + try { + Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader); + if (!factoryClass.isAssignableFrom(instanceClass)) { + throw new IllegalArgumentException( + "Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]"); + } + return (T) instanceClass.newInstance(); + } + catch (Throwable ex) { + throw new IllegalArgumentException("Cannot instantiate factory class: " + factoryClass.getName(), ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/VfsPatternUtils.java b/spring-core/src/main/java/org/springframework/core/io/support/VfsPatternUtils.java new file mode 100644 index 00000000..f6e523ea --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/VfsPatternUtils.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2010 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 java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.net.URL; + +import org.springframework.core.io.VfsUtils; + +/** + * Artificial class used for accessing the {@link VfsUtils} methods + * without exposing them to the entire world. + * + * @author Costin Leau + * @since 3.0.3 + */ +abstract class VfsPatternUtils extends VfsUtils { + + static Object getVisitorAttribute() { + return doGetVisitorAttribute(); + } + + static String getPath(Object resource) { + return doGetPath(resource); + } + + static Object findRoot(URL url) throws IOException { + return getRoot(url); + } + + static void visit(Object resource, InvocationHandler visitor) throws IOException { + Object visitorProxy = Proxy.newProxyInstance(VIRTUAL_FILE_VISITOR_INTERFACE.getClassLoader(), + new Class<?>[] { VIRTUAL_FILE_VISITOR_INTERFACE }, visitor); + invokeVfsMethod(VIRTUAL_FILE_METHOD_VISIT, resource, visitorProxy); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/package-info.java b/spring-core/src/main/java/org/springframework/core/io/support/package-info.java new file mode 100644 index 00000000..dc3e6b98 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * Support classes for Spring's resource abstraction. + * Includes a ResourcePatternResolver mechanism. + * + */ +package org.springframework.core.io.support; + diff --git a/spring-core/src/main/java/org/springframework/core/package-info.java b/spring-core/src/main/java/org/springframework/core/package-info.java new file mode 100644 index 00000000..9b2e8458 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * Provides basic classes for exception handling and version detection, + * and other core helpers that are not specific to any part of the framework. + * + */ +package org.springframework.core; + diff --git a/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java b/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java new file mode 100644 index 00000000..f61f697d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; + +import org.springframework.core.NestedIOException; + +/** + * Deserializer that reads an input stream using Java Serialization. + * + * @author Gary Russell + * @author Mark Fisher + * @since 3.0.5 + */ +public class DefaultDeserializer implements Deserializer<Object> { + + /** + * Reads the input stream and deserializes into an object. + */ + public Object deserialize(InputStream inputStream) throws IOException { + ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); + try { + return objectInputStream.readObject(); + } + catch (ClassNotFoundException ex) { + throw new NestedIOException("Failed to deserialize object type", ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/DefaultSerializer.java b/spring-core/src/main/java/org/springframework/core/serializer/DefaultSerializer.java new file mode 100644 index 00000000..04fe8bed --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/DefaultSerializer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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; + +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; + +/** + * Serializer that writes an object to an output stream using Java Serialization. + * + * @author Gary Russell + * @author Mark Fisher + * @since 3.0.5 + */ +public class DefaultSerializer implements Serializer<Object> { + + /** + * Writes the source object to an output stream using Java Serialization. + * The source object must implement {@link Serializable}. + */ + public void serialize(Object object, OutputStream outputStream) throws IOException { + if (!(object instanceof Serializable)) { + throw new IllegalArgumentException(getClass().getSimpleName() + " requires a Serializable payload " + + "but received an object of type [" + object.getClass().getName() + "]"); + } + ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); + objectOutputStream.writeObject(object); + objectOutputStream.flush(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/Deserializer.java b/spring-core/src/main/java/org/springframework/core/serializer/Deserializer.java new file mode 100644 index 00000000..f64fbdf7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/Deserializer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2012 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; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A strategy interface for converting from data in an InputStream to an Object. + * + * @author Gary Russell + * @author Mark Fisher + * @since 3.0.5 + */ +public interface Deserializer<T> { + + /** + * Read (assemble) an object of type T from the given InputStream. + * <p>Note: Implementations should not close the given InputStream + * (or any decorators of that InputStream) but rather leave this up + * to the caller. + * @param inputStream the input stream + * @return the deserialized object + * @throws IOException in case of errors reading from the stream + */ + T deserialize(InputStream inputStream) throws IOException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/Serializer.java b/spring-core/src/main/java/org/springframework/core/serializer/Serializer.java new file mode 100644 index 00000000..8af65195 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/Serializer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2012 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; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A strategy interface for streaming an object to an OutputStream. + * + * @author Gary Russell + * @author Mark Fisher + * @since 3.0.5 + */ +public interface Serializer<T> { + + /** + * Write an object of type T to the given OutputStream. + * <p>Note: Implementations should not close the given OutputStream + * (or any decorators of that OutputStream) but rather leave this up + * to the caller. + * @param object the object to serialize + * @param outputStream the output stream + * @throws IOException in case of errors writing to the stream + */ + void serialize(T object, OutputStream outputStream) throws IOException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/package-info.java b/spring-core/src/main/java/org/springframework/core/serializer/package-info.java new file mode 100644 index 00000000..36a79f41 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/package-info.java @@ -0,0 +1,10 @@ + +/** + * + * Root package for Spring's serializer interfaces and implementations. + * Provides an abstraction over various serialization techniques. + * Includes exceptions for serialization and deserialization failures. + * + */ +package org.springframework.core.serializer; + diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java b/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java new file mode 100644 index 00000000..20020611 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2010 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.ByteArrayInputStream; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.serializer.DefaultDeserializer; +import org.springframework.core.serializer.Deserializer; +import org.springframework.util.Assert; + +/** + * A {@link Converter} that delegates to a {@link org.springframework.core.serializer.Deserializer} + * to convert data in a byte array to an object. + * + * @author Gary Russell + * @author Mark Fisher + * @since 3.0.5 + */ +public class DeserializingConverter implements Converter<byte[], Object> { + + private final Deserializer<Object> deserializer; + + + /** + * Create a default DeserializingConverter that uses standard Java deserialization. + */ + public DeserializingConverter() { + this.deserializer = new DefaultDeserializer(); + } + + /** + * Create a DeserializingConverter that delegates to the provided {@link Deserializer}. + */ + public DeserializingConverter(Deserializer<Object> deserializer) { + Assert.notNull(deserializer, "Deserializer must not be null"); + this.deserializer = deserializer; + } + + + public Object convert(byte[] source) { + ByteArrayInputStream byteStream = new ByteArrayInputStream(source); + try { + return this.deserializer.deserialize(byteStream); + } + catch (Throwable ex) { + throw new SerializationFailedException("Failed to deserialize payload. " + + "Is the byte array a result of corresponding serialization for " + + this.deserializer.getClass().getSimpleName() + "?", ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/SerializationFailedException.java b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializationFailedException.java new file mode 100644 index 00000000..274a6cb0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializationFailedException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2012 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 org.springframework.core.NestedRuntimeException; + +/** + * Wrapper for the native IOException (or similar) when a + * {@link org.springframework.core.serializer.Serializer} or + * {@link org.springframework.core.serializer.Deserializer} failed. + * Thrown by {@link SerializingConverter} and {@link DeserializingConverter}. + * + * @author Gary Russell + * @author Juergen Hoeller + * @since 3.0.5 + */ +@SuppressWarnings("serial") +public class SerializationFailedException extends NestedRuntimeException { + + /** + * Construct a {@code SerializationException} with the specified detail message. + * @param message the detail message + */ + public SerializationFailedException(String message) { + super(message); + } + + /** + * Construct a {@code SerializationException} with the specified detail message + * and nested exception. + * @param message the detail message + * @param cause the nested exception + */ + public SerializationFailedException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/SerializingConverter.java b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializingConverter.java new file mode 100644 index 00000000..b9909f61 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializingConverter.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2014 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.ByteArrayOutputStream; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.serializer.DefaultSerializer; +import org.springframework.core.serializer.Serializer; +import org.springframework.util.Assert; + +/** + * A {@link Converter} that delegates to a {@link org.springframework.core.serializer.Serializer} + * to convert an object to a byte array. + * + * @author Gary Russell + * @author Mark Fisher + * @since 3.0.5 + */ +public class SerializingConverter implements Converter<Object, byte[]> { + + private final Serializer<Object> serializer; + + + /** + * Create a default SerializingConverter that uses standard Java serialization. + */ + public SerializingConverter() { + this.serializer = new DefaultSerializer(); + } + + /** + * Create a SerializingConverter that delegates to the provided {@link Serializer} + */ + public SerializingConverter(Serializer<Object> serializer) { + Assert.notNull(serializer, "Serializer must not be null"); + this.serializer = serializer; + } + + + /** + * Serializes the source object and returns the byte array result. + */ + public byte[] convert(Object source) { + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(256); + try { + this.serializer.serialize(source, byteStream); + return byteStream.toByteArray(); + } + catch (Throwable ex) { + throw new SerializationFailedException("Failed to serialize object using " + + this.serializer.getClass().getSimpleName(), ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/package-info.java b/spring-core/src/main/java/org/springframework/core/serializer/support/package-info.java new file mode 100644 index 00000000..33f7365f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * Support classes for Spring's serializer abstraction. + * Includes adapters to the Converter SPI. + * + */ +package org.springframework.core.serializer.support; + diff --git a/spring-core/src/main/java/org/springframework/core/style/DefaultToStringStyler.java b/spring-core/src/main/java/org/springframework/core/style/DefaultToStringStyler.java new file mode 100644 index 00000000..f81cb4a8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/DefaultToStringStyler.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2012 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.style; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Spring's default {@code toString()} styler. + * + * <p>This class is used by {@link ToStringCreator} to style {@code toString()} + * output in a consistent manner according to Spring conventions. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + */ +public class DefaultToStringStyler implements ToStringStyler { + + private final ValueStyler valueStyler; + + + /** + * Create a new DefaultToStringStyler. + * @param valueStyler the ValueStyler to use + */ + public DefaultToStringStyler(ValueStyler valueStyler) { + Assert.notNull(valueStyler, "ValueStyler must not be null"); + this.valueStyler = valueStyler; + } + + /** + * Return the ValueStyler used by this ToStringStyler. + */ + protected final ValueStyler getValueStyler() { + return this.valueStyler; + } + + + public void styleStart(StringBuilder buffer, Object obj) { + if (!obj.getClass().isArray()) { + buffer.append('[').append(ClassUtils.getShortName(obj.getClass())); + styleIdentityHashCode(buffer, obj); + } + else { + buffer.append('['); + styleIdentityHashCode(buffer, obj); + buffer.append(' '); + styleValue(buffer, obj); + } + } + + private void styleIdentityHashCode(StringBuilder buffer, Object obj) { + buffer.append('@'); + buffer.append(ObjectUtils.getIdentityHexString(obj)); + } + + public void styleEnd(StringBuilder buffer, Object o) { + buffer.append(']'); + } + + public void styleField(StringBuilder buffer, String fieldName, Object value) { + styleFieldStart(buffer, fieldName); + styleValue(buffer, value); + styleFieldEnd(buffer, fieldName); + } + + protected void styleFieldStart(StringBuilder buffer, String fieldName) { + buffer.append(' ').append(fieldName).append(" = "); + } + + protected void styleFieldEnd(StringBuilder buffer, String fieldName) { + } + + public void styleValue(StringBuilder buffer, Object value) { + buffer.append(this.valueStyler.style(value)); + } + + public void styleFieldSeparator(StringBuilder buffer) { + buffer.append(','); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java b/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java new file mode 100644 index 00000000..2e6251c0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2012 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.style; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Converts objects to String form, generally for debugging purposes, + * using Spring's {@code toString} styling conventions. + * + * <p>Uses the reflective visitor pattern underneath the hood to nicely + * encapsulate styling algorithms for each type of styled object. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + */ +public class DefaultValueStyler implements ValueStyler { + + private static final String EMPTY = "[empty]"; + private static final String NULL = "[null]"; + private static final String COLLECTION = "collection"; + private static final String SET = "set"; + private static final String LIST = "list"; + private static final String MAP = "map"; + private static final String ARRAY = "array"; + + + public String style(Object value) { + if (value == null) { + return NULL; + } + else if (value instanceof String) { + return "\'" + value + "\'"; + } + else if (value instanceof Class) { + return ClassUtils.getShortName((Class) value); + } + else if (value instanceof Method) { + Method method = (Method) value; + return method.getName() + "@" + ClassUtils.getShortName(method.getDeclaringClass()); + } + else if (value instanceof Map) { + return style((Map) value); + } + else if (value instanceof Map.Entry) { + return style((Map.Entry) value); + } + else if (value instanceof Collection) { + return style((Collection) value); + } + else if (value.getClass().isArray()) { + return styleArray(ObjectUtils.toObjectArray(value)); + } + else { + return String.valueOf(value); + } + } + + private String style(Map value) { + StringBuilder result = new StringBuilder(value.size() * 8 + 16); + result.append(MAP + "["); + for (Iterator it = value.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + result.append(style(entry)); + if (it.hasNext()) { + result.append(',').append(' '); + } + } + if (value.isEmpty()) { + result.append(EMPTY); + } + result.append("]"); + return result.toString(); + } + + private String style(Map.Entry value) { + return style(value.getKey()) + " -> " + style(value.getValue()); + } + + private String style(Collection value) { + StringBuilder result = new StringBuilder(value.size() * 8 + 16); + result.append(getCollectionTypeString(value)).append('['); + for (Iterator i = value.iterator(); i.hasNext();) { + result.append(style(i.next())); + if (i.hasNext()) { + result.append(',').append(' '); + } + } + if (value.isEmpty()) { + result.append(EMPTY); + } + result.append("]"); + return result.toString(); + } + + private String getCollectionTypeString(Collection value) { + if (value instanceof List) { + return LIST; + } + else if (value instanceof Set) { + return SET; + } + else { + return COLLECTION; + } + } + + private String styleArray(Object[] array) { + StringBuilder result = new StringBuilder(array.length * 8 + 16); + result.append(ARRAY + "<").append(ClassUtils.getShortName(array.getClass().getComponentType())).append(">["); + for (int i = 0; i < array.length - 1; i++) { + result.append(style(array[i])); + result.append(',').append(' '); + } + if (array.length > 0) { + result.append(style(array[array.length - 1])); + } + else { + result.append(EMPTY); + } + result.append("]"); + return result.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/style/StylerUtils.java b/spring-core/src/main/java/org/springframework/core/style/StylerUtils.java new file mode 100644 index 00000000..91e09dee --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/StylerUtils.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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.style; + +/** + * Simple utility class to allow for convenient access to value + * styling logic, mainly to support descriptive logging messages. + * + * <p>For more sophisticated needs, use the {@link ValueStyler} abstraction + * directly. This class simply uses a shared {@link DefaultValueStyler} + * instance underneath. + * + * @author Keith Donald + * @since 1.2.2 + * @see ValueStyler + * @see DefaultValueStyler + */ +public abstract class StylerUtils { + + /** + * Default ValueStyler instance used by the {@code style} method. + * Also available for the {@link ToStringCreator} class in this package. + */ + static final ValueStyler DEFAULT_VALUE_STYLER = new DefaultValueStyler(); + + /** + * Style the specified value according to default conventions. + * @param value the Object value to style + * @return the styled String + * @see DefaultValueStyler + */ + public static String style(Object value) { + return DEFAULT_VALUE_STYLER.style(value); + } + +} 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 new file mode 100644 index 00000000..6f682939 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/ToStringCreator.java @@ -0,0 +1,189 @@ +/* + * Copyright 2002-2014 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.style; + +import org.springframework.util.Assert; + +/** + * Utility class that builds pretty-printing {@code toString()} methods + * with pluggable styling conventions. By default, ToStringCreator adheres + * to Spring's {@code toString()} styling conventions. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + */ +public class ToStringCreator { + + /** + * Default ToStringStyler instance used by this ToStringCreator. + */ + private static final ToStringStyler DEFAULT_TO_STRING_STYLER = + new DefaultToStringStyler(StylerUtils.DEFAULT_VALUE_STYLER); + + + private final StringBuilder buffer = new StringBuilder(256); + + private final ToStringStyler styler; + + private final Object object; + + private boolean styledFirstField; + + + /** + * Create a ToStringCreator for the given object. + * @param obj the object to be stringified + */ + public ToStringCreator(Object obj) { + this(obj, (ToStringStyler) null); + } + + /** + * Create a ToStringCreator for the given object, using the provided style. + * @param obj the object to be stringified + * @param styler the ValueStyler encapsulating pretty-print instructions + */ + public ToStringCreator(Object obj, ValueStyler styler) { + this(obj, new DefaultToStringStyler(styler != null ? styler : StylerUtils.DEFAULT_VALUE_STYLER)); + } + + /** + * Create a ToStringCreator for the given object, using the provided style. + * @param obj the object to be stringified + * @param styler the ToStringStyler encapsulating pretty-print instructions + */ + public ToStringCreator(Object obj, ToStringStyler styler) { + Assert.notNull(obj, "The object to be styled must not be null"); + this.object = obj; + this.styler = (styler != null ? styler : DEFAULT_TO_STRING_STYLER); + this.styler.styleStart(this.buffer, this.object); + } + + + /** + * Append a byte field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, byte value) { + return append(fieldName, new Byte(value)); + } + + /** + * Append a short field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, short value) { + return append(fieldName, new Short(value)); + } + + /** + * Append a integer field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, int value) { + return append(fieldName, new Integer(value)); + } + + /** + * Append a long field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, long value) { + return append(fieldName, new Long(value)); + } + + /** + * Append a float field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, float value) { + return append(fieldName, new Float(value)); + } + + /** + * Append a double field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, double value) { + return append(fieldName, new Double(value)); + } + + /** + * Append a boolean field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, boolean value) { + return append(fieldName, Boolean.valueOf(value)); + } + + /** + * Append a field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, Object value) { + printFieldSeparatorIfNecessary(); + this.styler.styleField(this.buffer, fieldName, value); + return this; + } + + private void printFieldSeparatorIfNecessary() { + if (this.styledFirstField) { + this.styler.styleFieldSeparator(this.buffer); + } + else { + this.styledFirstField = true; + } + } + + /** + * Append the provided value. + * @param value The value to append + * @return this, to support call-chaining. + */ + public ToStringCreator append(Object value) { + this.styler.styleValue(this.buffer, value); + return this; + } + + + /** + * Return the String representation that this ToStringCreator built. + */ + @Override + public String toString() { + this.styler.styleEnd(this.buffer, this.object); + return this.buffer.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/style/ToStringStyler.java b/spring-core/src/main/java/org/springframework/core/style/ToStringStyler.java new file mode 100644 index 00000000..9ae12df5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/ToStringStyler.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2012 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.style; + +/** + * A strategy interface for pretty-printing {@code toString()} methods. + * Encapsulates the print algorithms; some other object such as a builder + * should provide the workflow. + * + * @author Keith Donald + * @since 1.2.2 + */ +public interface ToStringStyler { + + /** + * Style a {@code toString()}'ed object before its fields are styled. + * @param buffer the buffer to print to + * @param obj the object to style + */ + void styleStart(StringBuilder buffer, Object obj); + + /** + * Style a {@code toString()}'ed object after it's fields are styled. + * @param buffer the buffer to print to + * @param obj the object to style + */ + void styleEnd(StringBuilder buffer, Object obj); + + /** + * Style a field value as a string. + * @param buffer the buffer to print to + * @param fieldName the he name of the field + * @param value the field value + */ + void styleField(StringBuilder buffer, String fieldName, Object value); + + /** + * Style the given value. + * @param buffer the buffer to print to + * @param value the field value + */ + void styleValue(StringBuilder buffer, Object value); + + /** + * Style the field separator. + * @param buffer buffer to print to + */ + void styleFieldSeparator(StringBuilder buffer); + +} diff --git a/spring-core/src/main/java/org/springframework/core/style/ValueStyler.java b/spring-core/src/main/java/org/springframework/core/style/ValueStyler.java new file mode 100644 index 00000000..731fb3ae --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/ValueStyler.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2007 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.style; + +/** + * Strategy that encapsulates value String styling algorithms + * according to Spring conventions. + * + * @author Keith Donald + * @since 1.2.2 + */ +public interface ValueStyler { + + /** + * Style the given value, returning a String representation. + * @param value the Object value to style + * @return the styled String + */ + String style(Object value); + +} diff --git a/spring-core/src/main/java/org/springframework/core/style/package-info.java b/spring-core/src/main/java/org/springframework/core/style/package-info.java new file mode 100644 index 00000000..a88ee998 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * Support for styling values as Strings, with ToStringCreator as central class. + * + */ +package org.springframework.core.style; + diff --git a/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java new file mode 100644 index 00000000..f650e1d5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2013 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; + +import java.util.concurrent.Callable; +import java.util.concurrent.Future; + +/** + * Extended interface for asynchronous {@link TaskExecutor} implementations, + * offering an overloaded {@link #execute(Runnable, long)} variant with a start + * timeout parameter as well support for {@link java.util.concurrent.Callable}. + * + * <p>Note: The {@link java.util.concurrent.Executors} class includes a set of + * methods that can convert some other common closure-like objects, for example, + * {@link java.security.PrivilegedAction} to {@link Callable} before executing them. + * + * <p>Implementing this interface also indicates that the {@link #execute(Runnable)} + * method will not execute its Runnable in the caller's thread but rather + * asynchronously in some other thread. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see SimpleAsyncTaskExecutor + * @see org.springframework.scheduling.SchedulingTaskExecutor + * @see java.util.concurrent.Callable + * @see java.util.concurrent.Executors + */ +public interface AsyncTaskExecutor extends TaskExecutor { + + /** Constant that indicates immediate execution */ + long TIMEOUT_IMMEDIATE = 0; + + /** Constant that indicates no time limit */ + long TIMEOUT_INDEFINITE = Long.MAX_VALUE; + + + /** + * Execute the given {@code task}. + * @param task the {@code Runnable} to execute (never {@code null}) + * @param startTimeout the time duration (milliseconds) within which the task is + * supposed to start. This is intended as a hint to the executor, allowing for + * preferred handling of immediate tasks. Typical values are {@link #TIMEOUT_IMMEDIATE} + * or {@link #TIMEOUT_INDEFINITE} (the default as used by {@link #execute(Runnable)}). + * @throws TaskTimeoutException in case of the task being rejected because + * of the timeout (i.e. it cannot be started in time) + * @throws TaskRejectedException if the given task was not accepted + */ + void execute(Runnable task, long startTimeout); + + /** + * Submit a Runnable task for execution, receiving a Future representing that task. + * The Future will return a {@code null} result upon completion. + * @param task the {@code Runnable} to execute (never {@code null}) + * @return a Future representing pending completion of the task + * @throws TaskRejectedException if the given task was not accepted + * @since 3.0 + */ + Future<?> submit(Runnable task); + + /** + * Submit a Callable task for execution, receiving a Future representing that task. + * The Future will return the Callable's result upon completion. + * @param task the {@code Callable} to execute (never {@code null}) + * @return a Future representing pending completion of the task + * @throws TaskRejectedException if the given task was not accepted + * @since 3.0 + */ + <T> Future<T> submit(Callable<T> task); + +} 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 new file mode 100644 index 00000000..f6b71400 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java @@ -0,0 +1,238 @@ +/* + * Copyright 2002-2012 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; + +import java.io.Serializable; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadFactory; + +import org.springframework.util.Assert; +import org.springframework.util.ConcurrencyThrottleSupport; +import org.springframework.util.CustomizableThreadCreator; + +/** + * {@link TaskExecutor} implementation that fires up a new Thread for each task, + * executing it asynchronously. + * + * <p>Supports limiting concurrent threads through the "concurrencyLimit" + * bean property. By default, the number of concurrent threads is unlimited. + * + * <p><b>NOTE: This implementation does not reuse threads!</b> Consider a + * thread-pooling TaskExecutor implementation instead, in particular for + * executing a large number of short-lived tasks. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setConcurrencyLimit + * @see SyncTaskExecutor + * @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor + * @see org.springframework.scheduling.commonj.WorkManagerTaskExecutor + */ +@SuppressWarnings("serial") +public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator implements AsyncTaskExecutor, Serializable { + + /** + * Permit any number of concurrent invocations: that is, don't throttle concurrency. + */ + public static final int UNBOUNDED_CONCURRENCY = ConcurrencyThrottleSupport.UNBOUNDED_CONCURRENCY; + + /** + * Switch concurrency 'off': that is, don't allow any concurrent invocations. + */ + public static final int NO_CONCURRENCY = ConcurrencyThrottleSupport.NO_CONCURRENCY; + + + /** Internal concurrency throttle used by this executor */ + private final ConcurrencyThrottleAdapter concurrencyThrottle = new ConcurrencyThrottleAdapter(); + + private ThreadFactory threadFactory; + + + /** + * Create a new SimpleAsyncTaskExecutor with default thread name prefix. + */ + public SimpleAsyncTaskExecutor() { + super(); + } + + /** + * Create a new SimpleAsyncTaskExecutor with the given thread name prefix. + * @param threadNamePrefix the prefix to use for the names of newly created threads + */ + public SimpleAsyncTaskExecutor(String threadNamePrefix) { + super(threadNamePrefix); + } + + /** + * Create a new SimpleAsyncTaskExecutor with the given external thread factory. + * @param threadFactory the factory to use for creating new Threads + */ + public SimpleAsyncTaskExecutor(ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + } + + + /** + * Specify an external factory to use for creating new Threads, + * instead of relying on the local properties of this executor. + * <p>You may specify an inner ThreadFactory bean or also a ThreadFactory reference + * obtained from JNDI (on a Java EE 6 server) or some other lookup mechanism. + * @see #setThreadNamePrefix + * @see #setThreadPriority + */ + public void setThreadFactory(ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + } + + /** + * Return the external factory to use for creating new Threads, if any. + */ + public final ThreadFactory getThreadFactory() { + return this.threadFactory; + } + + /** + * Set the maximum number of parallel accesses allowed. + * -1 indicates no concurrency limit at all. + * <p>In principle, this limit can be changed at runtime, + * although it is generally designed as a config time setting. + * NOTE: Do not switch between -1 and any concrete limit at runtime, + * as this will lead to inconsistent concurrency counts: A limit + * of -1 effectively turns off concurrency counting completely. + * @see #UNBOUNDED_CONCURRENCY + */ + public void setConcurrencyLimit(int concurrencyLimit) { + this.concurrencyThrottle.setConcurrencyLimit(concurrencyLimit); + } + + /** + * Return the maximum number of parallel accesses allowed. + */ + public final int getConcurrencyLimit() { + return this.concurrencyThrottle.getConcurrencyLimit(); + } + + /** + * Return whether this throttle is currently active. + * @return {@code true} if the concurrency limit for this instance is active + * @see #getConcurrencyLimit() + * @see #setConcurrencyLimit + */ + public final boolean isThrottleActive() { + return this.concurrencyThrottle.isThrottleActive(); + } + + + /** + * Executes the given task, within a concurrency throttle + * if configured (through the superclass's settings). + * @see #doExecute(Runnable) + */ + public void execute(Runnable task) { + execute(task, TIMEOUT_INDEFINITE); + } + + /** + * Executes the given task, within a concurrency throttle + * if configured (through the superclass's settings). + * <p>Executes urgent tasks (with 'immediate' timeout) directly, + * bypassing the concurrency throttle (if active). All other + * tasks are subject to throttling. + * @see #TIMEOUT_IMMEDIATE + * @see #doExecute(Runnable) + */ + public void execute(Runnable task, long startTimeout) { + Assert.notNull(task, "Runnable must not be null"); + if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) { + this.concurrencyThrottle.beforeAccess(); + doExecute(new ConcurrencyThrottlingRunnable(task)); + } + else { + doExecute(task); + } + } + + public Future<?> submit(Runnable task) { + FutureTask<Object> future = new FutureTask<Object>(task, null); + execute(future, TIMEOUT_INDEFINITE); + return future; + } + + public <T> Future<T> submit(Callable<T> task) { + FutureTask<T> future = new FutureTask<T>(task); + execute(future, TIMEOUT_INDEFINITE); + return future; + } + + /** + * Template method for the actual execution of a task. + * <p>The default implementation creates a new Thread and starts it. + * @param task the Runnable to execute + * @see #setThreadFactory + * @see #createThread + * @see java.lang.Thread#start() + */ + protected void doExecute(Runnable task) { + Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task)); + thread.start(); + } + + + /** + * Subclass of the general ConcurrencyThrottleSupport class, + * making {@code beforeAccess()} and {@code afterAccess()} + * visible to the surrounding class. + */ + private static class ConcurrencyThrottleAdapter extends ConcurrencyThrottleSupport { + + @Override + protected void beforeAccess() { + super.beforeAccess(); + } + + @Override + protected void afterAccess() { + super.afterAccess(); + } + } + + + /** + * This Runnable calls {@code afterAccess()} after the + * target Runnable has finished its execution. + */ + private class ConcurrencyThrottlingRunnable implements Runnable { + + private final Runnable target; + + public ConcurrencyThrottlingRunnable(Runnable target) { + this.target = target; + } + + public void run() { + try { + this.target.run(); + } + finally { + concurrencyThrottle.afterAccess(); + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/SyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SyncTaskExecutor.java new file mode 100644 index 00000000..47bebf46 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/SyncTaskExecutor.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2012 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; + +import java.io.Serializable; + +import org.springframework.util.Assert; + +/** + * {@link TaskExecutor} implementation that executes each task <i>synchronously</i> + * in the calling thread. + * + * <p>Mainly intended for testing scenarios. + * + * <p>Execution in the calling thread does have the advantage of participating + * in it's thread context, for example the thread context class loader or the + * thread's current transaction association. That said, in many cases, + * asynchronous execution will be preferable: choose an asynchronous + * {@code TaskExecutor} instead for such scenarios. + * + * @author Juergen Hoeller + * @since 2.0 + * @see SimpleAsyncTaskExecutor + */ +@SuppressWarnings("serial") +public class SyncTaskExecutor implements TaskExecutor, Serializable { + + /** + * Executes the given {@code task} synchronously, through direct + * invocation of it's {@link Runnable#run() run()} method. + * @throws IllegalArgumentException if the given {@code task} is {@code null} + */ + public void execute(Runnable task) { + Assert.notNull(task, "Runnable must not be null"); + task.run(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/TaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/TaskExecutor.java new file mode 100644 index 00000000..fea960e3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/TaskExecutor.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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; + +import java.util.concurrent.Executor; + +/** + * Simple task executor interface that abstracts the execution + * of a {@link Runnable}. + * + * <p>Implementations can use all sorts of different execution strategies, + * such as: synchronous, asynchronous, using a thread pool, and more. + * + * <p>Equivalent to JDK 1.5's {@link java.util.concurrent.Executor} + * interface; extending it now in Spring 3.0, so that clients may declare + * a dependency on an Executor and receive any TaskExecutor implementation. + * This interface remains separate from the standard Executor interface + * mainly for backwards compatibility with JDK 1.4 in Spring 2.x. + * + * @author Juergen Hoeller + * @since 2.0 + * @see java.util.concurrent.Executor + */ +public interface TaskExecutor extends Executor { + + /** + * Execute the given {@code task}. + * <p>The call might return immediately if the implementation uses + * an asynchronous execution strategy, or might block in the case + * of synchronous execution. + * @param task the {@code Runnable} to execute (never {@code null}) + * @throws TaskRejectedException if the given task was not accepted + */ + void execute(Runnable task); + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java b/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java new file mode 100644 index 00000000..feacf971 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2012 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; + +import java.util.concurrent.RejectedExecutionException; + +/** + * Exception thrown when a {@link TaskExecutor} rejects to accept + * a given task for execution. + * + * @author Juergen Hoeller + * @since 2.0.1 + * @see TaskExecutor#execute(Runnable) + * @see TaskTimeoutException + */ +@SuppressWarnings("serial") +public class TaskRejectedException extends RejectedExecutionException { + + /** + * Create a new {@code TaskRejectedException} + * with the specified detail message and no root cause. + * @param msg the detail message + */ + public TaskRejectedException(String msg) { + super(msg); + } + + /** + * Create a new {@code TaskRejectedException} + * with the specified detail message and the given root cause. + * @param msg the detail message + * @param cause the root cause (usually from using an underlying + * API such as the {@code java.util.concurrent} package) + * @see java.util.concurrent.RejectedExecutionException + */ + public TaskRejectedException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/TaskTimeoutException.java b/spring-core/src/main/java/org/springframework/core/task/TaskTimeoutException.java new file mode 100644 index 00000000..476658bb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/TaskTimeoutException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2012 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; + +/** + * Exception thrown when a {@link AsyncTaskExecutor} rejects to accept + * a given task for execution because of the specified timeout. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see AsyncTaskExecutor#execute(Runnable, long) + * @see TaskRejectedException + */ +@SuppressWarnings("serial") +public class TaskTimeoutException extends TaskRejectedException { + + /** + * Create a new {@code TaskTimeoutException} + * with the specified detail message and no root cause. + * @param msg the detail message + */ + public TaskTimeoutException(String msg) { + super(msg); + } + + /** + * Create a new {@code TaskTimeoutException} + * with the specified detail message and the given root cause. + * @param msg the detail message + * @param cause the root cause (usually from using an underlying + * API such as the {@code java.util.concurrent} package) + * @see java.util.concurrent.RejectedExecutionException + */ + public TaskTimeoutException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/package-info.java b/spring-core/src/main/java/org/springframework/core/task/package-info.java new file mode 100644 index 00000000..fdbf0010 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * This package defines Spring's core TaskExecutor abstraction, + * and provides SyncTaskExecutor and SimpleAsyncTaskExecutor implementations. + * + */ +package org.springframework.core.task; + diff --git a/spring-core/src/main/java/org/springframework/core/task/support/ConcurrentExecutorAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/ConcurrentExecutorAdapter.java new file mode 100644 index 00000000..2acf1a28 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/support/ConcurrentExecutorAdapter.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2009 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.support; + +import java.util.concurrent.Executor; + +import org.springframework.core.task.TaskExecutor; +import org.springframework.util.Assert; + +/** + * Adapter that exposes the {@link java.util.concurrent.Executor} interface + * for any Spring {@link org.springframework.core.task.TaskExecutor}. + * + * <p>This is less useful as of Spring 3.0, since TaskExecutor itself + * extends the Executor interface. The adapter is only relevant for + * <em>hiding</em> the TaskExecutor nature of a given object now, + * solely exposing the standard Executor interface to a client. + * + * @author Juergen Hoeller + * @since 2.5 + * @see java.util.concurrent.Executor + * @see org.springframework.core.task.TaskExecutor + */ +public class ConcurrentExecutorAdapter implements Executor { + + private final TaskExecutor taskExecutor; + + + /** + * Create a new ConcurrentExecutorAdapter for the given Spring TaskExecutor. + * @param taskExecutor the Spring TaskExecutor to wrap + */ + public ConcurrentExecutorAdapter(TaskExecutor taskExecutor) { + Assert.notNull(taskExecutor, "TaskExecutor must not be null"); + this.taskExecutor = taskExecutor; + } + + + public void execute(Runnable command) { + this.taskExecutor.execute(command); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java new file mode 100644 index 00000000..0c594b93 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2012 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.support; + +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.TimeUnit; + +import org.springframework.core.task.TaskExecutor; +import org.springframework.util.Assert; + +/** + * Adapter that takes a Spring {@link org.springframework.core.task.TaskExecutor}) + * and exposes a full {@code java.util.concurrent.ExecutorService} for it. + * + * <p>This is primarily for adapting to client components that communicate via the + * {@code java.util.concurrent.ExecutorService} API. It can also be used as + * common ground between a local Spring {@code TaskExecutor} backend and a + * JNDI-located {@code ManagedExecutorService} in a Java EE 6 environment. + * + * <p><b>NOTE:</b> This ExecutorService adapter does <em>not</em> support the + * lifecycle methods in the {@code java.util.concurrent.ExecutorService} API + * ("shutdown()" etc), similar to a server-wide {@code ManagedExecutorService} + * in a Java EE 6 environment. The lifecycle is always up to the backend pool, + * with this adapter acting as an access-only proxy for that target pool. + * + * @author Juergen Hoeller + * @since 3.0 + * @see java.util.concurrent.ExecutorService + */ +public class ExecutorServiceAdapter extends AbstractExecutorService { + + private final TaskExecutor taskExecutor; + + + /** + * Create a new ExecutorServiceAdapter, using the given target executor. + * @param taskExecutor the target executor to delegate to + */ + public ExecutorServiceAdapter(TaskExecutor taskExecutor) { + Assert.notNull(taskExecutor, "TaskExecutor must not be null"); + this.taskExecutor = taskExecutor; + } + + + public void execute(Runnable task) { + this.taskExecutor.execute(task); + } + + public void shutdown() { + throw new IllegalStateException( + "Manual shutdown not supported - ExecutorServiceAdapter is dependent on an external lifecycle"); + } + + public List<Runnable> shutdownNow() { + throw new IllegalStateException( + "Manual shutdown not supported - ExecutorServiceAdapter is dependent on an external lifecycle"); + } + + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + throw new IllegalStateException( + "Manual shutdown not supported - ExecutorServiceAdapter is dependent on an external lifecycle"); + } + + public boolean isShutdown() { + return false; + } + + public boolean isTerminated() { + return false; + } + +} 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 new file mode 100644 index 00000000..f980c4d4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2013 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.support; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.RejectedExecutionException; + +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.TaskRejectedException; +import org.springframework.util.Assert; + +/** + * Adapter that takes a JDK {@code java.util.concurrent.Executor} and + * exposes a Spring {@link org.springframework.core.task.TaskExecutor} for it. + * Also detects an extended {@code java.util.concurrent.ExecutorService}, adapting + * the {@link org.springframework.core.task.AsyncTaskExecutor} interface accordingly. + * + * @author Juergen Hoeller + * @since 3.0 + * @see java.util.concurrent.Executor + * @see java.util.concurrent.ExecutorService + * @see java.util.concurrent.Executors + */ +public class TaskExecutorAdapter implements AsyncTaskExecutor { + + private final Executor concurrentExecutor; + + + /** + * Create a new TaskExecutorAdapter, + * using the given JDK concurrent executor. + * @param concurrentExecutor the JDK concurrent executor to delegate to + */ + public TaskExecutorAdapter(Executor concurrentExecutor) { + Assert.notNull(concurrentExecutor, "Executor must not be null"); + this.concurrentExecutor = concurrentExecutor; + } + + + /** + * Delegates to the specified JDK concurrent executor. + * @see java.util.concurrent.Executor#execute(Runnable) + */ + public void execute(Runnable task) { + try { + this.concurrentExecutor.execute(task); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException( + "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + } + } + + public void execute(Runnable task, long startTimeout) { + execute(task); + } + + public Future<?> submit(Runnable task) { + try { + if (this.concurrentExecutor instanceof ExecutorService) { + return ((ExecutorService) this.concurrentExecutor).submit(task); + } + else { + FutureTask<Object> future = new FutureTask<Object>(task, null); + this.concurrentExecutor.execute(future); + return future; + } + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException( + "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + } + } + + public <T> Future<T> submit(Callable<T> task) { + try { + if (this.concurrentExecutor instanceof ExecutorService) { + return ((ExecutorService) this.concurrentExecutor).submit(task); + } + else { + FutureTask<T> future = new FutureTask<T>(task); + this.concurrentExecutor.execute(future); + return future; + } + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException( + "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/support/package-info.java b/spring-core/src/main/java/org/springframework/core/task/support/package-info.java new file mode 100644 index 00000000..b1bd9430 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/support/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * Support classes for Spring's TaskExecutor abstraction. + * Includes an adapter for the standard ExecutorService interface. + * + */ +package org.springframework.core.task.support; + diff --git a/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java b/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java new file mode 100644 index 00000000..beeb7d93 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2012 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.type; + +import java.util.Map; +import java.util.Set; + +/** + * Interface that defines abstract access to the annotations of a specific + * class, in a form that does not require that class to be loaded yet. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.5 + * @see StandardAnnotationMetadata + * @see org.springframework.core.type.classreading.MetadataReader#getAnnotationMetadata() + */ +public interface AnnotationMetadata extends ClassMetadata { + + /** + * Return the names of all annotation types defined on the underlying class. + * @return the annotation type names + */ + Set<String> getAnnotationTypes(); + + /** + * Return the names of all meta-annotation types defined on the + * given annotation type of the underlying class. + * @param annotationType the meta-annotation type to look for + * @return the meta-annotation type names + */ + Set<String> getMetaAnnotationTypes(String annotationType); + + /** + * Determine whether the underlying class has an annotation of the given + * type defined. + * @param annotationType the annotation type to look for + * @return whether a matching annotation is defined + */ + boolean hasAnnotation(String annotationType); + + /** + * Determine whether the underlying class has an annotation that + * is itself annotated with the meta-annotation of the given type. + * @param metaAnnotationType the meta-annotation type to look for + * @return whether a matching meta-annotation is defined + */ + boolean hasMetaAnnotation(String metaAnnotationType); + + /** + * Determine whether the underlying class has an annotation or + * meta-annotation of the given type defined. + * <p>This is equivalent to a "hasAnnotation || hasMetaAnnotation" + * check. If this method returns {@code true}, then + * {@link #getAnnotationAttributes} will return a non-null Map. + * @param annotationType the annotation type to look for + * @return whether a matching annotation is defined + */ + boolean isAnnotated(String annotationType); + + /** + * Retrieve the attributes of the annotation of the given type, + * if any (i.e. if defined on the underlying class, as direct + * annotation or as meta-annotation). + * @param annotationType the annotation type to look for + * @return a Map of attributes, with the attribute name as key (e.g. "value") + * and the defined attribute value as Map value. This return value will be + * {@code null} if no matching annotation is defined. + */ + Map<String, Object> getAnnotationAttributes(String annotationType); + + /** + * Retrieve the attributes of the annotation of the given type, + * if any (i.e. if defined on the underlying class, as direct + * annotation or as meta-annotation). + * @param annotationType the annotation type to look for + * @param classValuesAsString whether to convert class references to String + * class names for exposure as values in the returned Map, instead of Class + * references which might potentially have to be loaded first + * @return a Map of attributes, with the attribute name as key (e.g. "value") + * and the defined attribute value as Map value. This return value will be + * {@code null} if no matching annotation is defined. + */ + Map<String, Object> getAnnotationAttributes(String annotationType, boolean classValuesAsString); + + /** + * Determine whether the underlying class has any methods that are + * annotated (or meta-annotated) with the given annotation type. + */ + boolean hasAnnotatedMethods(String annotationType); + + /** + * Retrieve the method metadata for all methods that are annotated + * (or meta-annotated) with the given annotation type. + * <p>For any returned method, {@link MethodMetadata#isAnnotated} will + * return {@code true} for the given annotation type. + * @param annotationType the annotation type to look for + * @return a Set of {@link MethodMetadata} for methods that have a matching + * annotation. The return value will be an empty set if no methods match + * the annotation type. + */ + Set<MethodMetadata> getAnnotatedMethods(String annotationType); + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/ClassMetadata.java b/spring-core/src/main/java/org/springframework/core/type/ClassMetadata.java new file mode 100644 index 00000000..3f649713 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/ClassMetadata.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2012 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.type; + +/** + * Interface that defines abstract metadata of a specific class, + * in a form that does not require that class to be loaded yet. + * + * @author Juergen Hoeller + * @since 2.5 + * @see StandardClassMetadata + * @see org.springframework.core.type.classreading.MetadataReader#getClassMetadata() + * @see AnnotationMetadata + */ +public interface ClassMetadata { + + /** + * Return the name of the underlying class. + */ + String getClassName(); + + /** + * Return whether the underlying class represents an interface. + */ + boolean isInterface(); + + /** + * Return whether the underlying class is marked as abstract. + */ + boolean isAbstract(); + + /** + * Return whether the underlying class represents a concrete class, + * i.e. neither an interface nor an abstract class. + */ + boolean isConcrete(); + + /** + * Return whether the underlying class is marked as 'final'. + */ + boolean isFinal(); + + /** + * Determine whether the underlying class is independent, + * i.e. whether it is a top-level class or a nested class + * (static inner class) that can be constructed independent + * from an enclosing class. + */ + boolean isIndependent(); + + /** + * Return whether the underlying class has an enclosing class + * (i.e. the underlying class is an inner/nested class or + * a local class within a method). + * <p>If this method returns {@code false}, then the + * underlying class is a top-level class. + */ + boolean hasEnclosingClass(); + + /** + * Return the name of the enclosing class of the underlying class, + * or {@code null} if the underlying class is a top-level class. + */ + String getEnclosingClassName(); + + /** + * Return whether the underlying class has a super class. + */ + boolean hasSuperClass(); + + /** + * Return the name of the super class of the underlying class, + * or {@code null} if there is no super class defined. + */ + String getSuperClassName(); + + /** + * Return the names of all interfaces that the underlying class + * implements, or an empty array if there are none. + */ + String[] getInterfaceNames(); + + /** + * Return the names of all classes declared as members of the class represented by + * this ClassMetadata object. This includes public, protected, default (package) + * access, and private classes and interfaces declared by the class, but excludes + * inherited classes and interfaces. An empty array is returned if no member classes + * or interfaces exist. + */ + String[] getMemberClassNames(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/MethodMetadata.java b/spring-core/src/main/java/org/springframework/core/type/MethodMetadata.java new file mode 100644 index 00000000..17d7bab2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/MethodMetadata.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2012 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.type; + +import java.util.Map; + +/** + * Interface that defines abstract access to the annotations of a specific + * class, in a form that does not require that class to be loaded yet. + * + * @author Juergen Hoeller + * @author Mark Pollack + * @author Chris Beams + * @since 3.0 + * @see StandardMethodMetadata + * @see AnnotationMetadata#getAnnotatedMethods + */ +public interface MethodMetadata { + + /** + * Return the name of the method. + */ + String getMethodName(); + + /** + * Return the fully-qualified name of the class that declares this method. + */ + public String getDeclaringClassName(); + + /** + * Return whether the underlying method is declared as 'static'. + */ + boolean isStatic(); + + /** + * Return whether the underlying method is marked as 'final'. + */ + boolean isFinal(); + + /** + * Return whether the underlying method is overridable, + * i.e. not marked as static, final or private. + */ + boolean isOverridable(); + + /** + * Determine whether the underlying method has an annotation or + * meta-annotation of the given type defined. + * @param annotationType the annotation type to look for + * @return whether a matching annotation is defined + */ + boolean isAnnotated(String annotationType); + + /** + * Retrieve the attributes of the annotation of the given type, + * if any (i.e. if defined on the underlying method, as direct + * annotation or as meta-annotation). + * @param annotationType the annotation type to look for + * @return a Map of attributes, with the attribute name as key (e.g. "value") + * and the defined attribute value as Map value. This return value will be + * {@code null} if no matching annotation is defined. + */ + Map<String, Object> getAnnotationAttributes(String annotationType); + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java new file mode 100644 index 00000000..1a4e5365 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2014 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.type; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; + +/** + * {@link AnnotationMetadata} implementation that uses standard reflection + * to introspect a given {@link Class}. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @author Chris Beams + * @since 2.5 + */ +public class StandardAnnotationMetadata extends StandardClassMetadata implements AnnotationMetadata { + + private final boolean nestedAnnotationsAsMap; + + + /** + * Create a new {@code StandardAnnotationMetadata} wrapper for the given Class. + * @param introspectedClass the Class to introspect + * @see #StandardAnnotationMetadata(Class, boolean) + */ + public StandardAnnotationMetadata(Class<?> introspectedClass) { + this(introspectedClass, false); + } + + /** + * Create a new {@link StandardAnnotationMetadata} wrapper for the given Class, + * providing the option to return any nested annotations or annotation arrays in the + * form of {@link AnnotationAttributes} instead of actual {@link Annotation} instances. + * @param introspectedClass the Class to instrospect + * @param nestedAnnotationsAsMap return nested annotations and annotation arrays as + * {@link AnnotationAttributes} for compatibility with ASM-based + * {@link AnnotationMetadata} implementations + * @since 3.1.1 + */ + public StandardAnnotationMetadata(Class<?> introspectedClass, boolean nestedAnnotationsAsMap) { + super(introspectedClass); + this.nestedAnnotationsAsMap = nestedAnnotationsAsMap; + } + + + public Set<String> getAnnotationTypes() { + Set<String> types = new LinkedHashSet<String>(); + Annotation[] anns = getIntrospectedClass().getAnnotations(); + for (Annotation ann : anns) { + types.add(ann.annotationType().getName()); + } + return types; + } + + public Set<String> getMetaAnnotationTypes(String annotationType) { + Annotation[] anns = getIntrospectedClass().getAnnotations(); + for (Annotation ann : anns) { + if (ann.annotationType().getName().equals(annotationType)) { + Set<String> types = new LinkedHashSet<String>(); + Annotation[] metaAnns = ann.annotationType().getAnnotations(); + for (Annotation metaAnn : metaAnns) { + types.add(metaAnn.annotationType().getName()); + for (Annotation metaMetaAnn : metaAnn.annotationType().getAnnotations()) { + types.add(metaMetaAnn.annotationType().getName()); + } + } + return types; + } + } + return null; + } + + public boolean hasAnnotation(String annotationType) { + Annotation[] anns = getIntrospectedClass().getAnnotations(); + for (Annotation ann : anns) { + if (ann.annotationType().getName().equals(annotationType)) { + return true; + } + } + return false; + } + + public boolean hasMetaAnnotation(String annotationType) { + Annotation[] anns = getIntrospectedClass().getAnnotations(); + for (Annotation ann : anns) { + Annotation[] metaAnns = ann.annotationType().getAnnotations(); + for (Annotation metaAnn : metaAnns) { + if (metaAnn.annotationType().getName().equals(annotationType)) { + return true; + } + for (Annotation metaMetaAnn : metaAnn.annotationType().getAnnotations()) { + if (metaMetaAnn.annotationType().getName().equals(annotationType)) { + return true; + } + } + } + } + return false; + } + + public boolean isAnnotated(String annotationType) { + Annotation[] anns = getIntrospectedClass().getAnnotations(); + for (Annotation ann : anns) { + if (ann.annotationType().getName().equals(annotationType)) { + return true; + } + for (Annotation metaAnn : ann.annotationType().getAnnotations()) { + if (metaAnn.annotationType().getName().equals(annotationType)) { + return true; + } + } + } + return false; + } + + public Map<String, Object> getAnnotationAttributes(String annotationType) { + return this.getAnnotationAttributes(annotationType, false); + } + + public Map<String, Object> getAnnotationAttributes(String annotationType, boolean classValuesAsString) { + Annotation[] anns = getIntrospectedClass().getAnnotations(); + for (Annotation ann : anns) { + if (ann.annotationType().getName().equals(annotationType)) { + return AnnotationUtils.getAnnotationAttributes( + ann, classValuesAsString, this.nestedAnnotationsAsMap); + } + } + for (Annotation ann : anns) { + for (Annotation metaAnn : ann.annotationType().getAnnotations()) { + if (metaAnn.annotationType().getName().equals(annotationType)) { + return AnnotationUtils.getAnnotationAttributes( + metaAnn, classValuesAsString, this.nestedAnnotationsAsMap); + } + } + } + return null; + } + + public boolean hasAnnotatedMethods(String annotationType) { + Method[] methods = getIntrospectedClass().getDeclaredMethods(); + for (Method method : methods) { + if (!method.isBridge()) { + for (Annotation ann : method.getAnnotations()) { + if (ann.annotationType().getName().equals(annotationType)) { + return true; + } + else { + for (Annotation metaAnn : ann.annotationType().getAnnotations()) { + if (metaAnn.annotationType().getName().equals(annotationType)) { + return true; + } + } + } + } + } + } + return false; + } + + public Set<MethodMetadata> getAnnotatedMethods(String annotationType) { + Set<MethodMetadata> annotatedMethods = new LinkedHashSet<MethodMetadata>(); + Method[] methods = getIntrospectedClass().getDeclaredMethods(); + for (Method method : methods) { + if (!method.isBridge()) { + for (Annotation ann : method.getAnnotations()) { + if (ann.annotationType().getName().equals(annotationType)) { + annotatedMethods.add(new StandardMethodMetadata(method, this.nestedAnnotationsAsMap)); + break; + } + else { + for (Annotation metaAnn : ann.annotationType().getAnnotations()) { + if (metaAnn.annotationType().getName().equals(annotationType)) { + annotatedMethods.add(new StandardMethodMetadata(method, this.nestedAnnotationsAsMap)); + break; + } + } + } + } + } + } + return annotatedMethods; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardClassMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardClassMetadata.java new file mode 100644 index 00000000..4ac99d4d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/StandardClassMetadata.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2012 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.type; + +import java.lang.reflect.Modifier; +import java.util.LinkedHashSet; + +import org.springframework.util.Assert; + +/** + * {@link ClassMetadata} implementation that uses standard reflection + * to introspect a given {@code Class}. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public class StandardClassMetadata implements ClassMetadata { + + private final Class introspectedClass; + + + /** + * Create a new StandardClassMetadata wrapper for the given Class. + * @param introspectedClass the Class to introspect + */ + public StandardClassMetadata(Class introspectedClass) { + Assert.notNull(introspectedClass, "Class must not be null"); + this.introspectedClass = introspectedClass; + } + + /** + * Return the underlying Class. + */ + public final Class getIntrospectedClass() { + return this.introspectedClass; + } + + + public String getClassName() { + return this.introspectedClass.getName(); + } + + public boolean isInterface() { + return this.introspectedClass.isInterface(); + } + + public boolean isAbstract() { + return Modifier.isAbstract(this.introspectedClass.getModifiers()); + } + + public boolean isConcrete() { + return !(isInterface() || isAbstract()); + } + + public boolean isFinal() { + return Modifier.isFinal(this.introspectedClass.getModifiers()); + } + + public boolean isIndependent() { + return (!hasEnclosingClass() || + (this.introspectedClass.getDeclaringClass() != null && + Modifier.isStatic(this.introspectedClass.getModifiers()))); + } + + public boolean hasEnclosingClass() { + return (this.introspectedClass.getEnclosingClass() != null); + } + + public String getEnclosingClassName() { + Class enclosingClass = this.introspectedClass.getEnclosingClass(); + return (enclosingClass != null ? enclosingClass.getName() : null); + } + + public boolean hasSuperClass() { + return (this.introspectedClass.getSuperclass() != null); + } + + public String getSuperClassName() { + Class superClass = this.introspectedClass.getSuperclass(); + return (superClass != null ? superClass.getName() : null); + } + + public String[] getInterfaceNames() { + Class[] ifcs = this.introspectedClass.getInterfaces(); + String[] ifcNames = new String[ifcs.length]; + for (int i = 0; i < ifcs.length; i++) { + ifcNames[i] = ifcs[i].getName(); + } + return ifcNames; + } + + public String[] getMemberClassNames() { + LinkedHashSet<String> memberClassNames = new LinkedHashSet<String>(); + for (Class<?> nestedClass : this.introspectedClass.getDeclaredClasses()) { + memberClassNames.add(nestedClass.getName()); + } + return memberClassNames.toArray(new String[memberClassNames.size()]); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java new file mode 100644 index 00000000..8288debe --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2013 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.type; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Map; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; + +/** + * {@link MethodMetadata} implementation that uses standard reflection + * to introspect a given {@code Method}. + * + * @author Juergen Hoeller + * @author Mark Pollack + * @author Chris Beams + * @since 3.0 + */ +public class StandardMethodMetadata implements MethodMetadata { + + private final Method introspectedMethod; + + private final boolean nestedAnnotationsAsMap; + + + /** + * Create a new StandardMethodMetadata wrapper for the given Method. + * @param introspectedMethod the Method to introspect + */ + public StandardMethodMetadata(Method introspectedMethod) { + this(introspectedMethod, false); + } + + /** + * Create a new StandardMethodMetadata wrapper for the given Method, + * providing the option to return any nested annotations or annotation arrays in the + * form of {@link org.springframework.core.annotation.AnnotationAttributes} instead + * of actual {@link java.lang.annotation.Annotation} instances. + * @param introspectedMethod the Method to introspect + * @param nestedAnnotationsAsMap return nested annotations and annotation arrays as + * {@link org.springframework.core.annotation.AnnotationAttributes} for compatibility + * with ASM-based {@link AnnotationMetadata} implementations + * @since 3.1.1 + */ + public StandardMethodMetadata(Method introspectedMethod, boolean nestedAnnotationsAsMap) { + Assert.notNull(introspectedMethod, "Method must not be null"); + this.introspectedMethod = introspectedMethod; + this.nestedAnnotationsAsMap = nestedAnnotationsAsMap; + } + + /** + * Return the underlying Method. + */ + public final Method getIntrospectedMethod() { + return this.introspectedMethod; + } + + + public String getMethodName() { + return this.introspectedMethod.getName(); + } + + public String getDeclaringClassName() { + return this.introspectedMethod.getDeclaringClass().getName(); + } + + public boolean isStatic() { + return Modifier.isStatic(this.introspectedMethod.getModifiers()); + } + + public boolean isFinal() { + return Modifier.isFinal(this.introspectedMethod.getModifiers()); + } + + public boolean isOverridable() { + return (!isStatic() && !isFinal() && !Modifier.isPrivate(this.introspectedMethod.getModifiers())); + } + + public boolean isAnnotated(String annotationType) { + Annotation[] anns = this.introspectedMethod.getAnnotations(); + for (Annotation ann : anns) { + if (ann.annotationType().getName().equals(annotationType)) { + return true; + } + for (Annotation metaAnn : ann.annotationType().getAnnotations()) { + if (metaAnn.annotationType().getName().equals(annotationType)) { + return true; + } + } + } + return false; + } + + public Map<String, Object> getAnnotationAttributes(String annotationType) { + Annotation[] anns = this.introspectedMethod.getAnnotations(); + for (Annotation ann : anns) { + if (ann.annotationType().getName().equals(annotationType)) { + return AnnotationUtils.getAnnotationAttributes( + ann, true, nestedAnnotationsAsMap); + } + for (Annotation metaAnn : ann.annotationType().getAnnotations()) { + if (metaAnn.annotationType().getName().equals(annotationType)) { + return AnnotationUtils.getAnnotationAttributes( + metaAnn, true, this.nestedAnnotationsAsMap); + } + } + } + return null; + } + +} 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 new file mode 100644 index 00000000..4e9bb8fa --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java @@ -0,0 +1,276 @@ +/* + * Copyright 2002-2014 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.type.classreading; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.asm.Type; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1.1 + */ +abstract class AbstractRecursiveAnnotationVisitor extends AnnotationVisitor { + + protected final Log logger = LogFactory.getLog(getClass()); + + protected final AnnotationAttributes attributes; + + protected final ClassLoader classLoader; + + public AbstractRecursiveAnnotationVisitor(ClassLoader classLoader, AnnotationAttributes attributes) { + super(SpringAsmInfo.ASM_VERSION); + this.classLoader = classLoader; + this.attributes = attributes; + } + + public void visit(String attributeName, Object attributeValue) { + this.attributes.put(attributeName, attributeValue); + } + + public AnnotationVisitor visitAnnotation(String attributeName, String asmTypeDescriptor) { + String annotationType = Type.getType(asmTypeDescriptor).getClassName(); + AnnotationAttributes nestedAttributes = new AnnotationAttributes(); + this.attributes.put(attributeName, nestedAttributes); + return new RecursiveAnnotationAttributesVisitor(annotationType, nestedAttributes, this.classLoader); + } + + public AnnotationVisitor visitArray(String attributeName) { + return new RecursiveAnnotationArrayVisitor(attributeName, this.attributes, this.classLoader); + } + + public void visitEnum(String attributeName, String asmTypeDescriptor, String attributeValue) { + Object newValue = getEnumValue(asmTypeDescriptor, attributeValue); + visit(attributeName, newValue); + } + + protected Object getEnumValue(String asmTypeDescriptor, String attributeValue) { + Object valueToUse = attributeValue; + try { + Class<?> enumType = this.classLoader.loadClass(Type.getType(asmTypeDescriptor).getClassName()); + Field enumConstant = ReflectionUtils.findField(enumType, attributeValue); + if (enumConstant != null) { + valueToUse = enumConstant.get(null); + } + } + catch (ClassNotFoundException ex) { + logger.debug("Failed to classload enum type while reading annotation metadata", ex); + } + catch (IllegalAccessException ex) { + logger.warn("Could not access enum value while reading annotation metadata", ex); + } + return valueToUse; + } +} + + +/** + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1.1 + */ +final class RecursiveAnnotationArrayVisitor extends AbstractRecursiveAnnotationVisitor { + + private final String attributeName; + + private final List<AnnotationAttributes> allNestedAttributes = new ArrayList<AnnotationAttributes>(); + + public RecursiveAnnotationArrayVisitor( + String attributeName, AnnotationAttributes attributes, ClassLoader classLoader) { + super(classLoader, attributes); + this.attributeName = attributeName; + } + + @Override + public void visit(String attributeName, Object attributeValue) { + Object newValue = attributeValue; + Object existingValue = this.attributes.get(this.attributeName); + if (existingValue != null) { + newValue = ObjectUtils.addObjectToArray((Object[]) existingValue, newValue); + } + else { + Class<?> arrayClass = newValue.getClass(); + if (Enum.class.isAssignableFrom(arrayClass)) { + while (arrayClass.getSuperclass() != null && !arrayClass.isEnum()) { + arrayClass = arrayClass.getSuperclass(); + } + } + Object[] newArray = (Object[]) Array.newInstance(arrayClass, 1); + newArray[0] = newValue; + newValue = newArray; + } + this.attributes.put(this.attributeName, newValue); + } + + @Override + public AnnotationVisitor visitAnnotation(String attributeName, String asmTypeDescriptor) { + String annotationType = Type.getType(asmTypeDescriptor).getClassName(); + AnnotationAttributes nestedAttributes = new AnnotationAttributes(); + this.allNestedAttributes.add(nestedAttributes); + return new RecursiveAnnotationAttributesVisitor(annotationType, nestedAttributes, this.classLoader); + } + + public void visitEnd() { + if (!this.allNestedAttributes.isEmpty()) { + this.attributes.put(this.attributeName, + this.allNestedAttributes.toArray(new AnnotationAttributes[this.allNestedAttributes.size()])); + } + } +} + + +/** + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1.1 + */ +class RecursiveAnnotationAttributesVisitor extends AbstractRecursiveAnnotationVisitor { + + private final String annotationType; + + public RecursiveAnnotationAttributesVisitor(String annotationType, AnnotationAttributes attributes, + ClassLoader classLoader) { + super(classLoader, attributes); + this.annotationType = annotationType; + } + + 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 further 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); + } + } + } + } +} + + +/** + * ASM visitor which looks for the annotations defined on a class or method, including + * tracking meta-annotations. + * + * <p>As of Spring 3.1.1, this visitor is fully recursive, taking into account any nested + * annotations or nested annotation arrays. These annotations are in turn read into + * {@link AnnotationAttributes} map structures. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 3.0 + */ +final class AnnotationAttributesReadingVisitor extends RecursiveAnnotationAttributesVisitor { + + private final String annotationType; + + private final Map<String, AnnotationAttributes> attributesMap; + + private final Map<String, Set<String>> metaAnnotationMap; + + public AnnotationAttributesReadingVisitor( + String annotationType, Map<String, AnnotationAttributes> attributesMap, + Map<String, Set<String>> metaAnnotationMap, ClassLoader classLoader) { + + super(annotationType, new AnnotationAttributes(), classLoader); + this.annotationType = annotationType; + this.attributesMap = attributesMap; + this.metaAnnotationMap = metaAnnotationMap; + } + + @Override + public void doVisitEnd(Class<?> annotationClass) { + super.doVisitEnd(annotationClass); + this.attributesMap.put(this.annotationType, this.attributes); + registerMetaAnnotations(annotationClass); + } + + private void registerMetaAnnotations(Class<?> annotationClass) { + // Register annotations that the annotation type is annotated with. + Set<String> metaAnnotationTypeNames = new LinkedHashSet<String>(); + for (Annotation metaAnnotation : annotationClass.getAnnotations()) { + metaAnnotationTypeNames.add(metaAnnotation.annotationType().getName()); + // Only do further scanning for public annotations; we'd run into IllegalAccessExceptions + // otherwise, and don't want to mess with accessibility in a SecurityManager environment. + if (Modifier.isPublic(metaAnnotation.annotationType().getModifiers())) { + if (!this.attributesMap.containsKey(metaAnnotation.annotationType().getName())) { + this.attributesMap.put(metaAnnotation.annotationType().getName(), + AnnotationUtils.getAnnotationAttributes(metaAnnotation, true, true)); + } + for (Annotation metaMetaAnnotation : metaAnnotation.annotationType().getAnnotations()) { + metaAnnotationTypeNames.add(metaMetaAnnotation.annotationType().getName()); + } + } + } + if (this.metaAnnotationMap != null) { + 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 new file mode 100644 index 00000000..e1739846 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2014 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.type.classreading; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.Type; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; + +/** + * ASM class visitor which looks for the class name and implemented types as + * well as for the annotations defined on the class, exposing them through + * the {@link org.springframework.core.type.AnnotationMetadata} interface. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @author Costin Leau + * @since 2.5 + */ +final class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor implements AnnotationMetadata { + + private final ClassLoader classLoader; + + private final Set<String> annotationSet = new LinkedHashSet<String>(4); + + private final Map<String, Set<String>> metaAnnotationMap = new LinkedHashMap<String, Set<String>>(4); + + private final Map<String, AnnotationAttributes> attributeMap = new LinkedHashMap<String, AnnotationAttributes>(4); + + private final Set<MethodMetadata> methodMetadataSet = new LinkedHashSet<MethodMetadata>(4); + + + public AnnotationMetadataReadingVisitor(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + // Skip bridge methods - we're only interested in original annotation-defining user methods. + // On JDK 8, we'd otherwise run into double detection of the same annotated method... + if ((access & Opcodes.ACC_BRIDGE) != 0) { + return super.visitMethod(access, name, desc, signature, exceptions); + } + return new MethodMetadataReadingVisitor(name, access, getClassName(), this.classLoader, this.methodMetadataSet); + } + + @Override + public AnnotationVisitor visitAnnotation(final String desc, boolean visible) { + String className = Type.getType(desc).getClassName(); + this.annotationSet.add(className); + return new AnnotationAttributesReadingVisitor(className, this.attributeMap, this.metaAnnotationMap, this.classLoader); + } + + + public Set<String> getAnnotationTypes() { + return this.annotationSet; + } + + public Set<String> getMetaAnnotationTypes(String annotationType) { + return this.metaAnnotationMap.get(annotationType); + } + + public boolean hasAnnotation(String annotationType) { + return this.annotationSet.contains(annotationType); + } + + public boolean hasMetaAnnotation(String metaAnnotationType) { + Collection<Set<String>> allMetaTypes = this.metaAnnotationMap.values(); + for (Set<String> metaTypes : allMetaTypes) { + if (metaTypes.contains(metaAnnotationType)) { + return true; + } + } + return false; + } + + public boolean isAnnotated(String annotationType) { + return this.attributeMap.containsKey(annotationType); + } + + public AnnotationAttributes getAnnotationAttributes(String annotationType) { + return getAnnotationAttributes(annotationType, false); + } + + public AnnotationAttributes getAnnotationAttributes(String annotationType, boolean classValuesAsString) { + AnnotationAttributes raw = this.attributeMap.get(annotationType); + return convertClassValues(raw, classValuesAsString); + } + + private AnnotationAttributes convertClassValues(AnnotationAttributes original, boolean classValuesAsString) { + if (original == null) { + return null; + } + AnnotationAttributes result = new AnnotationAttributes(original.size()); + for (Map.Entry<String, Object> entry : original.entrySet()) { + try { + Object value = entry.getValue(); + if (value instanceof AnnotationAttributes) { + value = convertClassValues((AnnotationAttributes) value, classValuesAsString); + } + else if (value instanceof AnnotationAttributes[]) { + AnnotationAttributes[] values = (AnnotationAttributes[])value; + for (int i = 0; i < values.length; i++) { + values[i] = convertClassValues(values[i], classValuesAsString); + } + } + else if (value instanceof Type) { + value = (classValuesAsString ? ((Type) value).getClassName() : + this.classLoader.loadClass(((Type) value).getClassName())); + } + else if (value instanceof Type[]) { + Type[] array = (Type[]) value; + 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() : + this.classLoader.loadClass(array[i].getClassName())); + } + value = convArray; + } + else if (classValuesAsString) { + if (value instanceof Class) { + value = ((Class<?>) value).getName(); + } + 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(); + } + value = newValue; + } + } + result.put(entry.getKey(), value); + } + catch (Exception ex) { + // Class not found - can't resolve class reference in annotation attribute. + } + } + return result; + } + + public boolean hasAnnotatedMethods(String annotationType) { + for (MethodMetadata methodMetadata : this.methodMetadataSet) { + if (methodMetadata.isAnnotated(annotationType)) { + return true; + } + } + return false; + } + + public Set<MethodMetadata> getAnnotatedMethods(String annotationType) { + Set<MethodMetadata> annotatedMethods = new LinkedHashSet<MethodMetadata>(4); + for (MethodMetadata methodMetadata : this.methodMetadataSet) { + if (methodMetadata.isAnnotated(annotationType)) { + annotatedMethods.add(methodMetadata); + } + } + return annotatedMethods; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java b/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java new file mode 100644 index 00000000..ef8201d6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2012 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.type.classreading; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +/** + * Caching implementation of the {@link MetadataReaderFactory} interface, + * caching {@link MetadataReader} per Spring {@link Resource} handle + * (i.e. per ".class" file). + * + * @author Juergen Hoeller + * @author Costin Leau + * @since 2.5 + */ +public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory { + + /** Default maximum number of entries for the MetadataReader cache: 256 */ + public static final int DEFAULT_CACHE_LIMIT = 256; + + + private volatile int cacheLimit = DEFAULT_CACHE_LIMIT; + + @SuppressWarnings("serial") + private final Map<Resource, MetadataReader> metadataReaderCache = + new LinkedHashMap<Resource, MetadataReader>(DEFAULT_CACHE_LIMIT, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry<Resource, MetadataReader> eldest) { + return size() > getCacheLimit(); + } + }; + + + /** + * Create a new CachingMetadataReaderFactory for the default class loader. + */ + public CachingMetadataReaderFactory() { + super(); + } + + /** + * Create a new CachingMetadataReaderFactory for the given resource loader. + * @param resourceLoader the Spring ResourceLoader to use + * (also determines the ClassLoader to use) + */ + public CachingMetadataReaderFactory(ResourceLoader resourceLoader) { + super(resourceLoader); + } + + /** + * Create a new CachingMetadataReaderFactory for the given class loader. + * @param classLoader the ClassLoader to use + */ + public CachingMetadataReaderFactory(ClassLoader classLoader) { + super(classLoader); + } + + + /** + * Specify the maximum number of entries for the MetadataReader cache. + * Default is 256. + */ + public void setCacheLimit(int cacheLimit) { + this.cacheLimit = cacheLimit; + } + + /** + * Return the maximum number of entries for the MetadataReader cache. + */ + public int getCacheLimit() { + return this.cacheLimit; + } + + + @Override + public MetadataReader getMetadataReader(Resource resource) throws IOException { + if (getCacheLimit() <= 0) { + return super.getMetadataReader(resource); + } + synchronized (this.metadataReaderCache) { + MetadataReader metadataReader = this.metadataReaderCache.get(resource); + if (metadataReader == null) { + metadataReader = super.getMetadataReader(resource); + this.metadataReaderCache.put(resource, metadataReader); + } + return metadataReader; + } + } + + /** + * Clear the entire MetadataReader cache, removing all cached class metadata. + */ + public void clearCache() { + synchronized (this.metadataReaderCache) { + this.metadataReaderCache.clear(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/ClassMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/ClassMetadataReadingVisitor.java new file mode 100644 index 00000000..45785bc8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/ClassMetadataReadingVisitor.java @@ -0,0 +1,213 @@ +/* + * Copyright 2002-2012 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.type.classreading; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.Attribute; +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.FieldVisitor; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.core.type.ClassMetadata; +import org.springframework.util.ClassUtils; + +/** + * ASM class visitor which looks only for the class name and implemented types, + * exposing them through the {@link org.springframework.core.type.ClassMetadata} + * interface. + * + * @author Rod Johnson + * @author Costin Leau + * @author Mark Fisher + * @author Ramnivas Laddad + * @author Chris Beams + * @since 2.5 + */ +class ClassMetadataReadingVisitor extends ClassVisitor implements ClassMetadata { + + private String className; + + private boolean isInterface; + + private boolean isAbstract; + + private boolean isFinal; + + private String enclosingClassName; + + private boolean independentInnerClass; + + private String superClassName; + + private String[] interfaces; + + private Set<String> memberClassNames = new LinkedHashSet<String>(); + + + public ClassMetadataReadingVisitor() { + super(SpringAsmInfo.ASM_VERSION); + } + + + public void visit(int version, int access, String name, String signature, String supername, String[] interfaces) { + this.className = ClassUtils.convertResourcePathToClassName(name); + this.isInterface = ((access & Opcodes.ACC_INTERFACE) != 0); + this.isAbstract = ((access & Opcodes.ACC_ABSTRACT) != 0); + this.isFinal = ((access & Opcodes.ACC_FINAL) != 0); + if (supername != null) { + this.superClassName = ClassUtils.convertResourcePathToClassName(supername); + } + this.interfaces = new String[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + this.interfaces[i] = ClassUtils.convertResourcePathToClassName(interfaces[i]); + } + } + + public void visitOuterClass(String owner, String name, String desc) { + this.enclosingClassName = ClassUtils.convertResourcePathToClassName(owner); + } + + public void visitInnerClass(String name, String outerName, String innerName, int access) { + if (outerName != null) { + String fqName = ClassUtils.convertResourcePathToClassName(name); + String fqOuterName = ClassUtils.convertResourcePathToClassName(outerName); + if (this.className.equals(fqName)) { + this.enclosingClassName = fqOuterName; + this.independentInnerClass = ((access & Opcodes.ACC_STATIC) != 0); + } + else if (this.className.equals(fqOuterName)) { + this.memberClassNames.add(fqName); + } + } + } + + public void visitSource(String source, String debug) { + // no-op + } + + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // no-op + return new EmptyAnnotationVisitor(); + } + + public void visitAttribute(Attribute attr) { + // no-op + } + + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + // no-op + return new EmptyFieldVisitor(); + } + + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + // no-op + return new EmptyMethodVisitor(); + } + + public void visitEnd() { + // no-op + } + + + public String getClassName() { + return this.className; + } + + public boolean isInterface() { + return this.isInterface; + } + + public boolean isAbstract() { + return this.isAbstract; + } + + public boolean isConcrete() { + return !(this.isInterface || this.isAbstract); + } + + public boolean isFinal() { + return this.isFinal; + } + + public boolean isIndependent() { + return (this.enclosingClassName == null || this.independentInnerClass); + } + + public boolean hasEnclosingClass() { + return (this.enclosingClassName != null); + } + + public String getEnclosingClassName() { + return this.enclosingClassName; + } + + public boolean hasSuperClass() { + return (this.superClassName != null); + } + + public String getSuperClassName() { + return this.superClassName; + } + + public String[] getInterfaceNames() { + return this.interfaces; + } + + public String[] getMemberClassNames() { + return this.memberClassNames.toArray(new String[this.memberClassNames.size()]); + } + +} + + +class EmptyAnnotationVisitor extends AnnotationVisitor { + + public EmptyAnnotationVisitor() { + super(SpringAsmInfo.ASM_VERSION); + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String desc) { + return this; + } + + @Override + public AnnotationVisitor visitArray(String name) { + return this; + } +} + + +class EmptyMethodVisitor extends MethodVisitor { + + public EmptyMethodVisitor() { + super(SpringAsmInfo.ASM_VERSION); + } +} + + +class EmptyFieldVisitor extends FieldVisitor { + + public EmptyFieldVisitor() { + super(SpringAsmInfo.ASM_VERSION); + } + +}
\ No newline at end of file diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReader.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReader.java new file mode 100644 index 00000000..99b67d4d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReader.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2009 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.type.classreading; + +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.ClassMetadata; + +/** + * Simple facade for accessing class metadata, + * as read by an ASM {@link org.springframework.asm.ClassReader}. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public interface MetadataReader { + + /** + * Return the resource reference for the class file. + */ + Resource getResource(); + + /** + * Read basic class metadata for the underlying class. + */ + ClassMetadata getClassMetadata(); + + /** + * Read full annotation metadata for the underlying class, + * including metadata for annotated methods. + */ + AnnotationMetadata getAnnotationMetadata(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java new file mode 100644 index 00000000..e2e010c9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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.type.classreading; + +import java.io.IOException; + +import org.springframework.core.io.Resource; + +/** + * Factory interface for {@link MetadataReader} instances. + * Allows for caching a MetadataReader per original resource. + * + * @author Juergen Hoeller + * @since 2.5 + * @see SimpleMetadataReaderFactory + * @see CachingMetadataReaderFactory + */ +public interface MetadataReaderFactory { + + /** + * Obtain a MetadataReader for the given class name. + * @param className the class name (to be resolved to a ".class" file) + * @return a holder for the ClassReader instance (never {@code null}) + * @throws IOException in case of I/O failure + */ + MetadataReader getMetadataReader(String className) throws IOException; + + /** + * Obtain a MetadataReader for the given resource. + * @param resource the resource (pointing to a ".class" file) + * @return a holder for the ClassReader instance (never {@code null}) + * @throws IOException in case of I/O failure + */ + MetadataReader getMetadataReader(Resource resource) throws IOException; + +} 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 new file mode 100644 index 00000000..dde3790c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2014 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.type.classreading; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.asm.Type; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.MethodMetadata; + +/** + * ASM method visitor which looks for the annotations defined on the method, + * exposing them through the {@link org.springframework.core.type.MethodMetadata} + * interface. + * + * @author Juergen Hoeller + * @author Mark Pollack + * @author Costin Leau + * @author Chris Beams + * @since 3.0 + */ +final class MethodMetadataReadingVisitor extends MethodVisitor implements MethodMetadata { + + private final String name; + + private final int access; + + private final String declaringClassName; + + private final ClassLoader classLoader; + + private final Set<MethodMetadata> methodMetadataSet; + + private final Map<String, AnnotationAttributes> attributeMap = new LinkedHashMap<String, AnnotationAttributes>(2); + + + public MethodMetadataReadingVisitor(String name, int access, String declaringClassName, + ClassLoader classLoader, Set<MethodMetadata> methodMetadataSet) { + + super(SpringAsmInfo.ASM_VERSION); + this.name = name; + this.access = access; + this.declaringClassName = declaringClassName; + this.classLoader = classLoader; + this.methodMetadataSet = methodMetadataSet; + } + + + @Override + public AnnotationVisitor visitAnnotation(final String desc, boolean visible) { + String className = Type.getType(desc).getClassName(); + this.methodMetadataSet.add(this); + return new AnnotationAttributesReadingVisitor(className, this.attributeMap, null, this.classLoader); + } + + public String getMethodName() { + return this.name; + } + + public boolean isStatic() { + return ((this.access & Opcodes.ACC_STATIC) != 0); + } + + public boolean isFinal() { + return ((this.access & Opcodes.ACC_FINAL) != 0); + } + + public boolean isOverridable() { + return (!isStatic() && !isFinal() && ((this.access & Opcodes.ACC_PRIVATE) == 0)); + } + + public boolean isAnnotated(String annotationType) { + return this.attributeMap.containsKey(annotationType); + } + + public AnnotationAttributes getAnnotationAttributes(String annotationType) { + return this.attributeMap.get(annotationType); + } + + public String getDeclaringClassName() { + return this.declaringClassName; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java new file mode 100644 index 00000000..36c495e5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2013 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.type.classreading; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.asm.ClassReader; +import org.springframework.core.NestedIOException; +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.ClassMetadata; + +/** + * {@link MetadataReader} implementation based on an ASM + * {@link org.springframework.asm.ClassReader}. + * + * <p>Package-visible in order to allow for repackaging the ASM library + * without effect on users of the {@code core.type} package. + * + * @author Juergen Hoeller + * @author Costin Leau + * @since 2.5 + */ +final class SimpleMetadataReader implements MetadataReader { + + private final Resource resource; + + private final ClassMetadata classMetadata; + + private final AnnotationMetadata annotationMetadata; + + + SimpleMetadataReader(Resource resource, ClassLoader classLoader) throws IOException { + InputStream is = new BufferedInputStream(resource.getInputStream()); + ClassReader classReader; + try { + classReader = new ClassReader(is); + } + catch (IllegalArgumentException ex) { + throw new NestedIOException("ASM ClassReader failed to parse class file - " + + "probably due to a new Java class file version that isn't supported yet: " + resource, ex); + } + finally { + is.close(); + } + + AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader); + classReader.accept(visitor, ClassReader.SKIP_DEBUG); + + this.annotationMetadata = visitor; + // (since AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor) + this.classMetadata = visitor; + this.resource = resource; + } + + + public Resource getResource() { + return this.resource; + } + + public ClassMetadata getClassMetadata() { + return this.classMetadata; + } + + public AnnotationMetadata getAnnotationMetadata() { + return this.annotationMetadata; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReaderFactory.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReaderFactory.java new file mode 100644 index 00000000..9e1c736f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReaderFactory.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2014 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.type.classreading; + +import java.io.IOException; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.ClassUtils; + +/** + * Simple implementation of the {@link MetadataReaderFactory} interface, + * creating a new ASM {@link org.springframework.asm.ClassReader} for every request. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public class SimpleMetadataReaderFactory implements MetadataReaderFactory { + + private final ResourceLoader resourceLoader; + + + /** + * Create a new SimpleMetadataReaderFactory for the default class loader. + */ + public SimpleMetadataReaderFactory() { + this.resourceLoader = new DefaultResourceLoader(); + } + + /** + * Create a new SimpleMetadataReaderFactory for the given resource loader. + * @param resourceLoader the Spring ResourceLoader to use + * (also determines the ClassLoader to use) + */ + public SimpleMetadataReaderFactory(ResourceLoader resourceLoader) { + this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); + } + + /** + * Create a new SimpleMetadataReaderFactory for the given class loader. + * @param classLoader the ClassLoader to use + */ + public SimpleMetadataReaderFactory(ClassLoader classLoader) { + this.resourceLoader = + (classLoader != null ? new DefaultResourceLoader(classLoader) : new DefaultResourceLoader()); + } + + + /** + * Return the ResourceLoader that this MetadataReaderFactory has been + * constructed with. + */ + public final ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + + + public MetadataReader getMetadataReader(String className) throws IOException { + String resourcePath = ResourceLoader.CLASSPATH_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(className) + ClassUtils.CLASS_FILE_SUFFIX; + Resource resource = this.resourceLoader.getResource(resourcePath); + if (!resource.exists()) { + // Maybe an inner class name using the dot name syntax? Need to use the dollar syntax here... + // ClassUtils.forName has an equivalent check for resolution into Class references later on. + int lastDotIndex = className.lastIndexOf('.'); + if (lastDotIndex != -1) { + String innerClassName = + className.substring(0, lastDotIndex) + '$' + className.substring(lastDotIndex + 1); + String innerClassResourcePath = ResourceLoader.CLASSPATH_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(innerClassName) + ClassUtils.CLASS_FILE_SUFFIX; + Resource innerClassResource = this.resourceLoader.getResource(innerClassResourcePath); + if (innerClassResource.exists()) { + resource = innerClassResource; + } + } + } + return getMetadataReader(resource); + } + + public MetadataReader getMetadataReader(Resource resource) throws IOException { + return new SimpleMetadataReader(resource, this.resourceLoader.getClassLoader()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/package-info.java b/spring-core/src/main/java/org/springframework/core/type/classreading/package-info.java new file mode 100644 index 00000000..62b269f4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * Support classes for reading annotation and class-level metadata. + * + */ +package org.springframework.core.type.classreading; + diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractClassTestingTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractClassTestingTypeFilter.java new file mode 100644 index 00000000..e63571f6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractClassTestingTypeFilter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2014 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.type.filter; + +import java.io.IOException; + +import org.springframework.core.type.ClassMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; + +/** + * Type filter that exposes a + * {@link org.springframework.core.type.ClassMetadata} object + * to subclasses, for class testing purposes. + * + * @author Rod Johnson + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.5 + * @see #match(org.springframework.core.type.ClassMetadata) + */ +public abstract class AbstractClassTestingTypeFilter implements TypeFilter { + + public final boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + + return match(metadataReader.getClassMetadata()); + } + + /** + * Determine a match based on the given ClassMetadata object. + * @param metadata the ClassMetadata object + * @return whether this filter matches on the specified type + */ + protected abstract boolean match(ClassMetadata metadata); + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java new file mode 100644 index 00000000..6ee0c875 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2014 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.type.filter; + +import java.io.IOException; + +import org.springframework.core.type.ClassMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; + +/** + * Type filter that is aware of traversing over hierarchy. + * + * <p>This filter is useful when matching needs to be made based on potentially the + * whole class/interface hierarchy. The algorithm employed uses a succeed-fast + * strategy: if at any time a match is declared, no further processing is + * carried out. + * + * @author Ramnivas Laddad + * @author Mark Fisher + * @since 2.5 + */ +public abstract class AbstractTypeHierarchyTraversingFilter implements TypeFilter { + + private final boolean considerInherited; + + private final boolean considerInterfaces; + + + protected AbstractTypeHierarchyTraversingFilter(boolean considerInherited, boolean considerInterfaces) { + this.considerInherited = considerInherited; + this.considerInterfaces = considerInterfaces; + } + + + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + + // This method optimizes avoiding unnecessary creation of ClassReaders + // as well as visiting over those readers. + if (matchSelf(metadataReader)) { + return true; + } + ClassMetadata metadata = metadataReader.getClassMetadata(); + if (matchClassName(metadata.getClassName())) { + return true; + } + + if (!this.considerInherited) { + return false; + } + if (metadata.hasSuperClass()) { + // Optimization to avoid creating ClassReader for super class. + Boolean superClassMatch = matchSuperClass(metadata.getSuperClassName()); + if (superClassMatch != null) { + if (superClassMatch.booleanValue()) { + return true; + } + } + else { + // Need to read super class to determine a match... + if (match(metadata.getSuperClassName(), metadataReaderFactory)) { + return true; + } + } + } + + if (!this.considerInterfaces) { + return false; + } + for (String ifc : metadata.getInterfaceNames()) { + // Optimization to avoid creating ClassReader for super class + Boolean interfaceMatch = matchInterface(ifc); + if (interfaceMatch != null) { + if (interfaceMatch.booleanValue()) { + return true; + } + } + else { + // Need to read interface to determine a match... + if (match(ifc, metadataReaderFactory)) { + return true; + } + } + } + + return false; + } + + private boolean match(String className, MetadataReaderFactory metadataReaderFactory) throws IOException { + return match(metadataReaderFactory.getMetadataReader(className), metadataReaderFactory); + } + + /** + * Override this to match self characteristics alone. Typically, + * the implementation will use a visitor to extract information + * to perform matching. + */ + protected boolean matchSelf(MetadataReader metadataReader) { + return false; + } + + /** + * Override this to match on type name. + */ + protected boolean matchClassName(String className) { + return false; + } + + /** + * Override this to match on super type name. + */ + protected Boolean matchSuperClass(String superClassName) { + return null; + } + + /** + * Override this to match on interface type name. + */ + protected Boolean matchInterface(String interfaceName) { + return null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java new file mode 100644 index 00000000..7d2d674c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2014 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.type.filter; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.util.ClassUtils; + +/** + * A simple filter which matches classes with a given annotation, + * checking inherited annotations as well. + * + * <p>The matching logic mirrors that of {@code Class.isAnnotationPresent()}. + * + * @author Mark Fisher + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @since 2.5 + */ +public class AnnotationTypeFilter extends AbstractTypeHierarchyTraversingFilter { + + private final Class<? extends Annotation> annotationType; + + private final boolean considerMetaAnnotations; + + + /** + * Create a new AnnotationTypeFilter for the given annotation type. + * This filter will also match meta-annotations. To disable the + * meta-annotation matching, use the constructor that accepts a + * '{@code considerMetaAnnotations}' argument. The filter will + * not match interfaces. + * @param annotationType the annotation type to match + */ + public AnnotationTypeFilter(Class<? extends Annotation> annotationType) { + this(annotationType, true, false); + } + + /** + * Create a new AnnotationTypeFilter for the given annotation type. + * The filter will not match interfaces. + * @param annotationType the annotation type to match + * @param considerMetaAnnotations whether to also match on meta-annotations + */ + public AnnotationTypeFilter(Class<? extends Annotation> annotationType, boolean considerMetaAnnotations) { + this(annotationType, considerMetaAnnotations, false); + } + + /** + * Create a new {@link AnnotationTypeFilter} for the given annotation type. + * @param annotationType the annotation type to match + * @param considerMetaAnnotations whether to also match on meta-annotations + * @param considerInterfaces whether to also match interfaces + */ + public AnnotationTypeFilter(Class<? extends Annotation> annotationType, boolean considerMetaAnnotations, boolean considerInterfaces) { + super(annotationType.isAnnotationPresent(Inherited.class), considerInterfaces); + this.annotationType = annotationType; + this.considerMetaAnnotations = considerMetaAnnotations; + } + + + @Override + protected boolean matchSelf(MetadataReader metadataReader) { + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + return metadata.hasAnnotation(this.annotationType.getName()) || + (this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName())); + } + + @Override + protected Boolean matchSuperClass(String superClassName) { + return hasAnnotation(superClassName); + } + + @Override + protected Boolean matchInterface(String interfaceName) { + return hasAnnotation(interfaceName); + } + + protected Boolean hasAnnotation(String typeName) { + if (Object.class.getName().equals(typeName)) { + return false; + } + else if (typeName.startsWith("java")) { + try { + Class<?> clazz = ClassUtils.forName(typeName, getClass().getClassLoader()); + return ((this.considerMetaAnnotations ? AnnotationUtils.getAnnotation(clazz, this.annotationType) : + clazz.getAnnotation(this.annotationType)) != null); + } + catch (Throwable ex) { + // Class not regularly loadable - can't determine a match that way. + } + } + return null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AspectJTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AspectJTypeFilter.java new file mode 100644 index 00000000..0e2a3a5d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AspectJTypeFilter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2014 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.type.filter; + +import java.io.IOException; + +import org.aspectj.bridge.IMessageHandler; +import org.aspectj.weaver.ResolvedType; +import org.aspectj.weaver.World; +import org.aspectj.weaver.bcel.BcelWorld; +import org.aspectj.weaver.patterns.Bindings; +import org.aspectj.weaver.patterns.FormalBinding; +import org.aspectj.weaver.patterns.IScope; +import org.aspectj.weaver.patterns.PatternParser; +import org.aspectj.weaver.patterns.SimpleScope; +import org.aspectj.weaver.patterns.TypePattern; + +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; + +/** + * Type filter that uses AspectJ type pattern for matching. + * + * <p>A critical implementation details of this type filter is that it does not + * load the class being examined to match with a type pattern. + * + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @since 2.5 + */ +public class AspectJTypeFilter implements TypeFilter { + + private final World world; + + private final TypePattern typePattern; + + + public AspectJTypeFilter(String typePatternExpression, ClassLoader classLoader) { + this.world = new BcelWorld(classLoader, IMessageHandler.THROW, null); + this.world.setBehaveInJava5Way(true); + PatternParser patternParser = new PatternParser(typePatternExpression); + TypePattern typePattern = patternParser.parseTypePattern(); + typePattern.resolve(this.world); + IScope scope = new SimpleScope(this.world, new FormalBinding[0]); + this.typePattern = typePattern.resolveBindings(scope, Bindings.NONE, false, false); + } + + + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + + String className = metadataReader.getClassMetadata().getClassName(); + ResolvedType resolvedType = this.world.resolve(className); + return this.typePattern.matchesStatically(resolvedType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java new file mode 100644 index 00000000..63653046 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2014 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.type.filter; + +import org.springframework.util.ClassUtils; + +/** + * A simple filter which matches classes that are assignable to a given type. + * + * @author Rod Johnson + * @author Mark Fisher + * @author Ramnivas Laddad + * @since 2.5 + */ +public class AssignableTypeFilter extends AbstractTypeHierarchyTraversingFilter { + + private final Class<?> targetType; + + + /** + * Create a new AssignableTypeFilter for the given type. + * @param targetType the type to match + */ + public AssignableTypeFilter(Class<?> targetType) { + super(true, true); + this.targetType = targetType; + } + + + @Override + protected boolean matchClassName(String className) { + return this.targetType.getName().equals(className); + } + + @Override + protected Boolean matchSuperClass(String superClassName) { + return matchTargetType(superClassName); + } + + @Override + protected Boolean matchInterface(String interfaceName) { + return matchTargetType(interfaceName); + } + + protected Boolean matchTargetType(String typeName) { + if (this.targetType.getName().equals(typeName)) { + return true; + } + else if (Object.class.getName().equals(typeName)) { + return false; + } + else if (typeName.startsWith("java")) { + try { + Class<?> clazz = ClassUtils.forName(typeName, getClass().getClassLoader()); + return this.targetType.isAssignableFrom(clazz); + } + catch (Throwable ex) { + // Class not regularly loadable - can't determine a match that way. + } + } + return null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/RegexPatternTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/RegexPatternTypeFilter.java new file mode 100644 index 00000000..16605d64 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/RegexPatternTypeFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2007 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.type.filter; + +import java.util.regex.Pattern; + +import org.springframework.core.type.ClassMetadata; +import org.springframework.util.Assert; + +/** + * A simple filter for matching a fully-qualified class name with a regex {@link Pattern}. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + */ +public class RegexPatternTypeFilter extends AbstractClassTestingTypeFilter { + + private final Pattern pattern; + + + public RegexPatternTypeFilter(Pattern pattern) { + Assert.notNull(pattern, "Pattern must not be null"); + this.pattern = pattern; + } + + + @Override + protected boolean match(ClassMetadata metadata) { + return this.pattern.matcher(metadata.getClassName()).matches(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/TypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/TypeFilter.java new file mode 100644 index 00000000..bdcf3352 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/TypeFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2007 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.type.filter; + +import java.io.IOException; + +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; + +/** + * Base interface for type filters using a + * {@link org.springframework.core.type.classreading.MetadataReader}. + * + * @author Costin Leau + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.5 + */ +public interface TypeFilter { + + /** + * Determine whether this filter matches for the class described by + * the given metadata. + * @param metadataReader the metadata reader for the target class + * @param metadataReaderFactory a factory for obtaining metadata readers + * for other classes (such as superclasses and interfaces) + * @return whether this filter matches + * @throws IOException in case of I/O failure when reading metadata + */ + boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java b/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java new file mode 100644 index 00000000..05c63a26 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * Core support package for type filtering (e.g. for classpath scanning). + * + */ +package org.springframework.core.type.filter; + diff --git a/spring-core/src/main/java/org/springframework/core/type/package-info.java b/spring-core/src/main/java/org/springframework/core/type/package-info.java new file mode 100644 index 00000000..f9508358 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * Core support package for type introspection. + * + */ +package org.springframework.core.type; + diff --git a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java new file mode 100644 index 00000000..a4f74a4b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java @@ -0,0 +1,576 @@ +/* + * Copyright 2002-2013 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.util; + +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * PathMatcher implementation for Ant-style path patterns. Examples are provided below. + * + * <p>Part of this mapping code has been kindly borrowed from <a href="http://ant.apache.org">Apache Ant</a>. + * + * <p>The mapping matches URLs using the following rules:<br> <ul> <li>? matches one character</li> <li>* matches zero + * or more characters</li> <li>** matches zero or more 'directories' in a path</li> </ul> + * + * <p>Some examples:<br> <ul> <li>{@code com/t?st.jsp} - matches {@code com/test.jsp} but also + * {@code com/tast.jsp} or {@code com/txst.jsp}</li> <li>{@code com/*.jsp} - matches all + * {@code .jsp} files in the {@code com} directory</li> <li>{@code com/**/test.jsp} - matches all + * {@code test.jsp} files underneath the {@code com} path</li> <li>{@code org/springframework/**/*.jsp} + * - matches all {@code .jsp} files underneath the {@code org/springframework} path</li> + * <li>{@code org/**/servlet/bla.jsp} - matches {@code org/springframework/servlet/bla.jsp} but also + * {@code org/springframework/testing/servlet/bla.jsp} and {@code org/servlet/bla.jsp}</li> </ul> + * + * @author Alef Arendsen + * @author Juergen Hoeller + * @author Rob Harrop + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 16.07.2003 + */ +public class AntPathMatcher implements PathMatcher { + + /** Default path separator: "/" */ + public static final String DEFAULT_PATH_SEPARATOR = "/"; + + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?\\}"); + + + private String pathSeparator = DEFAULT_PATH_SEPARATOR; + + private boolean trimTokens = true; + + private final Map<String, AntPathStringMatcher> stringMatcherCache = + new ConcurrentHashMap<String, AntPathStringMatcher>(256); + + + /** + * Set the path separator to use for pattern parsing. + * Default is "/", as in Ant. + */ + public void setPathSeparator(String pathSeparator) { + this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); + } + + /** + * Specify whether to trim tokenized paths and patterns. + * Default is {@code true}. + */ + public void setTrimTokens(boolean trimTokens) { + this.trimTokens = trimTokens; + } + + + public boolean isPattern(String path) { + return (path.indexOf('*') != -1 || path.indexOf('?') != -1); + } + + public boolean match(String pattern, String path) { + return doMatch(pattern, path, true, null); + } + + public boolean matchStart(String pattern, String path) { + return doMatch(pattern, path, false, null); + } + + + /** + * Actually match the given {@code path} against the given {@code pattern}. + * @param pattern the pattern to match against + * @param path the path String to test + * @param fullMatch whether a full pattern match is required (else a pattern match + * as far as the given base path goes is sufficient) + * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't + */ + protected boolean doMatch(String pattern, String path, boolean fullMatch, Map<String, String> uriTemplateVariables) { + if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { + return false; + } + + String[] pattDirs = tokenizePath(pattern); + String[] pathDirs = tokenizePath(path); + + int pattIdxStart = 0; + int pattIdxEnd = pattDirs.length - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathDirs.length - 1; + + // Match all elements up to the first ** + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxStart]; + if ("**".equals(pattDir)) { + break; + } + if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + + if (pathIdxStart > pathIdxEnd) { + // Path is exhausted, only match if rest of pattern is * or **'s + if (pattIdxStart > pattIdxEnd) { + return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) : + !path.endsWith(this.pathSeparator)); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + else if (pattIdxStart > pattIdxEnd) { + // String not exhausted, but pattern is. Failure. + return false; + } + else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { + // Path start definitely matches due to "**" part in pattern. + return true; + } + + // up to last '**' + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxEnd]; + if (pattDir.equals("**")) { + break; + } + if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) { + return false; + } + pattIdxEnd--; + pathIdxEnd--; + } + if (pathIdxStart > pathIdxEnd) { + // String is exhausted + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (pattDirs[i].equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == pattIdxStart + 1) { + // '**/**' situation, so skip one + pattIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - pattIdxStart - 1); + int strLength = (pathIdxEnd - pathIdxStart + 1); + int foundIdx = -1; + + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = pattDirs[pattIdxStart + j + 1]; + String subStr = pathDirs[pathIdxStart + i + j]; + if (!matchStrings(subPat, subStr, uriTemplateVariables)) { + continue strLoop; + } + } + foundIdx = pathIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + pattIdxStart = patIdxTmp; + pathIdxStart = foundIdx + patLength; + } + + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + + return true; + } + + /** + * Tokenize the given path String into parts, based on this matcher's settings. + * @param path the path to tokenize + * @return the tokenized path parts + */ + protected String[] tokenizePath(String path) { + return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); + } + + /** + * Tests whether or not a string matches against a pattern. The pattern may contain two special characters: + * <br>'*' means zero or more characters + * <br>'?' means one and only one character + * @param pattern pattern to match against. Must not be {@code null}. + * @param str string which must be matched against the pattern. Must not be {@code null}. + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. + */ + private boolean matchStrings(String pattern, String str, Map<String, String> uriTemplateVariables) { + AntPathStringMatcher matcher = this.stringMatcherCache.get(pattern); + if (matcher == null) { + matcher = new AntPathStringMatcher(pattern); + this.stringMatcherCache.put(pattern, matcher); + } + return matcher.matchStrings(str, uriTemplateVariables); + } + + /** + * Given a pattern and a full path, determine the pattern-mapped part. <p>For example: <ul> + * <li>'{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''</li> + * <li>'{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li> + * <li>'{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'</li> + * <li>'{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li> + * <li>'{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'</li> + * <li>'{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'</li> + * <li>'{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'</li> + * <li>'{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'</li> </ul> + * <p>Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but + * does <strong>not</strong> enforce this. + */ + public String extractPathWithinPattern(String pattern, String path) { + String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator, this.trimTokens, true); + String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); + + StringBuilder builder = new StringBuilder(); + + // Add any path parts that have a wildcarded pattern part. + int puts = 0; + for (int i = 0; i < patternParts.length; i++) { + String patternPart = patternParts[i]; + if ((patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) && pathParts.length >= i + 1) { + if (puts > 0 || (i == 0 && !pattern.startsWith(this.pathSeparator))) { + builder.append(this.pathSeparator); + } + builder.append(pathParts[i]); + puts++; + } + } + + // Append any trailing path parts. + for (int i = patternParts.length; i < pathParts.length; i++) { + if (puts > 0 || i > 0) { + builder.append(this.pathSeparator); + } + builder.append(pathParts[i]); + } + + return builder.toString(); + } + + public Map<String, String> extractUriTemplateVariables(String pattern, String path) { + Map<String, String> variables = new LinkedHashMap<String, String>(); + boolean result = doMatch(pattern, path, true, variables); + Assert.state(result, "Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); + return variables; + } + + /** + * Combines two patterns into a new pattern that is returned. + * <p>This implementation simply concatenates the two patterns, unless the first pattern + * contains a file extension match (such as {@code *.html}. In that case, the second pattern + * should be included in the first, or an {@code IllegalArgumentException} is thrown. + * <p>For example: <table> + * <tr><th>Pattern 1</th><th>Pattern 2</th><th>Result</th></tr> <tr><td>/hotels</td><td>{@code + * null}</td><td>/hotels</td></tr> <tr><td>{@code null}</td><td>/hotels</td><td>/hotels</td></tr> + * <tr><td>/hotels</td><td>/bookings</td><td>/hotels/bookings</td></tr> <tr><td>/hotels</td><td>bookings</td><td>/hotels/bookings</td></tr> + * <tr><td>/hotels/*</td><td>/bookings</td><td>/hotels/bookings</td></tr> <tr><td>/hotels/**</td><td>/bookings</td><td>/hotels/**/bookings</td></tr> + * <tr><td>/hotels</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr> <tr><td>/hotels/*</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr> + * <tr><td>/hotels/**</td><td>{hotel}</td><td>/hotels/**/{hotel}</td></tr> + * <tr><td>/*.html</td><td>/hotels.html</td><td>/hotels.html</td></tr> <tr><td>/*.html</td><td>/hotels</td><td>/hotels.html</td></tr> + * <tr><td>/*.html</td><td>/*.txt</td><td>IllegalArgumentException</td></tr> </table> + * @param pattern1 the first pattern + * @param pattern2 the second pattern + * @return the combination of the two patterns + * @throws IllegalArgumentException when the two patterns cannot be combined + */ + public String combine(String pattern1, String pattern2) { + if (!StringUtils.hasText(pattern1) && !StringUtils.hasText(pattern2)) { + return ""; + } + else if (!StringUtils.hasText(pattern1)) { + return pattern2; + } + else if (!StringUtils.hasText(pattern2)) { + return pattern1; + } + + boolean pattern1ContainsUriVar = pattern1.indexOf('{') != -1; + if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) { + // /* + /hotel -> /hotel ; "/*.*" + "/*.html" -> /*.html + // However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar + return pattern2; + } + else if (pattern1.endsWith("/*")) { + if (pattern2.startsWith("/")) { + // /hotels/* + /booking -> /hotels/booking + return pattern1.substring(0, pattern1.length() - 1) + pattern2.substring(1); + } + else { + // /hotels/* + booking -> /hotels/booking + return pattern1.substring(0, pattern1.length() - 1) + pattern2; + } + } + else if (pattern1.endsWith("/**")) { + if (pattern2.startsWith("/")) { + // /hotels/** + /booking -> /hotels/**/booking + return pattern1 + pattern2; + } + else { + // /hotels/** + booking -> /hotels/**/booking + return pattern1 + "/" + pattern2; + } + } + else { + int dotPos1 = pattern1.indexOf('.'); + if (dotPos1 == -1 || pattern1ContainsUriVar) { + // simply concatenate the two patterns + if (pattern1.endsWith("/") || pattern2.startsWith("/")) { + return pattern1 + pattern2; + } + else { + return pattern1 + "/" + pattern2; + } + } + String fileName1 = pattern1.substring(0, dotPos1); + String extension1 = pattern1.substring(dotPos1); + String fileName2; + String extension2; + int dotPos2 = pattern2.indexOf('.'); + if (dotPos2 != -1) { + fileName2 = pattern2.substring(0, dotPos2); + extension2 = pattern2.substring(dotPos2); + } + else { + fileName2 = pattern2; + extension2 = ""; + } + String fileName = fileName1.endsWith("*") ? fileName2 : fileName1; + String extension = extension1.startsWith("*") ? extension2 : extension1; + + return fileName + extension; + } + } + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of explicitness. + * <p>The returned {@code Comparator} will {@linkplain java.util.Collections#sort(java.util.List, + * java.util.Comparator) sort} a list so that more specific patterns (without uri templates or wild cards) come before + * generic patterns. So given a list with the following patterns: <ol> <li>{@code /hotels/new}</li> + * <li>{@code /hotels/{hotel}}</li> <li>{@code /hotels/*}</li> </ol> the returned comparator will sort this + * list so that the order will be as indicated. + * <p>The full path given as parameter is used to test for exact matches. So when the given path is {@code /hotels/2}, + * the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}. + * @param path the full path to use for comparison + * @return a comparator capable of sorting patterns in order of explicitness + */ + public Comparator<String> getPatternComparator(String path) { + return new AntPatternComparator(path); + } + + + private static class AntPatternComparator implements Comparator<String> { + + private final String path; + + private AntPatternComparator(String path) { + this.path = path; + } + + public int compare(String pattern1, String pattern2) { + if (pattern1 == null && pattern2 == null) { + return 0; + } + else if (pattern1 == null) { + return 1; + } + else if (pattern2 == null) { + return -1; + } + boolean pattern1EqualsPath = pattern1.equals(path); + boolean pattern2EqualsPath = pattern2.equals(path); + if (pattern1EqualsPath && pattern2EqualsPath) { + return 0; + } + else if (pattern1EqualsPath) { + return -1; + } + else if (pattern2EqualsPath) { + return 1; + } + int wildCardCount1 = getWildCardCount(pattern1); + int wildCardCount2 = getWildCardCount(pattern2); + + int bracketCount1 = StringUtils.countOccurrencesOf(pattern1, "{"); + int bracketCount2 = StringUtils.countOccurrencesOf(pattern2, "{"); + + int totalCount1 = wildCardCount1 + bracketCount1; + int totalCount2 = wildCardCount2 + bracketCount2; + + if (totalCount1 != totalCount2) { + return totalCount1 - totalCount2; + } + + int pattern1Length = getPatternLength(pattern1); + int pattern2Length = getPatternLength(pattern2); + + if (pattern1Length != pattern2Length) { + return pattern2Length - pattern1Length; + } + + if (wildCardCount1 < wildCardCount2) { + return -1; + } + else if (wildCardCount2 < wildCardCount1) { + return 1; + } + + if (bracketCount1 < bracketCount2) { + return -1; + } + else if (bracketCount2 < bracketCount1) { + return 1; + } + + return 0; + } + + private int getWildCardCount(String pattern) { + if (pattern.endsWith(".*")) { + pattern = pattern.substring(0, pattern.length() - 2); + } + return StringUtils.countOccurrencesOf(pattern, "*"); + } + + /** + * Returns the length of the given pattern, where template variables are considered to be 1 long. + */ + private int getPatternLength(String pattern) { + Matcher m = VARIABLE_PATTERN.matcher(pattern); + return m.replaceAll("#").length(); + } + } + + + /** + * Tests whether or not a string matches against a pattern via a {@link Pattern}. + * <p>The pattern may contain special characters: '*' means zero or more characters; '?' means one and + * only one character; '{' and '}' indicate a URI template pattern. For example <tt>/users/{user}</tt>. + */ + private static class AntPathStringMatcher { + + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); + + private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; + + private final Pattern pattern; + + private final List<String> variableNames = new LinkedList<String>(); + + public AntPathStringMatcher(String pattern) { + StringBuilder patternBuilder = new StringBuilder(); + Matcher m = GLOB_PATTERN.matcher(pattern); + int end = 0; + while (m.find()) { + patternBuilder.append(quote(pattern, end, m.start())); + String match = m.group(); + if ("?".equals(match)) { + patternBuilder.append('.'); + } + else if ("*".equals(match)) { + patternBuilder.append(".*"); + } + else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + patternBuilder.append(DEFAULT_VARIABLE_PATTERN); + this.variableNames.add(m.group(1)); + } + else { + String variablePattern = match.substring(colonIdx + 1, match.length() - 1); + patternBuilder.append('('); + patternBuilder.append(variablePattern); + patternBuilder.append(')'); + String variableName = match.substring(1, colonIdx); + this.variableNames.add(variableName); + } + } + end = m.end(); + } + patternBuilder.append(quote(pattern, end, pattern.length())); + this.pattern = Pattern.compile(patternBuilder.toString()); + } + + private String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return Pattern.quote(s.substring(start, end)); + } + + /** + * Main entry point. + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. + */ + public boolean matchStrings(String str, Map<String, String> uriTemplateVariables) { + Matcher matcher = this.pattern.matcher(str); + if (matcher.matches()) { + if (uriTemplateVariables != null) { + // SPR-8455 + Assert.isTrue(this.variableNames.size() == matcher.groupCount(), + "The number of capturing groups in the pattern segment " + this.pattern + + " does not match the number of URI template variables it defines, which can occur if " + + " capturing groups are used in a URI template regex. Use non-capturing groups instead."); + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = this.variableNames.get(i - 1); + String value = matcher.group(i); + uriTemplateVariables.put(name, value); + } + } + return true; + } + else { + return false; + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/Assert.java b/spring-core/src/main/java/org/springframework/util/Assert.java new file mode 100644 index 00000000..55b2325d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/Assert.java @@ -0,0 +1,402 @@ +/* + * Copyright 2002-2013 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.util; + +import java.util.Collection; +import java.util.Map; + +/** + * Assertion utility class that assists in validating arguments. + * Useful for identifying programmer errors early and clearly at runtime. + * + * <p>For example, if the contract of a public method states it does not + * allow {@code null} arguments, Assert can be used to validate that + * contract. Doing this clearly indicates a contract violation when it + * occurs and protects the class's invariants. + * + * <p>Typically used to validate method arguments rather than configuration + * properties, to check for cases that are usually programmer errors rather than + * configuration errors. In contrast to config initialization code, there is + * usally no point in falling back to defaults in such methods. + * + * <p>This class is similar to JUnit's assertion library. If an argument value is + * deemed invalid, an {@link IllegalArgumentException} is thrown (typically). + * For example: + * + * <pre class="code"> + * Assert.notNull(clazz, "The class must not be null"); + * Assert.isTrue(i > 0, "The value must be greater than zero");</pre> + * + * Mainly for internal use within the framework; consider Jakarta's Commons Lang + * >= 2.0 for a more comprehensive suite of assertion utilities. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Colin Sampaleanu + * @author Rob Harrop + * @since 1.1.2 + */ +public abstract class Assert { + + /** + * Assert a boolean expression, throwing {@code IllegalArgumentException} + * if the test result is {@code false}. + * <pre class="code">Assert.isTrue(i > 0, "The value must be greater than zero");</pre> + * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if expression is {@code false} + */ + public static void isTrue(boolean expression, String message) { + if (!expression) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert a boolean expression, throwing {@code IllegalArgumentException} + * if the test result is {@code false}. + * <pre class="code">Assert.isTrue(i > 0);</pre> + * @param expression a boolean expression + * @throws IllegalArgumentException if expression is {@code false} + */ + public static void isTrue(boolean expression) { + isTrue(expression, "[Assertion failed] - this expression must be true"); + } + + /** + * Assert that an object is {@code null} . + * <pre class="code">Assert.isNull(value, "The value must be null");</pre> + * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is not {@code null} + */ + public static void isNull(Object object, String message) { + if (object != null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is {@code null} . + * <pre class="code">Assert.isNull(value);</pre> + * @param object the object to check + * @throws IllegalArgumentException if the object is not {@code null} + */ + public static void isNull(Object object) { + isNull(object, "[Assertion failed] - the object argument must be null"); + } + + /** + * Assert that an object is not {@code null} . + * <pre class="code">Assert.notNull(clazz, "The class must not be null");</pre> + * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is {@code null} + */ + public static void notNull(Object object, String message) { + if (object == null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is not {@code null} . + * <pre class="code">Assert.notNull(clazz);</pre> + * @param object the object to check + * @throws IllegalArgumentException if the object is {@code null} + */ + public static void notNull(Object object) { + notNull(object, "[Assertion failed] - this argument is required; it must not be null"); + } + + /** + * Assert that the given String is not empty; that is, + * it must not be {@code null} and not the empty String. + * <pre class="code">Assert.hasLength(name, "Name must not be empty");</pre> + * @param text the String to check + * @param message the exception message to use if the assertion fails + * @see StringUtils#hasLength + */ + public static void hasLength(String text, String message) { + if (!StringUtils.hasLength(text)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String is not empty; that is, + * it must not be {@code null} and not the empty String. + * <pre class="code">Assert.hasLength(name);</pre> + * @param text the String to check + * @see StringUtils#hasLength + */ + public static void hasLength(String text) { + hasLength(text, + "[Assertion failed] - this String argument must have length; it must not be null or empty"); + } + + /** + * Assert that the given String has valid text content; that is, it must not + * be {@code null} and must contain at least one non-whitespace character. + * <pre class="code">Assert.hasText(name, "'name' must not be empty");</pre> + * @param text the String to check + * @param message the exception message to use if the assertion fails + * @see StringUtils#hasText + */ + public static void hasText(String text, String message) { + if (!StringUtils.hasText(text)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String has valid text content; that is, it must not + * be {@code null} and must contain at least one non-whitespace character. + * <pre class="code">Assert.hasText(name, "'name' must not be empty");</pre> + * @param text the String to check + * @see StringUtils#hasText + */ + public static void hasText(String text) { + hasText(text, + "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); + } + + /** + * Assert that the given text does not contain the given substring. + * <pre class="code">Assert.doesNotContain(name, "rod", "Name must not contain 'rod'");</pre> + * @param textToSearch the text to search + * @param substring the substring to find within the text + * @param message the exception message to use if the assertion fails + */ + public static void doesNotContain(String textToSearch, String substring, String message) { + if (StringUtils.hasLength(textToSearch) && StringUtils.hasLength(substring) && + textToSearch.contains(substring)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given text does not contain the given substring. + * <pre class="code">Assert.doesNotContain(name, "rod");</pre> + * @param textToSearch the text to search + * @param substring the substring to find within the text + */ + public static void doesNotContain(String textToSearch, String substring) { + doesNotContain(textToSearch, substring, + "[Assertion failed] - this String argument must not contain the substring [" + substring + "]"); + } + + + /** + * Assert that an array has elements; that is, it must not be + * {@code null} and must have at least one element. + * <pre class="code">Assert.notEmpty(array, "The array must have elements");</pre> + * @param array the array to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object array is {@code null} or has no elements + */ + public static void notEmpty(Object[] array, String message) { + if (ObjectUtils.isEmpty(array)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an array has elements; that is, it must not be + * {@code null} and must have at least one element. + * <pre class="code">Assert.notEmpty(array);</pre> + * @param array the array to check + * @throws IllegalArgumentException if the object array is {@code null} or has no elements + */ + public static void notEmpty(Object[] array) { + notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element"); + } + + /** + * Assert that an array has no null elements. + * Note: Does not complain if the array is empty! + * <pre class="code">Assert.noNullElements(array, "The array must have non-null elements");</pre> + * @param array the array to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object array contains a {@code null} element + */ + public static void noNullElements(Object[] array, String message) { + if (array != null) { + for (Object element : array) { + if (element == null) { + throw new IllegalArgumentException(message); + } + } + } + } + + /** + * Assert that an array has no null elements. + * Note: Does not complain if the array is empty! + * <pre class="code">Assert.noNullElements(array);</pre> + * @param array the array to check + * @throws IllegalArgumentException if the object array contains a {@code null} element + */ + public static void noNullElements(Object[] array) { + noNullElements(array, "[Assertion failed] - this array must not contain any null elements"); + } + + /** + * Assert that a collection has elements; that is, it must not be + * {@code null} and must have at least one element. + * <pre class="code">Assert.notEmpty(collection, "Collection must have elements");</pre> + * @param collection the collection to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the collection is {@code null} or has no elements + */ + public static void notEmpty(Collection collection, String message) { + if (CollectionUtils.isEmpty(collection)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a collection has elements; that is, it must not be + * {@code null} and must have at least one element. + * <pre class="code">Assert.notEmpty(collection, "Collection must have elements");</pre> + * @param collection the collection to check + * @throws IllegalArgumentException if the collection is {@code null} or has no elements + */ + public static void notEmpty(Collection collection) { + notEmpty(collection, + "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); + } + + /** + * Assert that a Map has entries; that is, it must not be {@code null} + * and must have at least one entry. + * <pre class="code">Assert.notEmpty(map, "Map must have entries");</pre> + * @param map the map to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the map is {@code null} or has no entries + */ + public static void notEmpty(Map map, String message) { + if (CollectionUtils.isEmpty(map)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a Map has entries; that is, it must not be {@code null} + * and must have at least one entry. + * <pre class="code">Assert.notEmpty(map);</pre> + * @param map the map to check + * @throws IllegalArgumentException if the map is {@code null} or has no entries + */ + public static void notEmpty(Map map) { + notEmpty(map, "[Assertion failed] - this map must not be empty; it must contain at least one entry"); + } + + + /** + * Assert that the provided object is an instance of the provided class. + * <pre class="code">Assert.instanceOf(Foo.class, foo);</pre> + * @param clazz the required class + * @param obj the object to check + * @throws IllegalArgumentException if the object is not an instance of clazz + * @see Class#isInstance + */ + public static void isInstanceOf(Class<?> clazz, Object obj) { + isInstanceOf(clazz, obj, ""); + } + + /** + * Assert that the provided object is an instance of the provided class. + * <pre class="code">Assert.instanceOf(Foo.class, foo);</pre> + * @param type the type to check against + * @param obj the object to check + * @param message a message which will be prepended to the message produced by + * the function itself, and which may be used to provide context. It should + * normally end in a ": " or ". " so that the function generate message looks + * ok when prepended to it. + * @throws IllegalArgumentException if the object is not an instance of clazz + * @see Class#isInstance + */ + public static void isInstanceOf(Class<?> type, Object obj, String message) { + notNull(type, "Type to check against must not be null"); + if (!type.isInstance(obj)) { + throw new IllegalArgumentException( + (StringUtils.hasLength(message) ? message + " " : "") + + "Object of class [" + (obj != null ? obj.getClass().getName() : "null") + + "] must be an instance of " + type); + } + } + + /** + * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. + * <pre class="code">Assert.isAssignable(Number.class, myClass);</pre> + * @param superType the super type to check + * @param subType the sub type to check + * @throws IllegalArgumentException if the classes are not assignable + */ + public static void isAssignable(Class<?> superType, Class<?> subType) { + isAssignable(superType, subType, ""); + } + + /** + * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. + * <pre class="code">Assert.isAssignable(Number.class, myClass);</pre> + * @param superType the super type to check against + * @param subType the sub type to check + * @param message a message which will be prepended to the message produced by + * the function itself, and which may be used to provide context. It should + * normally end in a ": " or ". " so that the function generate message looks + * ok when prepended to it. + * @throws IllegalArgumentException if the classes are not assignable + */ + public static void isAssignable(Class<?> superType, Class<?> subType, String message) { + notNull(superType, "Type to check against must not be null"); + if (subType == null || !superType.isAssignableFrom(subType)) { + throw new IllegalArgumentException(message + subType + " is not assignable to " + superType); + } + } + + + /** + * Assert a boolean expression, throwing {@code IllegalStateException} + * if the test result is {@code false}. Call isTrue if you wish to + * throw IllegalArgumentException on an assertion failure. + * <pre class="code">Assert.state(id == null, "The id property must not already be initialized");</pre> + * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalStateException if expression is {@code false} + */ + public static void state(boolean expression, String message) { + if (!expression) { + throw new IllegalStateException(message); + } + } + + /** + * Assert a boolean expression, throwing {@link IllegalStateException} + * if the test result is {@code false}. + * <p>Call {@link #isTrue(boolean)} if you wish to + * throw {@link IllegalArgumentException} on an assertion failure. + * <pre class="code">Assert.state(id == null);</pre> + * @param expression a boolean expression + * @throws IllegalStateException if the supplied expression is {@code false} + */ + public static void state(boolean expression) { + state(expression, "[Assertion failed] - this state invariant must be true"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java b/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java new file mode 100644 index 00000000..7a2135fb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java @@ -0,0 +1,277 @@ +/* + * Copyright 2002-2012 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.util; + +import java.io.Serializable; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * Simple {@link List} wrapper class that allows for elements to be + * automatically populated as they are requested. This is particularly + * useful for data binding to {@link List Lists}, allowing for elements + * to be created and added to the {@link List} in a "just in time" fashion. + * + * <p>Note: This class is not thread-safe. To create a thread-safe version, + * use the {@link java.util.Collections#synchronizedList} utility methods. + * + * <p>Inspired by {@code LazyList} from Commons Collections. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public class AutoPopulatingList<E> implements List<E>, Serializable { + + /** + * The {@link List} that all operations are eventually delegated to. + */ + private final List<E> backingList; + + /** + * The {@link ElementFactory} to use to create new {@link List} elements + * on demand. + */ + private final ElementFactory<E> elementFactory; + + + /** + * Creates a new {@code AutoPopulatingList} that is backed by a standard + * {@link ArrayList} and adds new instances of the supplied {@link Class element Class} + * to the backing {@link List} on demand. + */ + public AutoPopulatingList(Class<? extends E> elementClass) { + this(new ArrayList<E>(), elementClass); + } + + /** + * Creates a new {@code AutoPopulatingList} that is backed by the supplied {@link List} + * and adds new instances of the supplied {@link Class element Class} to the backing + * {@link List} on demand. + */ + public AutoPopulatingList(List<E> backingList, Class<? extends E> elementClass) { + this(backingList, new ReflectiveElementFactory<E>(elementClass)); + } + + /** + * Creates a new {@code AutoPopulatingList} that is backed by a standard + * {@link ArrayList} and creates new elements on demand using the supplied {@link ElementFactory}. + */ + public AutoPopulatingList(ElementFactory<E> elementFactory) { + this(new ArrayList<E>(), elementFactory); + } + + /** + * Creates a new {@code AutoPopulatingList} that is backed by the supplied {@link List} + * and creates new elements on demand using the supplied {@link ElementFactory}. + */ + public AutoPopulatingList(List<E> backingList, ElementFactory<E> elementFactory) { + Assert.notNull(backingList, "Backing List must not be null"); + Assert.notNull(elementFactory, "Element factory must not be null"); + this.backingList = backingList; + this.elementFactory = elementFactory; + } + + + public void add(int index, E element) { + this.backingList.add(index, element); + } + + public boolean add(E o) { + return this.backingList.add(o); + } + + public boolean addAll(Collection<? extends E> c) { + return this.backingList.addAll(c); + } + + public boolean addAll(int index, Collection<? extends E> c) { + return this.backingList.addAll(index, c); + } + + public void clear() { + this.backingList.clear(); + } + + public boolean contains(Object o) { + return this.backingList.contains(o); + } + + public boolean containsAll(Collection c) { + return this.backingList.containsAll(c); + } + + /** + * Get the element at the supplied index, creating it if there is + * no element at that index. + */ + public E get(int index) { + int backingListSize = this.backingList.size(); + E element = null; + if (index < backingListSize) { + element = this.backingList.get(index); + if (element == null) { + element = this.elementFactory.createElement(index); + this.backingList.set(index, element); + } + } + else { + for (int x = backingListSize; x < index; x++) { + this.backingList.add(null); + } + element = this.elementFactory.createElement(index); + this.backingList.add(element); + } + return element; + } + + public int indexOf(Object o) { + return this.backingList.indexOf(o); + } + + public boolean isEmpty() { + return this.backingList.isEmpty(); + } + + public Iterator<E> iterator() { + return this.backingList.iterator(); + } + + public int lastIndexOf(Object o) { + return this.backingList.lastIndexOf(o); + } + + public ListIterator<E> listIterator() { + return this.backingList.listIterator(); + } + + public ListIterator<E> listIterator(int index) { + return this.backingList.listIterator(index); + } + + public E remove(int index) { + return this.backingList.remove(index); + } + + public boolean remove(Object o) { + return this.backingList.remove(o); + } + + public boolean removeAll(Collection<?> c) { + return this.backingList.removeAll(c); + } + + public boolean retainAll(Collection<?> c) { + return this.backingList.retainAll(c); + } + + public E set(int index, E element) { + return this.backingList.set(index, element); + } + + public int size() { + return this.backingList.size(); + } + + public List<E> subList(int fromIndex, int toIndex) { + return this.backingList.subList(fromIndex, toIndex); + } + + public Object[] toArray() { + return this.backingList.toArray(); + } + + public <T> T[] toArray(T[] a) { + return this.backingList.toArray(a); + } + + + @Override + public boolean equals(Object other) { + return this.backingList.equals(other); + } + + @Override + public int hashCode() { + return this.backingList.hashCode(); + } + + + /** + * Factory interface for creating elements for an index-based access + * data structure such as a {@link java.util.List}. + */ + public interface ElementFactory<E> { + + /** + * Create the element for the supplied index. + * @return the element object + * @throws ElementInstantiationException if the instantiation process failed + * (any exception thrown by a target constructor should be propagated as-is) + */ + E createElement(int index) throws ElementInstantiationException; + } + + + /** + * Exception to be thrown from ElementFactory. + */ + public static class ElementInstantiationException extends RuntimeException { + + public ElementInstantiationException(String msg) { + super(msg); + } + } + + + /** + * Reflective implementation of the ElementFactory interface, + * using {@code Class.newInstance()} on a given element class. + * @see Class#newInstance() + */ + private static class ReflectiveElementFactory<E> implements ElementFactory<E>, Serializable { + + private final Class<? extends E> elementClass; + + public ReflectiveElementFactory(Class<? extends E> elementClass) { + Assert.notNull(elementClass, "Element clas must not be null"); + Assert.isTrue(!elementClass.isInterface(), "Element class must not be an interface type"); + Assert.isTrue(!Modifier.isAbstract(elementClass.getModifiers()), "Element class cannot be an abstract class"); + this.elementClass = elementClass; + } + + public E createElement(int index) { + try { + return this.elementClass.newInstance(); + } + catch (InstantiationException ex) { + throw new ElementInstantiationException("Unable to instantiate element class [" + + this.elementClass.getName() + "]. Root cause is " + ex); + } + catch (IllegalAccessException ex) { + throw new ElementInstantiationException("Cannot access element class [" + + this.elementClass.getName() + "]. Root cause is " + ex); + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/CachingMapDecorator.java b/spring-core/src/main/java/org/springframework/util/CachingMapDecorator.java new file mode 100644 index 00000000..d1290588 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CachingMapDecorator.java @@ -0,0 +1,311 @@ +/* + * Copyright 2002-2012 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.util; + +import java.io.Serializable; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * A simple decorator for a Map, encapsulating the workflow for caching + * expensive values in a target Map. Supports caching weak or strong keys. + * + * <p>This class is an abstract template. Caching Map implementations + * should subclass and override the {@code create(key)} method which + * encapsulates expensive creation of a new object. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + * @deprecated as of Spring 3.2, to be removed along with LabeledEnum support + */ +@Deprecated +@SuppressWarnings("serial") +public abstract class CachingMapDecorator<K, V> implements Map<K, V>, Serializable { + + private static Object NULL_VALUE = new Object(); + + + private final Map<K, Object> targetMap; + + private final boolean synchronize; + + private final boolean weak; + + + /** + * Create a CachingMapDecorator with strong keys, + * using an underlying synchronized Map. + */ + public CachingMapDecorator() { + this(false); + } + + /** + * Create a CachingMapDecorator, + * using an underlying synchronized Map. + * @param weak whether to use weak references for keys and values + */ + public CachingMapDecorator(boolean weak) { + Map<K, Object> internalMap = (weak ? new WeakHashMap<K, Object>() : new HashMap<K, Object>()); + this.targetMap = Collections.synchronizedMap(internalMap); + this.synchronize = true; + this.weak = weak; + } + + /** + * Create a CachingMapDecorator with initial size, + * using an underlying synchronized Map. + * @param weak whether to use weak references for keys and values + * @param size the initial cache size + */ + public CachingMapDecorator(boolean weak, int size) { + Map<K, Object> internalMap = weak ? new WeakHashMap<K, Object> (size) : new HashMap<K, Object>(size); + this.targetMap = Collections.synchronizedMap(internalMap); + this.synchronize = true; + this.weak = weak; + } + + /** + * Create a CachingMapDecorator for the given Map. + * <p>The passed-in Map won't get synchronized explicitly, + * so make sure to pass in a properly synchronized Map, if desired. + * @param targetMap the Map to decorate + */ + public CachingMapDecorator(Map<K, V> targetMap) { + this(targetMap, false, false); + } + + /** + * Create a CachingMapDecorator for the given Map. + * <p>The passed-in Map won't get synchronized explicitly unless + * you specify "synchronize" as "true". + * @param targetMap the Map to decorate + * @param synchronize whether to synchronize on the given Map + * @param weak whether to use weak references for values + */ + @SuppressWarnings("unchecked") + public CachingMapDecorator(Map<K, V> targetMap, boolean synchronize, boolean weak) { + Assert.notNull(targetMap, "'targetMap' must not be null"); + this.targetMap = (Map<K, Object>) (synchronize ? Collections.synchronizedMap(targetMap) : targetMap); + this.synchronize = synchronize; + this.weak = weak; + } + + + public int size() { + return this.targetMap.size(); + } + + public boolean isEmpty() { + return this.targetMap.isEmpty(); + } + + public boolean containsKey(Object key) { + return this.targetMap.containsKey(key); + } + + public boolean containsValue(Object value) { + Object valueToCheck = (value != null ? value : NULL_VALUE); + if (this.synchronize) { + synchronized (this.targetMap) { + return containsValueOrReference(valueToCheck); + } + } + else { + return containsValueOrReference(valueToCheck); + } + } + + private boolean containsValueOrReference(Object value) { + if (this.targetMap.containsValue(value)) { + return true; + } + for (Object mapVal : this.targetMap.values()) { + if (mapVal instanceof Reference && value.equals(((Reference) mapVal).get())) { + return true; + } + } + return false; + } + + public V remove(Object key) { + return unwrapReturnValue(this.targetMap.remove(key)); + } + + @SuppressWarnings("unchecked") + private V unwrapReturnValue(Object value) { + Object returnValue = value; + if (returnValue instanceof Reference) { + returnValue = ((Reference) returnValue).get(); + } + return (returnValue == NULL_VALUE ? null : (V) returnValue); + } + + public void putAll(Map<? extends K, ? extends V> map) { + this.targetMap.putAll(map); + } + + public void clear() { + this.targetMap.clear(); + } + + public Set<K> keySet() { + if (this.synchronize) { + synchronized (this.targetMap) { + return new LinkedHashSet<K>(this.targetMap.keySet()); + } + } + else { + return new LinkedHashSet<K>(this.targetMap.keySet()); + } + } + + public Collection<V> values() { + if (this.synchronize) { + synchronized (this.targetMap) { + return valuesCopy(); + } + } + else { + return valuesCopy(); + } + } + + @SuppressWarnings("unchecked") + private Collection<V> valuesCopy() { + LinkedList<V> values = new LinkedList<V>(); + for (Iterator<Object> it = this.targetMap.values().iterator(); it.hasNext();) { + Object value = it.next(); + if (value instanceof Reference) { + value = ((Reference) value).get(); + if (value == null) { + it.remove(); + continue; + } + } + values.add(value == NULL_VALUE ? null : (V) value); + } + return values; + } + + public Set<Map.Entry<K, V>> entrySet() { + if (this.synchronize) { + synchronized (this.targetMap) { + return entryCopy(); + } + } + else { + return entryCopy(); + } + } + + @SuppressWarnings("unchecked") + private Set<Map.Entry<K, V>> entryCopy() { + Map<K,V> entries = new LinkedHashMap<K, V>(); + for (Iterator<Entry<K, Object>> it = this.targetMap.entrySet().iterator(); it.hasNext();) { + Entry<K, Object> entry = it.next(); + Object value = entry.getValue(); + if (value instanceof Reference) { + value = ((Reference) value).get(); + if (value == null) { + it.remove(); + continue; + } + } + entries.put(entry.getKey(), value == NULL_VALUE ? null : (V) value); + } + return entries.entrySet(); + } + + + /** + * Put an object into the cache, possibly wrapping it with a weak + * reference. + * @see #useWeakValue(Object, Object) + */ + public V put(K key, V value) { + Object newValue = value; + if (value == null) { + newValue = NULL_VALUE; + } + else if (useWeakValue(key, value)) { + newValue = new WeakReference<Object>(newValue); + } + return unwrapReturnValue(this.targetMap.put(key, newValue)); + } + + /** + * Decide whether to use a weak reference for the value of + * the given key-value pair. + * @param key the candidate key + * @param value the candidate value + * @return {@code true} in order to use a weak reference; + * {@code false} otherwise. + */ + protected boolean useWeakValue(K key, V value) { + return this.weak; + } + + /** + * Get value for key. + * Creates and caches value if it doesn't already exist in the cache. + * <p>This implementation is <i>not</i> synchronized: This is highly + * concurrent but does not guarantee unique instances in the cache, + * as multiple values for the same key could get created in parallel. + * Consider overriding this method to synchronize it, if desired. + * @see #create(Object) + */ + @SuppressWarnings("unchecked") + public V get(Object key) { + Object value = this.targetMap.get(key); + if (value instanceof Reference) { + value = ((Reference) value).get(); + } + if (value == null) { + V newValue = create((K) key); + put((K) key, newValue); + return newValue; + } + return (value == NULL_VALUE ? null : (V) value); + } + + /** + * Create a value to cache for the given key. + * Called by {@code get} if there is no value cached already. + * @param key the cache key + * @see #get(Object) + */ + protected abstract V create(K key); + + + @Override + public String toString() { + return "CachingMapDecorator [" + getClass().getName() + "]:" + this.targetMap; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java new file mode 100644 index 00000000..4bac3488 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -0,0 +1,1251 @@ +/* + * Copyright 2002-2014 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.util; + +import java.beans.Introspector; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * Miscellaneous class utility methods. + * Mainly for internal use within the framework. + * + * @author Juergen Hoeller + * @author Keith Donald + * @author Rob Harrop + * @author Sam Brannen + * @since 1.1 + * @see TypeUtils + * @see ReflectionUtils + */ +public abstract class ClassUtils { + + /** Suffix for array class names: "[]" */ + public static final String ARRAY_SUFFIX = "[]"; + + /** Prefix for internal array class names: "[" */ + private static final String INTERNAL_ARRAY_PREFIX = "["; + + /** Prefix for internal non-primitive array class names: "[L" */ + private static final String NON_PRIMITIVE_ARRAY_PREFIX = "[L"; + + /** The package separator character '.' */ + private static final char PACKAGE_SEPARATOR = '.'; + + /** The inner class separator character '$' */ + private static final char INNER_CLASS_SEPARATOR = '$'; + + /** The CGLIB class separator character "$$" */ + public static final String CGLIB_CLASS_SEPARATOR = "$$"; + + /** The ".class" file suffix */ + public static final String CLASS_FILE_SUFFIX = ".class"; + + + /** + * Map with primitive wrapper type as key and corresponding primitive + * type as value, for example: Integer.class -> int.class. + */ + private static final Map<Class<?>, Class<?>> primitiveWrapperTypeMap = new HashMap<Class<?>, Class<?>>(8); + + /** + * Map with primitive type as key and corresponding wrapper + * type as value, for example: int.class -> Integer.class. + */ + private static final Map<Class<?>, Class<?>> primitiveTypeToWrapperMap = new HashMap<Class<?>, Class<?>>(8); + + /** + * Map with primitive type name as key and corresponding primitive + * type as value, for example: "int" -> "int.class". + */ + private static final Map<String, Class<?>> primitiveTypeNameMap = new HashMap<String, Class<?>>(32); + + /** + * Map with common "java.lang" class name as key and corresponding Class as value. + * Primarily for efficient deserialization of remote invocations. + */ + private static final Map<String, Class<?>> commonClassCache = new HashMap<String, Class<?>>(32); + + + static { + primitiveWrapperTypeMap.put(Boolean.class, boolean.class); + primitiveWrapperTypeMap.put(Byte.class, byte.class); + primitiveWrapperTypeMap.put(Character.class, char.class); + primitiveWrapperTypeMap.put(Double.class, double.class); + primitiveWrapperTypeMap.put(Float.class, float.class); + primitiveWrapperTypeMap.put(Integer.class, int.class); + primitiveWrapperTypeMap.put(Long.class, long.class); + primitiveWrapperTypeMap.put(Short.class, short.class); + + for (Map.Entry<Class<?>, Class<?>> entry : primitiveWrapperTypeMap.entrySet()) { + primitiveTypeToWrapperMap.put(entry.getValue(), entry.getKey()); + registerCommonClasses(entry.getKey()); + } + + Set<Class<?>> primitiveTypes = new HashSet<Class<?>>(32); + primitiveTypes.addAll(primitiveWrapperTypeMap.values()); + primitiveTypes.addAll(Arrays.asList(new Class<?>[] { + boolean[].class, byte[].class, char[].class, double[].class, + float[].class, int[].class, long[].class, short[].class})); + primitiveTypes.add(void.class); + for (Class<?> primitiveType : primitiveTypes) { + primitiveTypeNameMap.put(primitiveType.getName(), primitiveType); + } + + registerCommonClasses(Boolean[].class, Byte[].class, Character[].class, Double[].class, + Float[].class, Integer[].class, Long[].class, Short[].class); + registerCommonClasses(Number.class, Number[].class, String.class, String[].class, + Object.class, Object[].class, Class.class, Class[].class); + registerCommonClasses(Throwable.class, Exception.class, RuntimeException.class, + Error.class, StackTraceElement.class, StackTraceElement[].class); + } + + + /** + * Register the given common classes with the ClassUtils cache. + */ + private static void registerCommonClasses(Class<?>... commonClasses) { + for (Class<?> clazz : commonClasses) { + commonClassCache.put(clazz.getName(), clazz); + } + } + + /** + * Return the default ClassLoader to use: typically the thread context + * ClassLoader, if available; the ClassLoader that loaded the ClassUtils + * class will be used as fallback. + * <p>Call this method if you intend to use the thread context ClassLoader + * in a scenario where you clearly prefer a non-null ClassLoader reference: + * for example, for class path resource loading (but not necessarily for + * {@code Class.forName}, which accepts a {@code null} ClassLoader + * reference as well). + * @return the default ClassLoader (only {@code null} if even the system + * ClassLoader isn't accessible) + * @see Thread#getContextClassLoader() + * @see ClassLoader#getSystemClassLoader() + */ + public static ClassLoader getDefaultClassLoader() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } + catch (Throwable ex) { + // Cannot access thread context ClassLoader - falling back... + } + if (cl == null) { + // No thread context class loader -> use class loader of this class. + cl = ClassUtils.class.getClassLoader(); + if (cl == null) { + // getClassLoader() returning null indicates the bootstrap ClassLoader + try { + cl = ClassLoader.getSystemClassLoader(); + } + catch (Throwable ex) { + // Cannot access system ClassLoader - oh well, maybe the caller can live with null... + } + } + } + return cl; + } + + /** + * Override the thread context ClassLoader with the environment's bean ClassLoader + * if necessary, i.e. if the bean ClassLoader is not equivalent to the thread + * context ClassLoader already. + * @param classLoaderToUse the actual ClassLoader to use for the thread context + * @return the original thread context ClassLoader, or {@code null} if not overridden + */ + public static ClassLoader overrideThreadContextClassLoader(ClassLoader classLoaderToUse) { + Thread currentThread = Thread.currentThread(); + ClassLoader threadContextClassLoader = currentThread.getContextClassLoader(); + if (classLoaderToUse != null && !classLoaderToUse.equals(threadContextClassLoader)) { + currentThread.setContextClassLoader(classLoaderToUse); + return threadContextClassLoader; + } + else { + return null; + } + } + + /** + * Replacement for {@code Class.forName()} that also returns Class instances + * for primitives (like "int") and array class names (like "String[]"). + * <p>Always uses the default class loader: that is, preferably the thread context + * class loader, or the ClassLoader that loaded the ClassUtils class as fallback. + * @param name the name of the Class + * @return Class instance for the supplied name + * @throws ClassNotFoundException if the class was not found + * @throws LinkageError if the class file could not be loaded + * @see Class#forName(String, boolean, ClassLoader) + * @see #getDefaultClassLoader() + * @deprecated as of Spring 3.0, in favor of specifying a ClassLoader explicitly: + * see {@link #forName(String, ClassLoader)} + */ + @Deprecated + public static Class<?> forName(String name) throws ClassNotFoundException, LinkageError { + return forName(name, getDefaultClassLoader()); + } + + /** + * Replacement for {@code Class.forName()} that also returns Class instances + * for primitives (e.g. "int") and array class names (e.g. "String[]"). + * Furthermore, it is also capable of resolving inner class names in Java source + * style (e.g. "java.lang.Thread.State" instead of "java.lang.Thread$State"). + * @param name the name of the Class + * @param classLoader the class loader to use + * (may be {@code null}, which indicates the default class loader) + * @return Class instance for the supplied name + * @throws ClassNotFoundException if the class was not found + * @throws LinkageError if the class file could not be loaded + * @see Class#forName(String, boolean, ClassLoader) + */ + public static Class<?> forName(String name, ClassLoader classLoader) throws ClassNotFoundException, LinkageError { + Assert.notNull(name, "Name must not be null"); + + Class<?> clazz = resolvePrimitiveClassName(name); + if (clazz == null) { + clazz = commonClassCache.get(name); + } + if (clazz != null) { + return clazz; + } + + // "java.lang.String[]" style arrays + if (name.endsWith(ARRAY_SUFFIX)) { + String elementClassName = name.substring(0, name.length() - ARRAY_SUFFIX.length()); + Class<?> elementClass = forName(elementClassName, classLoader); + return Array.newInstance(elementClass, 0).getClass(); + } + + // "[Ljava.lang.String;" style arrays + if (name.startsWith(NON_PRIMITIVE_ARRAY_PREFIX) && name.endsWith(";")) { + String elementName = name.substring(NON_PRIMITIVE_ARRAY_PREFIX.length(), name.length() - 1); + Class<?> elementClass = forName(elementName, classLoader); + return Array.newInstance(elementClass, 0).getClass(); + } + + // "[[I" or "[[Ljava.lang.String;" style arrays + if (name.startsWith(INTERNAL_ARRAY_PREFIX)) { + String elementName = name.substring(INTERNAL_ARRAY_PREFIX.length()); + Class<?> elementClass = forName(elementName, classLoader); + return Array.newInstance(elementClass, 0).getClass(); + } + + ClassLoader clToUse = classLoader; + if (clToUse == null) { + clToUse = getDefaultClassLoader(); + } + try { + return (clToUse != null ? clToUse.loadClass(name) : Class.forName(name)); + } + catch (ClassNotFoundException ex) { + int lastDotIndex = name.lastIndexOf('.'); + if (lastDotIndex != -1) { + String innerClassName = name.substring(0, lastDotIndex) + '$' + name.substring(lastDotIndex + 1); + try { + return (clToUse != null ? clToUse.loadClass(innerClassName) : Class.forName(innerClassName)); + } + catch (ClassNotFoundException ex2) { + // swallow - let original exception get through + } + } + throw ex; + } + } + + /** + * Resolve the given class name into a Class instance. Supports + * primitives (like "int") and array class names (like "String[]"). + * <p>This is effectively equivalent to the {@code forName} + * method with the same arguments, with the only difference being + * the exceptions thrown in case of class loading failure. + * @param className the name of the Class + * @param classLoader the class loader to use + * (may be {@code null}, which indicates the default class loader) + * @return Class instance for the supplied name + * @throws IllegalArgumentException if the class name was not resolvable + * (that is, the class could not be found or the class file could not be loaded) + * @see #forName(String, ClassLoader) + */ + public static Class<?> resolveClassName(String className, ClassLoader classLoader) throws IllegalArgumentException { + try { + return forName(className, classLoader); + } + catch (ClassNotFoundException ex) { + throw new IllegalArgumentException("Cannot find class [" + className + "]", ex); + } + catch (LinkageError ex) { + throw new IllegalArgumentException( + "Error loading class [" + className + "]: problem with class file or dependent class.", ex); + } + } + + /** + * Resolve the given class name as primitive class, if appropriate, + * according to the JVM's naming rules for primitive classes. + * <p>Also supports the JVM's internal class names for primitive arrays. + * Does <i>not</i> support the "[]" suffix notation for primitive arrays; + * this is only supported by {@link #forName(String, ClassLoader)}. + * @param name the name of the potentially primitive class + * @return the primitive class, or {@code null} if the name does not denote + * a primitive class or primitive array class + */ + public static Class<?> resolvePrimitiveClassName(String name) { + Class<?> result = null; + // Most class names will be quite long, considering that they + // SHOULD sit in a package, so a length check is worthwhile. + if (name != null && name.length() <= 8) { + // Could be a primitive - likely. + result = primitiveTypeNameMap.get(name); + } + return result; + } + + /** + * Determine whether the {@link Class} identified by the supplied name is present + * and can be loaded. Will return {@code false} if either the class or + * one of its dependencies is not present or cannot be loaded. + * @param className the name of the class to check + * @return whether the specified class is present + * @deprecated as of Spring 2.5, in favor of {@link #isPresent(String, ClassLoader)} + */ + @Deprecated + public static boolean isPresent(String className) { + return isPresent(className, getDefaultClassLoader()); + } + + /** + * Determine whether the {@link Class} identified by the supplied name is present + * and can be loaded. Will return {@code false} if either the class or + * one of its dependencies is not present or cannot be loaded. + * @param className the name of the class to check + * @param classLoader the class loader to use + * (may be {@code null}, which indicates the default class loader) + * @return whether the specified class is present + */ + public static boolean isPresent(String className, ClassLoader classLoader) { + try { + forName(className, classLoader); + return true; + } + catch (Throwable ex) { + // Class or one of its dependencies is not present... + return false; + } + } + + /** + * Return the user-defined class for the given instance: usually simply + * the class of the given instance, but the original class in case of a + * CGLIB-generated subclass. + * @param instance the instance to check + * @return the user-defined class + */ + public static Class<?> getUserClass(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getUserClass(instance.getClass()); + } + + /** + * Return the user-defined class for the given class: usually simply the given + * class, but the original class in case of a CGLIB-generated subclass. + * @param clazz the class to check + * @return the user-defined class + */ + public static Class<?> getUserClass(Class<?> clazz) { + if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) { + Class<?> superClass = clazz.getSuperclass(); + if (superClass != null && !Object.class.equals(superClass)) { + return superClass; + } + } + return clazz; + } + + /** + * Check whether the given class is cache-safe in the given context, + * i.e. whether it is loaded by the given ClassLoader or a parent of it. + * @param clazz the class to analyze + * @param classLoader the ClassLoader to potentially cache metadata in + */ + public static boolean isCacheSafe(Class<?> clazz, ClassLoader classLoader) { + Assert.notNull(clazz, "Class must not be null"); + try { + ClassLoader target = clazz.getClassLoader(); + if (target == null) { + return true; + } + ClassLoader cur = classLoader; + if (cur == target) { + return true; + } + while (cur != null) { + cur = cur.getParent(); + if (cur == target) { + return true; + } + } + return false; + } + catch (SecurityException ex) { + // Probably from the system ClassLoader - let's consider it safe. + return true; + } + } + + + /** + * Get the class name without the qualified package name. + * @param className the className to get the short name for + * @return the class name of the class without the package name + * @throws IllegalArgumentException if the className is empty + */ + public static String getShortName(String className) { + Assert.hasLength(className, "Class name must not be empty"); + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR); + if (nameEndIndex == -1) { + nameEndIndex = className.length(); + } + String shortName = className.substring(lastDotIndex + 1, nameEndIndex); + shortName = shortName.replace(INNER_CLASS_SEPARATOR, PACKAGE_SEPARATOR); + return shortName; + } + + /** + * Get the class name without the qualified package name. + * @param clazz the class to get the short name for + * @return the class name of the class without the package name + */ + public static String getShortName(Class<?> clazz) { + return getShortName(getQualifiedName(clazz)); + } + + /** + * Return the short string name of a Java class in uncapitalized JavaBeans + * property format. Strips the outer class name in case of an inner class. + * @param clazz the class + * @return the short name rendered in a standard JavaBeans property format + * @see java.beans.Introspector#decapitalize(String) + */ + public static String getShortNameAsProperty(Class<?> clazz) { + String shortName = ClassUtils.getShortName(clazz); + int dotIndex = shortName.lastIndexOf('.'); + shortName = (dotIndex != -1 ? shortName.substring(dotIndex + 1) : shortName); + return Introspector.decapitalize(shortName); + } + + /** + * Determine the name of the class file, relative to the containing + * package: e.g. "String.class" + * @param clazz the class + * @return the file name of the ".class" file + */ + public static String getClassFileName(Class<?> clazz) { + Assert.notNull(clazz, "Class must not be null"); + String className = clazz.getName(); + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + return className.substring(lastDotIndex + 1) + CLASS_FILE_SUFFIX; + } + + /** + * Determine the name of the package of the given class, + * e.g. "java.lang" for the {@code java.lang.String} class. + * @param clazz the class + * @return the package name, or the empty String if the class + * is defined in the default package + */ + public static String getPackageName(Class<?> clazz) { + Assert.notNull(clazz, "Class must not be null"); + return getPackageName(clazz.getName()); + } + + /** + * Determine the name of the package of the given fully-qualified class name, + * e.g. "java.lang" for the {@code java.lang.String} class name. + * @param fqClassName the fully-qualified class name + * @return the package name, or the empty String if the class + * is defined in the default package + */ + public static String getPackageName(String fqClassName) { + Assert.notNull(fqClassName, "Class name must not be null"); + int lastDotIndex = fqClassName.lastIndexOf(PACKAGE_SEPARATOR); + return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : ""); + } + + /** + * Return the qualified name of the given class: usually simply + * the class name, but component type class name + "[]" for arrays. + * @param clazz the class + * @return the qualified name of the class + */ + public static String getQualifiedName(Class<?> clazz) { + Assert.notNull(clazz, "Class must not be null"); + if (clazz.isArray()) { + return getQualifiedNameForArray(clazz); + } + else { + return clazz.getName(); + } + } + + /** + * Build a nice qualified name for an array: + * component type class name + "[]". + * @param clazz the array class + * @return a qualified name for the array class + */ + private static String getQualifiedNameForArray(Class<?> clazz) { + StringBuilder result = new StringBuilder(); + while (clazz.isArray()) { + clazz = clazz.getComponentType(); + result.append(ClassUtils.ARRAY_SUFFIX); + } + result.insert(0, clazz.getName()); + return result.toString(); + } + + /** + * Return the qualified name of the given method, consisting of + * fully qualified interface/class name + "." + method name. + * @param method the method + * @return the qualified name of the method + */ + public static String getQualifiedMethodName(Method method) { + Assert.notNull(method, "Method must not be null"); + return method.getDeclaringClass().getName() + "." + method.getName(); + } + + /** + * Return a descriptive name for the given object's type: usually simply + * the class name, but component type class name + "[]" for arrays, + * and an appended list of implemented interfaces for JDK proxies. + * @param value the value to introspect + * @return the qualified name of the class + */ + public static String getDescriptiveType(Object value) { + if (value == null) { + return null; + } + Class<?> clazz = value.getClass(); + if (Proxy.isProxyClass(clazz)) { + StringBuilder result = new StringBuilder(clazz.getName()); + result.append(" implementing "); + Class<?>[] ifcs = clazz.getInterfaces(); + for (int i = 0; i < ifcs.length; i++) { + result.append(ifcs[i].getName()); + if (i < ifcs.length - 1) { + result.append(','); + } + } + return result.toString(); + } + else if (clazz.isArray()) { + return getQualifiedNameForArray(clazz); + } + else { + return clazz.getName(); + } + } + + /** + * Check whether the given class matches the user-specified type name. + * @param clazz the class to check + * @param typeName the type name to match + */ + public static boolean matchesTypeName(Class<?> clazz, String typeName) { + return (typeName != null && + (typeName.equals(clazz.getName()) || typeName.equals(clazz.getSimpleName()) || + (clazz.isArray() && typeName.equals(getQualifiedNameForArray(clazz))))); + } + + + /** + * Determine whether the given class has a public constructor with the given signature. + * <p>Essentially translates {@code NoSuchMethodException} to "false". + * @param clazz the clazz to analyze + * @param paramTypes the parameter types of the method + * @return whether the class has a corresponding constructor + * @see Class#getMethod + */ + public static boolean hasConstructor(Class<?> clazz, Class<?>... paramTypes) { + return (getConstructorIfAvailable(clazz, paramTypes) != null); + } + + /** + * Determine whether the given class has a public constructor with the given signature, + * and return it if available (else return {@code null}). + * <p>Essentially translates {@code NoSuchMethodException} to {@code null}. + * @param clazz the clazz to analyze + * @param paramTypes the parameter types of the method + * @return the constructor, or {@code null} if not found + * @see Class#getConstructor + */ + public static <T> Constructor<T> getConstructorIfAvailable(Class<T> clazz, Class<?>... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + try { + return clazz.getConstructor(paramTypes); + } + catch (NoSuchMethodException ex) { + return null; + } + } + + /** + * Determine whether the given class has a public method with the given signature. + * <p>Essentially translates {@code NoSuchMethodException} to "false". + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * @return whether the class has a corresponding method + * @see Class#getMethod + */ + public static boolean hasMethod(Class<?> clazz, String methodName, Class<?>... paramTypes) { + return (getMethodIfAvailable(clazz, methodName, paramTypes) != null); + } + + /** + * Determine whether the given class has a public method with the given signature, + * and return it if available (else throws an {@code IllegalStateException}). + * <p>In case of any signature specified, only returns the method if there is a + * unique candidate, i.e. a single public method with the specified name. + * <p>Essentially translates {@code NoSuchMethodException} to {@code IllegalStateException}. + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * (may be {@code null} to indicate any signature) + * @return the method (never {@code null}) + * @throws IllegalStateException if the method has not been found + * @see Class#getMethod + */ + public static Method getMethod(Class<?> clazz, String methodName, Class<?>... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + if (paramTypes != null) { + try { + return clazz.getMethod(methodName, paramTypes); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("Expected method not found: " + ex); + } + } + else { + Set<Method> candidates = new HashSet<Method>(1); + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (methodName.equals(method.getName())) { + candidates.add(method); + } + } + if (candidates.size() == 1) { + return candidates.iterator().next(); + } + else if (candidates.isEmpty()) { + throw new IllegalStateException("Expected method not found: " + clazz + "." + methodName); + } + else { + throw new IllegalStateException("No unique method found: " + clazz + "." + methodName); + } + } + } + + /** + * Determine whether the given class has a public method with the given signature, + * and return it if available (else return {@code null}). + * <p>In case of any signature specified, only returns the method if there is a + * unique candidate, i.e. a single public method with the specified name. + * <p>Essentially translates {@code NoSuchMethodException} to {@code null}. + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * (may be {@code null} to indicate any signature) + * @return the method, or {@code null} if not found + * @see Class#getMethod + */ + public static Method getMethodIfAvailable(Class<?> clazz, String methodName, Class<?>... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + if (paramTypes != null) { + try { + return clazz.getMethod(methodName, paramTypes); + } + catch (NoSuchMethodException ex) { + return null; + } + } + else { + Set<Method> candidates = new HashSet<Method>(1); + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (methodName.equals(method.getName())) { + candidates.add(method); + } + } + if (candidates.size() == 1) { + return candidates.iterator().next(); + } + return null; + } + } + + /** + * Return the number of methods with a given name (with any argument types), + * for the given class and/or its superclasses. Includes non-public methods. + * @param clazz the clazz to check + * @param methodName the name of the method + * @return the number of methods with the given name + */ + public static int getMethodCountForName(Class<?> clazz, String methodName) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + int count = 0; + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method method : declaredMethods) { + if (methodName.equals(method.getName())) { + count++; + } + } + Class<?>[] ifcs = clazz.getInterfaces(); + for (Class<?> ifc : ifcs) { + count += getMethodCountForName(ifc, methodName); + } + if (clazz.getSuperclass() != null) { + count += getMethodCountForName(clazz.getSuperclass(), methodName); + } + return count; + } + + /** + * Does the given class or one of its superclasses at least have one or more + * methods with the supplied name (with any argument types)? + * Includes non-public methods. + * @param clazz the clazz to check + * @param methodName the name of the method + * @return whether there is at least one method with the given name + */ + public static boolean hasAtLeastOneMethodWithName(Class<?> clazz, String methodName) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method method : declaredMethods) { + if (method.getName().equals(methodName)) { + return true; + } + } + Class<?>[] ifcs = clazz.getInterfaces(); + for (Class<?> ifc : ifcs) { + if (hasAtLeastOneMethodWithName(ifc, methodName)) { + return true; + } + } + return (clazz.getSuperclass() != null && hasAtLeastOneMethodWithName(clazz.getSuperclass(), methodName)); + } + + /** + * Given a method, which may come from an interface, and a target class used + * in the current reflective invocation, find the corresponding target method + * if there is one. E.g. the method may be {@code IFoo.bar()} and the + * target class may be {@code DefaultFoo}. In this case, the method may be + * {@code DefaultFoo.bar()}. This enables attributes on that method to be found. + * <p><b>NOTE:</b> In contrast to {@link org.springframework.aop.support.AopUtils#getMostSpecificMethod}, + * this method does <i>not</i> resolve Java 5 bridge methods automatically. + * Call {@link org.springframework.core.BridgeMethodResolver#findBridgedMethod} + * if bridge method resolution is desirable (e.g. for obtaining metadata from + * the original method definition). + * <p><b>NOTE:</b> Since Spring 3.1.1, if Java security settings disallow reflective + * access (e.g. calls to {@code Class#getDeclaredMethods} etc, this implementation + * will fall back to returning the originally provided method. + * @param method the method to be invoked, which may come from an interface + * @param targetClass the target class for the current invocation. + * May be {@code null} or may not even implement the method. + * @return the specific target method, or the original method if the + * {@code targetClass} doesn't implement it or is {@code null} + */ + public static Method getMostSpecificMethod(Method method, Class<?> targetClass) { + if (method != null && isOverridable(method, targetClass) && + targetClass != null && !targetClass.equals(method.getDeclaringClass())) { + try { + if (Modifier.isPublic(method.getModifiers())) { + try { + return targetClass.getMethod(method.getName(), method.getParameterTypes()); + } + catch (NoSuchMethodException ex) { + return method; + } + } + else { + Method specificMethod = + ReflectionUtils.findMethod(targetClass, method.getName(), method.getParameterTypes()); + return (specificMethod != null ? specificMethod : method); + } + } + catch (SecurityException ex) { + // Security settings are disallowing reflective access; fall back to 'method' below. + } + } + return method; + } + + /** + * Determine whether the given method is overridable in the given target class. + * @param method the method to check + * @param targetClass the target class to check against + */ + private static boolean isOverridable(Method method, Class<?> targetClass) { + if (Modifier.isPrivate(method.getModifiers())) { + return false; + } + if (Modifier.isPublic(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) { + return true; + } + return getPackageName(method.getDeclaringClass()).equals(getPackageName(targetClass)); + } + + /** + * Return a public static method of a class. + * @param methodName the static method name + * @param clazz the class which defines the method + * @param args the parameter types to the method + * @return the static method, or {@code null} if no static method was found + * @throws IllegalArgumentException if the method name is blank or the clazz is null + */ + public static Method getStaticMethod(Class<?> clazz, String methodName, Class<?>... args) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + try { + Method method = clazz.getMethod(methodName, args); + return Modifier.isStatic(method.getModifiers()) ? method : null; + } + catch (NoSuchMethodException ex) { + return null; + } + } + + + /** + * Check if the given class represents a primitive wrapper, + * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double. + * @param clazz the class to check + * @return whether the given class is a primitive wrapper class + */ + public static boolean isPrimitiveWrapper(Class<?> clazz) { + Assert.notNull(clazz, "Class must not be null"); + return primitiveWrapperTypeMap.containsKey(clazz); + } + + /** + * Check if the given class represents a primitive (i.e. boolean, byte, + * char, short, int, long, float, or double) or a primitive wrapper + * (i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double). + * @param clazz the class to check + * @return whether the given class is a primitive or primitive wrapper class + */ + public static boolean isPrimitiveOrWrapper(Class<?> clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isPrimitive() || isPrimitiveWrapper(clazz)); + } + + /** + * Check if the given class represents an array of primitives, + * i.e. boolean, byte, char, short, int, long, float, or double. + * @param clazz the class to check + * @return whether the given class is a primitive array class + */ + public static boolean isPrimitiveArray(Class<?> clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isArray() && clazz.getComponentType().isPrimitive()); + } + + /** + * Check if the given class represents an array of primitive wrappers, + * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double. + * @param clazz the class to check + * @return whether the given class is a primitive wrapper array class + */ + public static boolean isPrimitiveWrapperArray(Class<?> clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isArray() && isPrimitiveWrapper(clazz.getComponentType())); + } + + /** + * Resolve the given class if it is a primitive class, + * returning the corresponding primitive wrapper type instead. + * @param clazz the class to check + * @return the original class, or a primitive wrapper for the original primitive type + */ + public static Class<?> resolvePrimitiveIfNecessary(Class<?> clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isPrimitive() && clazz != void.class? primitiveTypeToWrapperMap.get(clazz) : clazz); + } + + /** + * Check if the right-hand side type may be assigned to the left-hand side + * type, assuming setting by reflection. Considers primitive wrapper + * classes as assignable to the corresponding primitive types. + * @param lhsType the target type + * @param rhsType the value type that should be assigned to the target type + * @return if the target type is assignable from the value type + * @see TypeUtils#isAssignable + */ + public static boolean isAssignable(Class<?> lhsType, Class<?> rhsType) { + Assert.notNull(lhsType, "Left-hand side type must not be null"); + Assert.notNull(rhsType, "Right-hand side type must not be null"); + if (lhsType.isAssignableFrom(rhsType)) { + return true; + } + if (lhsType.isPrimitive()) { + Class<?> resolvedPrimitive = primitiveWrapperTypeMap.get(rhsType); + if (resolvedPrimitive != null && lhsType.equals(resolvedPrimitive)) { + return true; + } + } + else { + Class<?> resolvedWrapper = primitiveTypeToWrapperMap.get(rhsType); + if (resolvedWrapper != null && lhsType.isAssignableFrom(resolvedWrapper)) { + return true; + } + } + return false; + } + + /** + * Determine if the given type is assignable from the given value, + * assuming setting by reflection. Considers primitive wrapper classes + * as assignable to the corresponding primitive types. + * @param type the target type + * @param value the value that should be assigned to the type + * @return if the type is assignable from the value + */ + public static boolean isAssignableValue(Class<?> type, Object value) { + Assert.notNull(type, "Type must not be null"); + return (value != null ? isAssignable(type, value.getClass()) : !type.isPrimitive()); + } + + + /** + * Convert a "/"-based resource path to a "."-based fully qualified class name. + * @param resourcePath the resource path pointing to a class + * @return the corresponding fully qualified class name + */ + public static String convertResourcePathToClassName(String resourcePath) { + Assert.notNull(resourcePath, "Resource path must not be null"); + return resourcePath.replace('/', '.'); + } + + /** + * Convert a "."-based fully qualified class name to a "/"-based resource path. + * @param className the fully qualified class name + * @return the corresponding resource path, pointing to the class + */ + public static String convertClassNameToResourcePath(String className) { + Assert.notNull(className, "Class name must not be null"); + return className.replace('.', '/'); + } + + /** + * Return a path suitable for use with {@code ClassLoader.getResource} + * (also suitable for use with {@code Class.getResource} by prepending a + * slash ('/') to the return value). Built by taking the package of the specified + * class file, converting all dots ('.') to slashes ('/'), adding a trailing slash + * if necessary, and concatenating the specified resource name to this. + * <br/>As such, this function may be used to build a path suitable for + * loading a resource file that is in the same package as a class file, + * although {@link org.springframework.core.io.ClassPathResource} is usually + * even more convenient. + * @param clazz the Class whose package will be used as the base + * @param resourceName the resource name to append. A leading slash is optional. + * @return the built-up resource path + * @see ClassLoader#getResource + * @see Class#getResource + */ + public static String addResourcePathToPackagePath(Class<?> clazz, String resourceName) { + Assert.notNull(resourceName, "Resource name must not be null"); + if (!resourceName.startsWith("/")) { + return classPackageAsResourcePath(clazz) + "/" + resourceName; + } + return classPackageAsResourcePath(clazz) + resourceName; + } + + /** + * Given an input class object, return a string which consists of the + * class's package name as a pathname, i.e., all dots ('.') are replaced by + * slashes ('/'). Neither a leading nor trailing slash is added. The result + * could be concatenated with a slash and the name of a resource and fed + * directly to {@code ClassLoader.getResource()}. For it to be fed to + * {@code Class.getResource} instead, a leading slash would also have + * to be prepended to the returned value. + * @param clazz the input class. A {@code null} value or the default + * (empty) package will result in an empty string ("") being returned. + * @return a path which represents the package name + * @see ClassLoader#getResource + * @see Class#getResource + */ + public static String classPackageAsResourcePath(Class<?> clazz) { + if (clazz == null) { + return ""; + } + String className = clazz.getName(); + int packageEndIndex = className.lastIndexOf('.'); + if (packageEndIndex == -1) { + return ""; + } + String packageName = className.substring(0, packageEndIndex); + return packageName.replace('.', '/'); + } + + /** + * Build a String that consists of the names of the classes/interfaces + * in the given array. + * <p>Basically like {@code AbstractCollection.toString()}, but stripping + * the "class "/"interface " prefix before every class name. + * @param classes a Collection of Class objects (may be {@code null}) + * @return a String of form "[com.foo.Bar, com.foo.Baz]" + * @see java.util.AbstractCollection#toString() + */ + public static String classNamesToString(Class... classes) { + return classNamesToString(Arrays.asList(classes)); + } + + /** + * Build a String that consists of the names of the classes/interfaces + * in the given collection. + * <p>Basically like {@code AbstractCollection.toString()}, but stripping + * the "class "/"interface " prefix before every class name. + * @param classes a Collection of Class objects (may be {@code null}) + * @return a String of form "[com.foo.Bar, com.foo.Baz]" + * @see java.util.AbstractCollection#toString() + */ + public static String classNamesToString(Collection<Class> classes) { + if (CollectionUtils.isEmpty(classes)) { + return "[]"; + } + StringBuilder sb = new StringBuilder("["); + for (Iterator<Class> it = classes.iterator(); it.hasNext(); ) { + Class clazz = it.next(); + sb.append(clazz.getName()); + if (it.hasNext()) { + sb.append(", "); + } + } + sb.append("]"); + return sb.toString(); + } + + /** + * Copy the given Collection into a Class array. + * The Collection must contain Class elements only. + * @param collection the Collection to copy + * @return the Class array ({@code null} if the passed-in + * Collection was {@code null}) + */ + public static Class<?>[] toClassArray(Collection<Class<?>> collection) { + if (collection == null) { + return null; + } + return collection.toArray(new Class<?>[collection.size()]); + } + + /** + * Return all interfaces that the given instance implements as array, + * including ones implemented by superclasses. + * @param instance the instance to analyze for interfaces + * @return all interfaces that the given instance implements as array + */ + public static Class<?>[] getAllInterfaces(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getAllInterfacesForClass(instance.getClass()); + } + + /** + * Return all interfaces that the given class implements as array, + * including ones implemented by superclasses. + * <p>If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @return all interfaces that the given object implements as array + */ + public static Class<?>[] getAllInterfacesForClass(Class<?> clazz) { + return getAllInterfacesForClass(clazz, null); + } + + /** + * Return all interfaces that the given class implements as array, + * including ones implemented by superclasses. + * <p>If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @param classLoader the ClassLoader that the interfaces need to be visible in + * (may be {@code null} when accepting all declared interfaces) + * @return all interfaces that the given object implements as array + */ + public static Class<?>[] getAllInterfacesForClass(Class<?> clazz, ClassLoader classLoader) { + Set<Class> ifcs = getAllInterfacesForClassAsSet(clazz, classLoader); + return ifcs.toArray(new Class[ifcs.size()]); + } + + /** + * Return all interfaces that the given instance implements as Set, + * including ones implemented by superclasses. + * @param instance the instance to analyze for interfaces + * @return all interfaces that the given instance implements as Set + */ + public static Set<Class> getAllInterfacesAsSet(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getAllInterfacesForClassAsSet(instance.getClass()); + } + + /** + * Return all interfaces that the given class implements as Set, + * including ones implemented by superclasses. + * <p>If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @return all interfaces that the given object implements as Set + */ + public static Set<Class> getAllInterfacesForClassAsSet(Class clazz) { + return getAllInterfacesForClassAsSet(clazz, null); + } + + /** + * Return all interfaces that the given class implements as Set, + * including ones implemented by superclasses. + * <p>If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @param classLoader the ClassLoader that the interfaces need to be visible in + * (may be {@code null} when accepting all declared interfaces) + * @return all interfaces that the given object implements as Set + */ + public static Set<Class> getAllInterfacesForClassAsSet(Class clazz, ClassLoader classLoader) { + Assert.notNull(clazz, "Class must not be null"); + if (clazz.isInterface() && isVisible(clazz, classLoader)) { + return Collections.singleton(clazz); + } + Set<Class> interfaces = new LinkedHashSet<Class>(); + while (clazz != null) { + Class<?>[] ifcs = clazz.getInterfaces(); + for (Class<?> ifc : ifcs) { + interfaces.addAll(getAllInterfacesForClassAsSet(ifc, classLoader)); + } + clazz = clazz.getSuperclass(); + } + return interfaces; + } + + /** + * Create a composite interface Class for the given interfaces, + * implementing the given interfaces in one single Class. + * <p>This implementation builds a JDK proxy class for the given interfaces. + * @param interfaces the interfaces to merge + * @param classLoader the ClassLoader to create the composite Class in + * @return the merged interface as Class + * @see java.lang.reflect.Proxy#getProxyClass + */ + 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); + } + + /** + * Determine the common ancestor of the given classes, if any. + * @param clazz1 the class to introspect + * @param clazz2 the other class to introspect + * @return the common ancestor (i.e. common superclass, one interface + * extending the other), or {@code null} if none found. If any of the + * given classes is {@code null}, the other class will be returned. + * @since 3.2.6 + */ + public static Class<?> determineCommonAncestor(Class<?> clazz1, Class<?> clazz2) { + if (clazz1 == null) { + return clazz2; + } + if (clazz2 == null) { + return clazz1; + } + if (clazz1.isAssignableFrom(clazz2)) { + return clazz1; + } + if (clazz2.isAssignableFrom(clazz1)) { + return clazz2; + } + Class<?> ancestor = clazz1; + do { + ancestor = ancestor.getSuperclass(); + if (ancestor == null || Object.class.equals(ancestor)) { + return null; + } + } + while (!ancestor.isAssignableFrom(clazz2)); + return ancestor; + } + + /** + * Check whether the given class is visible in the given ClassLoader. + * @param clazz the class to check (typically an interface) + * @param classLoader the ClassLoader to check against (may be {@code null}, + * in which case this method will always return {@code true}) + */ + public static boolean isVisible(Class<?> clazz, ClassLoader classLoader) { + if (classLoader == null) { + return true; + } + try { + Class<?> actualClass = classLoader.loadClass(clazz.getName()); + return (clazz == actualClass); + // Else: different interface class found... + } + catch (ClassNotFoundException ex) { + // No interface class found... + return false; + } + } + + /** + * Check whether the given object is a CGLIB proxy. + * @param object the object to check + * @see org.springframework.aop.support.AopUtils#isCglibProxy(Object) + */ + public static boolean isCglibProxy(Object object) { + return ClassUtils.isCglibProxyClass(object.getClass()); + } + + /** + * Check whether the specified class is a CGLIB-generated class. + * @param clazz the class to check + */ + public static boolean isCglibProxyClass(Class<?> clazz) { + return (clazz != null && isCglibProxyClassName(clazz.getName())); + } + + /** + * Check whether the specified class name is a CGLIB-generated class. + * @param className the class name to check + */ + public static boolean isCglibProxyClassName(String className) { + return (className != null && className.contains(CGLIB_CLASS_SEPARATOR)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java new file mode 100644 index 00000000..5e74473f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -0,0 +1,500 @@ +/* + * Copyright 2002-2012 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.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Miscellaneous collection utility methods. + * Mainly for internal use within the framework. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Arjen Poutsma + * @since 1.1.3 + */ +public abstract class CollectionUtils { + + /** + * Return {@code true} if the supplied Collection is {@code null} + * or empty. Otherwise, return {@code false}. + * @param collection the Collection to check + * @return whether the given Collection is empty + */ + public static boolean isEmpty(Collection collection) { + return (collection == null || collection.isEmpty()); + } + + /** + * Return {@code true} if the supplied Map is {@code null} + * or empty. Otherwise, return {@code false}. + * @param map the Map to check + * @return whether the given Map is empty + */ + public static boolean isEmpty(Map map) { + return (map == null || map.isEmpty()); + } + + /** + * Convert the supplied array into a List. A primitive array gets + * converted into a List of the appropriate wrapper type. + * <p>A {@code null} source value will be converted to an + * empty List. + * @param source the (potentially primitive) array + * @return the converted List result + * @see ObjectUtils#toObjectArray(Object) + */ + public static List arrayToList(Object source) { + return Arrays.asList(ObjectUtils.toObjectArray(source)); + } + + /** + * Merge the given array into the given Collection. + * @param array the array to merge (may be {@code null}) + * @param collection the target Collection to merge the array into + */ + @SuppressWarnings("unchecked") + public static void mergeArrayIntoCollection(Object array, Collection collection) { + if (collection == null) { + throw new IllegalArgumentException("Collection must not be null"); + } + Object[] arr = ObjectUtils.toObjectArray(array); + for (Object elem : arr) { + collection.add(elem); + } + } + + /** + * Merge the given Properties instance into the given Map, + * copying all properties (key-value pairs) over. + * <p>Uses {@code Properties.propertyNames()} to even catch + * default properties linked into the original Properties instance. + * @param props the Properties instance to merge (may be {@code null}) + * @param map the target Map to merge the properties into + */ + @SuppressWarnings("unchecked") + public static void mergePropertiesIntoMap(Properties props, Map map) { + if (map == null) { + throw new IllegalArgumentException("Map must not be null"); + } + if (props != null) { + for (Enumeration en = props.propertyNames(); en.hasMoreElements();) { + String key = (String) en.nextElement(); + Object value = props.getProperty(key); + if (value == null) { + // Potentially a non-String value... + value = props.get(key); + } + map.put(key, value); + } + } + } + + + /** + * Check whether the given Iterator contains the given element. + * @param iterator the Iterator to check + * @param element the element to look for + * @return {@code true} if found, {@code false} else + */ + public static boolean contains(Iterator iterator, Object element) { + if (iterator != null) { + while (iterator.hasNext()) { + Object candidate = iterator.next(); + if (ObjectUtils.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Enumeration contains the given element. + * @param enumeration the Enumeration to check + * @param element the element to look for + * @return {@code true} if found, {@code false} else + */ + public static boolean contains(Enumeration enumeration, Object element) { + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + Object candidate = enumeration.nextElement(); + if (ObjectUtils.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Collection contains the given element instance. + * <p>Enforces the given instance to be present, rather than returning + * {@code true} for an equal element as well. + * @param collection the Collection to check + * @param element the element to look for + * @return {@code true} if found, {@code false} else + */ + public static boolean containsInstance(Collection collection, Object element) { + if (collection != null) { + for (Object candidate : collection) { + if (candidate == element) { + return true; + } + } + } + return false; + } + + /** + * Return {@code true} if any element in '{@code candidates}' is + * contained in '{@code source}'; otherwise returns {@code false}. + * @param source the source Collection + * @param candidates the candidates to search for + * @return whether any of the candidates has been found + */ + public static boolean containsAny(Collection source, Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return false; + } + for (Object candidate : candidates) { + if (source.contains(candidate)) { + return true; + } + } + return false; + } + + /** + * Return the first element in '{@code candidates}' that is contained in + * '{@code source}'. If no element in '{@code candidates}' is present in + * '{@code source}' returns {@code null}. Iteration order is + * {@link Collection} implementation specific. + * @param source the source Collection + * @param candidates the candidates to search for + * @return the first present object, or {@code null} if not found + */ + public static Object findFirstMatch(Collection source, Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return null; + } + for (Object candidate : candidates) { + if (source.contains(candidate)) { + return candidate; + } + } + return null; + } + + /** + * Find a single value of the given type in the given Collection. + * @param collection the Collection to search + * @param type the type to look for + * @return a value of the given type found if there is a clear match, + * or {@code null} if none or more than one such value found + */ + @SuppressWarnings("unchecked") + public static <T> T findValueOfType(Collection<?> collection, Class<T> type) { + if (isEmpty(collection)) { + return null; + } + T value = null; + for (Object element : collection) { + if (type == null || type.isInstance(element)) { + if (value != null) { + // More than one value found... no clear single value. + return null; + } + value = (T) element; + } + } + return value; + } + + /** + * Find a single value of one of the given types in the given Collection: + * searching the Collection for a value of the first type, then + * searching for a value of the second type, etc. + * @param collection the collection to search + * @param types the types to look for, in prioritized order + * @return a value of one of the given types found if there is a clear match, + * or {@code null} if none or more than one such value found + */ + public static Object findValueOfType(Collection<?> collection, Class<?>[] types) { + if (isEmpty(collection) || ObjectUtils.isEmpty(types)) { + return null; + } + for (Class<?> type : types) { + Object value = findValueOfType(collection, type); + if (value != null) { + return value; + } + } + return null; + } + + /** + * Determine whether the given Collection only contains a single unique object. + * @param collection the Collection to check + * @return {@code true} if the collection contains a single reference or + * multiple references to the same instance, {@code false} else + */ + public static boolean hasUniqueObject(Collection collection) { + if (isEmpty(collection)) { + return false; + } + boolean hasCandidate = false; + Object candidate = null; + for (Object elem : collection) { + if (!hasCandidate) { + hasCandidate = true; + candidate = elem; + } + else if (candidate != elem) { + return false; + } + } + return true; + } + + /** + * Find the common element type of the given Collection, if any. + * @param collection the Collection to check + * @return the common element type, or {@code null} if no clear + * common type has been found (or the collection was empty) + */ + public static Class<?> findCommonElementType(Collection collection) { + if (isEmpty(collection)) { + return null; + } + Class<?> candidate = null; + for (Object val : collection) { + if (val != null) { + if (candidate == null) { + candidate = val.getClass(); + } + else if (candidate != val.getClass()) { + return null; + } + } + } + return candidate; + } + + /** + * Marshal the elements from the given enumeration into an array of the given type. + * Enumeration elements must be assignable to the type of the given array. The array + * returned will be a different instance than the array given. + */ + public static <A,E extends A> A[] toArray(Enumeration<E> enumeration, A[] array) { + ArrayList<A> elements = new ArrayList<A>(); + while (enumeration.hasMoreElements()) { + elements.add(enumeration.nextElement()); + } + return elements.toArray(array); + } + + /** + * Adapt an enumeration to an iterator. + * @param enumeration the enumeration + * @return the iterator + */ + public static <E> Iterator<E> toIterator(Enumeration<E> enumeration) { + return new EnumerationIterator<E>(enumeration); + } + + /** + * Adapts a {@code Map<K, List<V>>} to an {@code MultiValueMap<K,V>}. + * + * @param map the map + * @return the multi-value map + */ + public static <K, V> MultiValueMap<K, V> toMultiValueMap(Map<K, List<V>> map) { + return new MultiValueMapAdapter<K, V>(map); + + } + + /** + * Returns an unmodifiable view of the specified multi-value map. + * + * @param map the map for which an unmodifiable view is to be returned. + * @return an unmodifiable view of the specified multi-value map. + */ + public static <K,V> MultiValueMap<K,V> unmodifiableMultiValueMap(MultiValueMap<? extends K, ? extends V> map) { + Assert.notNull(map, "'map' must not be null"); + Map<K, List<V>> result = new LinkedHashMap<K, List<V>>(map.size()); + for (Map.Entry<? extends K, ? extends List<? extends V>> entry : map.entrySet()) { + List<V> values = Collections.unmodifiableList(entry.getValue()); + result.put(entry.getKey(), values); + } + Map<K, List<V>> unmodifiableMap = Collections.unmodifiableMap(result); + return toMultiValueMap(unmodifiableMap); + } + + + + /** + * Iterator wrapping an Enumeration. + */ + private static class EnumerationIterator<E> implements Iterator<E> { + + private Enumeration<E> enumeration; + + public EnumerationIterator(Enumeration<E> enumeration) { + this.enumeration = enumeration; + } + + public boolean hasNext() { + return this.enumeration.hasMoreElements(); + } + + public E next() { + return this.enumeration.nextElement(); + } + + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException("Not supported"); + } + } + + /** + * Adapts a Map to the MultiValueMap contract. + */ + @SuppressWarnings("serial") + private static class MultiValueMapAdapter<K, V> implements MultiValueMap<K, V>, Serializable { + + private final Map<K, List<V>> map; + + public MultiValueMapAdapter(Map<K, List<V>> map) { + Assert.notNull(map, "'map' must not be null"); + this.map = map; + } + + public void add(K key, V value) { + List<V> values = this.map.get(key); + if (values == null) { + values = new LinkedList<V>(); + this.map.put(key, values); + } + values.add(value); + } + + public V getFirst(K key) { + List<V> values = this.map.get(key); + return (values != null ? values.get(0) : null); + } + + public void set(K key, V value) { + List<V> values = new LinkedList<V>(); + values.add(value); + this.map.put(key, values); + } + + public void setAll(Map<K, V> values) { + for (Entry<K, V> entry : values.entrySet()) { + set(entry.getKey(), entry.getValue()); + } + } + + public Map<K, V> toSingleValueMap() { + LinkedHashMap<K, V> singleValueMap = new LinkedHashMap<K,V>(this.map.size()); + for (Entry<K, List<V>> entry : map.entrySet()) { + singleValueMap.put(entry.getKey(), entry.getValue().get(0)); + } + return singleValueMap; + } + + public int size() { + return this.map.size(); + } + + public boolean isEmpty() { + return this.map.isEmpty(); + } + + public boolean containsKey(Object key) { + return this.map.containsKey(key); + } + + public boolean containsValue(Object value) { + return this.map.containsValue(value); + } + + public List<V> get(Object key) { + return this.map.get(key); + } + + public List<V> put(K key, List<V> value) { + return this.map.put(key, value); + } + + public List<V> remove(Object key) { + return this.map.remove(key); + } + + public void putAll(Map<? extends K, ? extends List<V>> m) { + this.map.putAll(m); + } + + public void clear() { + this.map.clear(); + } + + public Set<K> keySet() { + return this.map.keySet(); + } + + public Collection<List<V>> values() { + return this.map.values(); + } + + public Set<Entry<K, List<V>>> entrySet() { + return this.map.entrySet(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + return map.equals(other); + } + + @Override + public int hashCode() { + return this.map.hashCode(); + } + + @Override + public String toString() { + return this.map.toString(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/CommonsLogWriter.java b/spring-core/src/main/java/org/springframework/util/CommonsLogWriter.java new file mode 100644 index 00000000..30bdb086 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CommonsLogWriter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2012 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.util; + +import java.io.Writer; + +import org.apache.commons.logging.Log; + +/** + * {@code java.io.Writer} adapter for a Commons Logging {@code Log}. + * + * @author Juergen Hoeller + * @since 2.5.1 + */ +public class CommonsLogWriter extends Writer { + + private final Log logger; + + private final StringBuilder buffer = new StringBuilder(); + + + /** + * Create a new CommonsLogWriter for the given Commons Logging logger. + * @param logger the Commons Logging logger to write to + */ + public CommonsLogWriter(Log logger) { + Assert.notNull(logger, "Logger must not be null"); + this.logger = logger; + } + + + public void write(char ch) { + if (ch == '\n' && this.buffer.length() > 0) { + this.logger.debug(this.buffer.toString()); + this.buffer.setLength(0); + } + else { + this.buffer.append(ch); + } + } + + @Override + public void write(char[] buffer, int offset, int length) { + for (int i = 0; i < length; i++) { + char ch = buffer[offset + i]; + if (ch == '\n' && this.buffer.length() > 0) { + this.logger.debug(this.buffer.toString()); + this.buffer.setLength(0); + } + else { + this.buffer.append(ch); + } + } + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/CompositeIterator.java b/spring-core/src/main/java/org/springframework/util/CompositeIterator.java new file mode 100644 index 00000000..60b1571c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CompositeIterator.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2014 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.util; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * Composite iterator that combines multiple other iterators, + * as registered via {@link #add(Iterator)}. + * + * <p>This implementation maintains a linked set of iterators + * which are invoked in sequence until all iterators are exhausted. + * + * @author Erwin Vervaet + * @author Juergen Hoeller + * @since 3.0 + */ +public class CompositeIterator<E> implements Iterator<E> { + + private final Set<Iterator<E>> iterators = new LinkedHashSet<Iterator<E>>(); + + private boolean inUse = false; + + + /** + * Add given iterator to this composite. + */ + public void add(Iterator<E> iterator) { + Assert.state(!this.inUse, "You can no longer add iterators to a composite iterator that's already in use"); + if (this.iterators.contains(iterator)) { + throw new IllegalArgumentException("You cannot add the same iterator twice"); + } + this.iterators.add(iterator); + } + + public boolean hasNext() { + this.inUse = true; + for (Iterator<E> iterator : this.iterators) { + if (iterator.hasNext()) { + return true; + } + } + return false; + } + + public E next() { + this.inUse = true; + for (Iterator<E> iterator : this.iterators) { + if (iterator.hasNext()) { + return iterator.next(); + } + } + throw new NoSuchElementException("All iterators exhausted"); + } + + public void remove() { + throw new UnsupportedOperationException("CompositeIterator does not support remove()"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java new file mode 100644 index 00000000..56abcec7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2012 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.util; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Support class for throttling concurrent access to a specific resource. + * + * <p>Designed for use as a base class, with the subclass invoking + * the {@link #beforeAccess()} and {@link #afterAccess()} methods at + * appropriate points of its workflow. Note that {@code afterAccess} + * should usually be called in a finally block! + * + * <p>The default concurrency limit of this support class is -1 + * ("unbounded concurrency"). Subclasses may override this default; + * check the javadoc of the concrete class that you're using. + * + * @author Juergen Hoeller + * @since 1.2.5 + * @see #setConcurrencyLimit + * @see #beforeAccess() + * @see #afterAccess() + * @see org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor + * @see java.io.Serializable + */ +@SuppressWarnings("serial") +public abstract class ConcurrencyThrottleSupport implements Serializable { + + /** + * Permit any number of concurrent invocations: that is, don't throttle concurrency. + */ + public static final int UNBOUNDED_CONCURRENCY = -1; + + /** + * Switch concurrency 'off': that is, don't allow any concurrent invocations. + */ + public static final int NO_CONCURRENCY = 0; + + + /** Transient to optimize serialization */ + protected transient Log logger = LogFactory.getLog(getClass()); + + private transient Object monitor = new Object(); + + private int concurrencyLimit = UNBOUNDED_CONCURRENCY; + + private int concurrencyCount = 0; + + + /** + * Set the maximum number of concurrent access attempts allowed. + * -1 indicates unbounded concurrency. + * <p>In principle, this limit can be changed at runtime, + * although it is generally designed as a config time setting. + * <p>NOTE: Do not switch between -1 and any concrete limit at runtime, + * as this will lead to inconsistent concurrency counts: A limit + * of -1 effectively turns off concurrency counting completely. + */ + public void setConcurrencyLimit(int concurrencyLimit) { + this.concurrencyLimit = concurrencyLimit; + } + + /** + * Return the maximum number of concurrent access attempts allowed. + */ + public int getConcurrencyLimit() { + return this.concurrencyLimit; + } + + /** + * Return whether this throttle is currently active. + * @return {@code true} if the concurrency limit for this instance is active + * @see #getConcurrencyLimit() + */ + public boolean isThrottleActive() { + return (this.concurrencyLimit > 0); + } + + + /** + * To be invoked before the main execution logic of concrete subclasses. + * <p>This implementation applies the concurrency throttle. + * @see #afterAccess() + */ + protected void beforeAccess() { + if (this.concurrencyLimit == NO_CONCURRENCY) { + throw new IllegalStateException( + "Currently no invocations allowed - concurrency limit set to NO_CONCURRENCY"); + } + if (this.concurrencyLimit > 0) { + boolean debug = logger.isDebugEnabled(); + synchronized (this.monitor) { + boolean interrupted = false; + while (this.concurrencyCount >= this.concurrencyLimit) { + if (interrupted) { + throw new IllegalStateException("Thread was interrupted while waiting for invocation access, " + + "but concurrency limit still does not allow for entering"); + } + if (debug) { + logger.debug("Concurrency count " + this.concurrencyCount + + " has reached limit " + this.concurrencyLimit + " - blocking"); + } + try { + this.monitor.wait(); + } + catch (InterruptedException ex) { + // Re-interrupt current thread, to allow other threads to react. + Thread.currentThread().interrupt(); + interrupted = true; + } + } + if (debug) { + logger.debug("Entering throttle at concurrency count " + this.concurrencyCount); + } + this.concurrencyCount++; + } + } + } + + /** + * To be invoked after the main execution logic of concrete subclasses. + * @see #beforeAccess() + */ + protected void afterAccess() { + if (this.concurrencyLimit >= 0) { + synchronized (this.monitor) { + this.concurrencyCount--; + if (logger.isDebugEnabled()) { + logger.debug("Returning from throttle at concurrency count " + this.concurrencyCount); + } + this.monitor.notify(); + } + } + } + + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization, just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize transient fields. + this.logger = LogFactory.getLog(getClass()); + this.monitor = new Object(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java new file mode 100644 index 00000000..c7324610 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -0,0 +1,1006 @@ +/* + * Copyright 2002-2014 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.util; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.lang.reflect.Array; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.ReentrantLock; + +/** + * A {@link ConcurrentHashMap} that uses {@link ReferenceType#SOFT soft} or + * {@linkplain ReferenceType#WEAK weak} references for both {@code keys} and {@code values}. + * + * <p>This class can be used as an alternative to + * {@code Collections.synchronizedMap(new WeakHashMap<K, Reference<V>>())} in order to + * support better performance when accessed concurrently. This implementation follows the + * same design constraints as {@link ConcurrentHashMap} with the exception that + * {@code null} values and {@code null} keys are supported. + * + * <p><b>NOTE:</b> The use of references means that there is no guarantee that items + * placed into the map will be subsequently available. The garbage collector may discard + * references at any time, so it may appear that an unknown thread is silently removing + * entries. + * + * <p>If not explicitly specified, this implementation will use + * {@linkplain SoftReference soft entry references}. + * + * @param <K> The key type + * @param <V> The value type + * @author Phillip Webb + * @since 3.2 + */ +public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> { + + private static final int DEFAULT_INITIAL_CAPACITY = 16; + + private static final float DEFAULT_LOAD_FACTOR = 0.75f; + + private static final int DEFAULT_CONCURRENCY_LEVEL = 16; + + private static final ReferenceType DEFAULT_REFERENCE_TYPE = ReferenceType.SOFT; + + private static final int MAXIMUM_CONCURRENCY_LEVEL = 1 << 16; + + private static final int MAXIMUM_SEGMENT_SIZE = 1 << 30; + + + /** + * Array of segments indexed using the high order bits from the hash. + */ + private final Segment[] segments; + + /** + * When the average number of references per table exceeds this value resize will be attempted. + */ + private final float loadFactor; + + /** + * The reference type: SOFT or WEAK. + */ + private final ReferenceType referenceType; + + /** + * The shift value used to calculate the size of the segments array and an index from the hash. + */ + private final int shift; + + /** + * Late binding entry set. + */ + private Set<Map.Entry<K, V>> entrySet; + + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + */ + public ConcurrentReferenceHashMap() { + this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * @param initialCapacity the initial capacity of the map + */ + public ConcurrentReferenceHashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * @param initialCapacity the initial capacity of the map + * @param loadFactor the load factor. When the average number of references per table + * exceeds this value resize will be attempted + */ + public ConcurrentReferenceHashMap(int initialCapacity, float loadFactor) { + this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * @param initialCapacity the initial capacity of the map + * @param concurrencyLevel the expected number of threads that will concurrently + * write to the map + */ + public ConcurrentReferenceHashMap(int initialCapacity, int concurrencyLevel) { + this(initialCapacity, DEFAULT_LOAD_FACTOR, concurrencyLevel, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * @param initialCapacity the initial capacity of the map + * @param referenceType the reference type used for entries (soft or weak) + */ + public ConcurrentReferenceHashMap(int initialCapacity, ReferenceType referenceType) { + this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, referenceType); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * @param initialCapacity the initial capacity of the map + * @param loadFactor the load factor. When the average number of references per + * table exceeds this value, resize will be attempted. + * @param concurrencyLevel the expected number of threads that will concurrently + * write to the map + */ + public ConcurrentReferenceHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { + this(initialCapacity, loadFactor, concurrencyLevel, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * @param initialCapacity the initial capacity of the map + * @param loadFactor the load factor. When the average number of references per + * table exceeds this value, resize will be attempted. + * @param concurrencyLevel the expected number of threads that will concurrently + * write to the map + * @param referenceType the reference type used for entries (soft or weak) + */ + @SuppressWarnings("unchecked") + public ConcurrentReferenceHashMap(int initialCapacity, float loadFactor, int concurrencyLevel, + ReferenceType referenceType) { + + Assert.isTrue(initialCapacity >= 0, "Initial capacity must not be negative"); + Assert.isTrue(loadFactor > 0f, "Load factor must be positive"); + Assert.isTrue(concurrencyLevel > 0, "Concurrency level must be positive"); + Assert.notNull(referenceType, "Reference type must not be null"); + this.loadFactor = loadFactor; + this.shift = calculateShift(concurrencyLevel, MAXIMUM_CONCURRENCY_LEVEL); + int size = 1 << this.shift; + this.referenceType = referenceType; + int roundedUpSegmentCapacity = (int) ((initialCapacity + size - 1L) / size); + this.segments = (Segment[]) Array.newInstance(Segment.class, size); + for (int i = 0; i < this.segments.length; i++) { + this.segments[i] = new Segment(roundedUpSegmentCapacity); + } + } + + + protected final float getLoadFactor() { + return this.loadFactor; + } + + protected final int getSegmentsSize() { + return this.segments.length; + } + + protected final Segment getSegment(int index) { + return this.segments[index]; + } + + /** + * Factory method that returns the {@link ReferenceManager}. + * This method will be called once for each {@link Segment}. + * @return a new reference manager + */ + protected ReferenceManager createReferenceManager() { + return new ReferenceManager(); + } + + /** + * Get the hash for a given object, apply an additional hash function to reduce + * collisions. This implementation uses the same Wang/Jenkins algorithm as + * {@link ConcurrentHashMap}. Subclasses can override to provide alternative hashing. + * @param o the object to hash (may be null) + * @return the resulting hash code + */ + protected int getHash(Object o) { + int hash = o == null ? 0 : o.hashCode(); + hash += (hash << 15) ^ 0xffffcd7d; + hash ^= (hash >>> 10); + hash += (hash << 3); + hash ^= (hash >>> 6); + hash += (hash << 2) + (hash << 14); + hash ^= (hash >>> 16); + return hash; + } + + @Override + public V get(Object key) { + Reference<K, V> reference = getReference(key, Restructure.WHEN_NECESSARY); + Entry<K, V> entry = (reference != null ? reference.get() : null); + return (entry != null ? entry.getValue() : null); + } + + @Override + public boolean containsKey(Object key) { + Reference<K, V> reference = getReference(key, Restructure.WHEN_NECESSARY); + Entry<K, V> entry = (reference != null ? reference.get() : null); + return (entry != null && ObjectUtils.nullSafeEquals(entry.getKey(), key)); + } + + /** + * Return a {@link Reference} to the {@link Entry} for the specified {@code key}, + * or {@code null} if not found. + * @param key the key (can be {@code null}) + * @param restructure types of restructure allowed during this call + * @return the reference, or {@code null} if not found + */ + protected final Reference<K, V> getReference(Object key, Restructure restructure) { + int hash = getHash(key); + return getSegmentForHash(hash).getReference(key, hash, restructure); + } + + @Override + public V put(K key, V value) { + return put(key, value, true); + } + + public V putIfAbsent(K key, V value) { + return put(key, value, false); + } + + private V put(final K key, final V value, final boolean overwriteExisting) { + return doTask(key, new Task<V>(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) { + @Override + protected V execute(Reference<K, V> reference, Entry<K, V> entry, Entries entries) { + if (entry != null) { + V previousValue = entry.getValue(); + if (overwriteExisting) { + entry.setValue(value); + } + return previousValue; + } + entries.add(value); + return null; + } + }); + } + + @Override + public V remove(Object key) { + return doTask(key, new Task<V>(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { + @Override + protected V execute(Reference<K, V> reference, Entry<K, V> entry) { + if (entry != null) { + reference.release(); + return entry.value; + } + return null; + } + }); + } + + public boolean remove(Object key, final Object value) { + return doTask(key, new Task<Boolean>(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { + @Override + protected Boolean execute(Reference<K, V> reference, Entry<K, V> entry) { + if (entry != null && ObjectUtils.nullSafeEquals(entry.getValue(), value)) { + reference.release(); + return true; + } + return false; + } + }); + } + + public boolean replace(K key, final V oldValue, final V newValue) { + return doTask(key, new Task<Boolean>(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { + @Override + protected Boolean execute(Reference<K, V> reference, Entry<K, V> entry) { + if (entry != null && ObjectUtils.nullSafeEquals(entry.getValue(), oldValue)) { + entry.setValue(newValue); + return true; + } + return false; + } + }); + } + + public V replace(K key, final V value) { + return doTask(key, new Task<V>(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { + @Override + protected V execute(Reference<K, V> reference, Entry<K, V> entry) { + if (entry != null) { + V previousValue = entry.getValue(); + entry.setValue(value); + return previousValue; + } + return null; + } + }); + } + + @Override + public void clear() { + for (Segment segment : this.segments) { + segment.clear(); + } + } + + /** + * Remove any entries that have been garbage collected and are no longer referenced. + * Under normal circumstances garbage collected entries are automatically purged as + * items are added or removed from the Map. This method can be used to force a purge, + * and is useful when the Map is read frequently but updated less often. + */ + public void purgeUnreferencedEntries() { + for (Segment segment : this.segments) { + segment.restructureIfNecessary(false); + } + } + + + @Override + public int size() { + int size = 0; + for (Segment segment : this.segments) { + size += segment.getCount(); + } + return size; + } + + @Override + public Set<java.util.Map.Entry<K, V>> entrySet() { + if (this.entrySet == null) { + this.entrySet = new EntrySet(); + } + return this.entrySet; + } + + private <T> T doTask(Object key, Task<T> task) { + int hash = getHash(key); + return getSegmentForHash(hash).doTask(hash, key, task); + } + + private Segment getSegmentForHash(int hash) { + return this.segments[(hash >>> (32 - this.shift)) & (this.segments.length - 1)]; + } + + /** + * Calculate a shift value that can be used to create a power-of-two value between + * the specified maximum and minimum values. + * @param minimumValue the minimum value + * @param maximumValue the maximum value + * @return the calculated shift (use {@code 1 << shift} to obtain a value) + */ + protected static int calculateShift(int minimumValue, int maximumValue) { + int shift = 0; + int value = 1; + while (value < minimumValue && value < maximumValue) { + value <<= 1; + shift++; + } + return shift; + } + + + /** + * Various reference types supported by this map. + */ + public static enum ReferenceType { + + /** Use {@link SoftReference}s */ + SOFT, + + /** Use {@link WeakReference}s */ + WEAK + } + + + /** + * A single segment used to divide the map to allow better concurrent performance. + */ + @SuppressWarnings("serial") + protected final class Segment extends ReentrantLock { + + private final ReferenceManager referenceManager; + + private final int initialSize; + + /** + * Array of references indexed using the low order bits from the hash. This + * property should only be set via {@link #setReferences} to ensure that the + * {@code resizeThreshold} is maintained. + */ + private volatile Reference<K, V>[] references; + + /** + * The total number of references contained in this segment. This includes chained + * references and references that have been garbage collected but not purged. + */ + private volatile int count = 0; + + /** + * The threshold when resizing of the references should occur. When {@code count} + * exceeds this value references will be resized. + */ + private int resizeThreshold; + + public Segment(int initialCapacity) { + this.referenceManager = createReferenceManager(); + this.initialSize = 1 << calculateShift(initialCapacity, MAXIMUM_SEGMENT_SIZE); + setReferences(createReferenceArray(this.initialSize)); + } + + public Reference<K, V> getReference(Object key, int hash, Restructure restructure) { + if (restructure == Restructure.WHEN_NECESSARY) { + restructureIfNecessary(false); + } + if (this.count == 0) { + return null; + } + // Use a local copy to protect against other threads writing + Reference<K, V>[] references = this.references; + int index = getIndex(hash, references); + Reference<K, V> head = references[index]; + return findInChain(head, key, hash); + } + + /** + * Apply an update operation to this segment. + * The segment will be locked during the update. + * @param hash the hash of the key + * @param key the key + * @param task the update operation + * @return the result of the operation + */ + public <T> T doTask(final int hash, final Object key, final Task<T> task) { + boolean resize = task.hasOption(TaskOption.RESIZE); + if (task.hasOption(TaskOption.RESTRUCTURE_BEFORE)) { + restructureIfNecessary(resize); + } + if (task.hasOption(TaskOption.SKIP_IF_EMPTY) && this.count == 0) { + return task.execute(null, null, null); + } + lock(); + try { + final int index = getIndex(hash, this.references); + final Reference<K, V> head = this.references[index]; + Reference<K, V> reference = findInChain(head, key, hash); + Entry<K, V> entry = (reference != null ? reference.get() : null); + Entries entries = new Entries() { + @Override + public void add(V value) { + @SuppressWarnings("unchecked") + Entry<K, V> newEntry = new Entry<K, V>((K) key, value); + Reference<K, V> newReference = Segment.this.referenceManager.createReference(newEntry, hash, head); + Segment.this.references[index] = newReference; + Segment.this.count++; + } + }; + return task.execute(reference, entry, entries); + } + finally { + unlock(); + if (task.hasOption(TaskOption.RESTRUCTURE_AFTER)) { + restructureIfNecessary(resize); + } + } + } + + /** + * Clear all items from this segment. + */ + public void clear() { + if (this.count == 0) { + return; + } + lock(); + try { + setReferences(createReferenceArray(this.initialSize)); + this.count = 0; + } + finally { + unlock(); + } + } + + /** + * Restructure the underlying data structure when it becomes necessary. This + * method can increase the size of the references table as well as purge any + * references that have been garbage collected. + * @param allowResize if resizing is permitted + */ + protected final void restructureIfNecessary(boolean allowResize) { + boolean needsResize = ((this.count > 0) && (this.count >= this.resizeThreshold)); + Reference<K, V> reference = this.referenceManager.pollForPurge(); + if ((reference != null) || (needsResize && allowResize)) { + lock(); + try { + int countAfterRestructure = this.count; + + Set<Reference<K, V>> toPurge = Collections.emptySet(); + if (reference != null) { + toPurge = new HashSet<Reference<K, V>>(); + while (reference != null) { + toPurge.add(reference); + reference = this.referenceManager.pollForPurge(); + } + } + countAfterRestructure -= toPurge.size(); + + // Recalculate taking into account count inside lock and items that + // will be purged + needsResize = (countAfterRestructure > 0 && countAfterRestructure >= this.resizeThreshold); + boolean resizing = false; + int restructureSize = this.references.length; + if (allowResize && needsResize && restructureSize < MAXIMUM_SEGMENT_SIZE) { + restructureSize <<= 1; + resizing = true; + } + + // Either create a new table or reuse the existing one + Reference<K, V>[] restructured = (resizing ? createReferenceArray(restructureSize) : this.references); + + // Restructure + for (int i = 0; i < this.references.length; i++) { + reference = this.references[i]; + if (!resizing) { + restructured[i] = null; + } + while (reference != null) { + if (!toPurge.contains(reference) && (reference.get() != null)) { + int index = getIndex(reference.getHash(), restructured); + restructured[index] = this.referenceManager.createReference( + reference.get(), reference.getHash(), + restructured[index]); + } + reference = reference.getNext(); + } + } + + // Replace volatile members + if (resizing) { + setReferences(restructured); + } + this.count = Math.max(countAfterRestructure, 0); + } + finally { + unlock(); + } + } + } + + private Reference<K, V> findInChain(Reference<K, V> reference, Object key, int hash) { + while (reference != null) { + if (reference.getHash() == hash) { + Entry<K, V> entry = reference.get(); + if (entry != null) { + K entryKey = entry.getKey(); + if (entryKey == key || entryKey.equals(key)) { + return reference; + } + } + } + reference = reference.getNext(); + } + return null; + } + + @SuppressWarnings("unchecked") + private Reference<K, V>[] createReferenceArray(int size) { + return (Reference<K, V>[]) Array.newInstance(Reference.class, size); + } + + private int getIndex(int hash, Reference<K, V>[] references) { + return (hash & (references.length - 1)); + } + + /** + * Replace the references with a new value, recalculating the resizeThreshold. + * @param references the new references + */ + private void setReferences(Reference<K, V>[] references) { + this.references = references; + this.resizeThreshold = (int) (references.length * getLoadFactor()); + } + + /** + * @return the size of the current references array + */ + public final int getSize() { + return this.references.length; + } + + /** + * @return the total number of references in this segment + */ + public final int getCount() { + return this.count; + } + } + + + /** + * A reference to an {@link Entry} contained in the map. Implementations are usually + * wrappers around specific Java reference implementations (e.g., {@link SoftReference}). + */ + protected static interface Reference<K, V> { + + /** + * Returns the referenced entry or {@code null} if the entry is no longer + * available. + * @return the entry or {@code null} + */ + Entry<K, V> get(); + + /** + * Returns the hash for the reference. + * @return the hash + */ + int getHash(); + + /** + * Returns the next reference in the chain or {@code null} + * @return the next reference of {@code null} + */ + Reference<K, V> getNext(); + + /** + * Release this entry and ensure that it will be returned from + * {@code ReferenceManager#pollForPurge()}. + */ + void release(); + } + + + /** + * A single map entry. + */ + protected static final class Entry<K, V> implements Map.Entry<K, V> { + + private final K key; + + private volatile V value; + + public Entry(K key, V value) { + this.key = key; + this.value = value; + } + + public K getKey() { + return this.key; + } + + public V getValue() { + return this.value; + } + + public V setValue(V value) { + V previous = this.value; + this.value = value; + return previous; + } + + @Override + public String toString() { + return (this.key + "=" + this.value); + } + + @Override + @SuppressWarnings("rawtypes") + public final boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Map.Entry)) { + return false; + } + Map.Entry otherEntry = (Map.Entry) other; + return (ObjectUtils.nullSafeEquals(getKey(), otherEntry.getKey()) && + ObjectUtils.nullSafeEquals(getValue(), otherEntry.getValue())); + } + + @Override + public final int hashCode() { + return (ObjectUtils.nullSafeHashCode(this.key) ^ ObjectUtils.nullSafeHashCode(this.value)); + } + } + + + /** + * A task that can be {@link Segment#doTask run} against a {@link Segment}. + */ + private abstract class Task<T> { + + private final EnumSet<TaskOption> options; + + public Task(TaskOption... options) { + this.options = (options.length == 0 ? EnumSet.noneOf(TaskOption.class) : EnumSet.of(options[0], options)); + } + + public boolean hasOption(TaskOption option) { + return this.options.contains(option); + } + + /** + * Execute the task. + * @param reference the found reference or {@code null} + * @param entry the found entry or {@code null} + * @param entries access to the underlying entries + * @return the result of the task + * @see #execute(Reference, Entry) + */ + protected T execute(Reference<K, V> reference, Entry<K, V> entry, Entries entries) { + return execute(reference, entry); + } + + /** + * Convenience method that can be used for tasks that do not need access to {@link Entries}. + * @param reference the found reference or {@code null} + * @param entry the found entry or {@code null} + * @return the result of the task + * @see #execute(Reference, Entry, Entries) + */ + protected T execute(Reference<K, V> reference, Entry<K, V> entry) { + return null; + } + } + + + /** + * Various options supported by a {@code Task}. + */ + private static enum TaskOption { + + RESTRUCTURE_BEFORE, RESTRUCTURE_AFTER, SKIP_IF_EMPTY, RESIZE + } + + + /** + * Allows a task access to {@link Segment} entries. + */ + private abstract class Entries { + + /** + * Add a new entry with the specified value. + * @param value the value to add + */ + public abstract void add(V value); + } + + + /** + * Internal entry-set implementation. + */ + private class EntrySet extends AbstractSet<Map.Entry<K, V>> { + + @Override + public Iterator<Map.Entry<K, V>> iterator() { + return new EntryIterator(); + } + + @Override + public boolean contains(Object o) { + if (o != null && o instanceof Map.Entry<?, ?>) { + Map.Entry<?, ?> entry = (java.util.Map.Entry<?, ?>) o; + Reference<K, V> reference = ConcurrentReferenceHashMap.this.getReference(entry.getKey(), Restructure.NEVER); + Entry<K, V> other = (reference != null ? reference.get() : null); + if (other != null) { + return ObjectUtils.nullSafeEquals(entry.getValue(), other.getValue()); + } + } + return false; + } + + @Override + public boolean remove(Object o) { + if (o instanceof Map.Entry<?, ?>) { + Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o; + return ConcurrentReferenceHashMap.this.remove(entry.getKey(), entry.getValue()); + } + return false; + } + + @Override + public int size() { + return ConcurrentReferenceHashMap.this.size(); + } + + @Override + public void clear() { + ConcurrentReferenceHashMap.this.clear(); + } + } + + + /** + * Internal entry iterator implementation. + */ + private class EntryIterator implements Iterator<Map.Entry<K, V>> { + + private int segmentIndex; + + private int referenceIndex; + + private Reference<K, V>[] references; + + private Reference<K, V> reference; + + private Entry<K, V> next; + + private Entry<K, V> last; + + public EntryIterator() { + moveToNextSegment(); + } + + public boolean hasNext() { + getNextIfNecessary(); + return (this.next != null); + } + + public Entry<K, V> next() { + getNextIfNecessary(); + if (this.next == null) { + throw new NoSuchElementException(); + } + this.last = this.next; + this.next = null; + return this.last; + } + + private void getNextIfNecessary() { + while (this.next == null) { + moveToNextReference(); + if (this.reference == null) { + return; + } + this.next = this.reference.get(); + } + } + + private void moveToNextReference() { + if (this.reference != null) { + this.reference = this.reference.getNext(); + } + while (this.reference == null && this.references != null) { + if (this.referenceIndex >= this.references.length) { + moveToNextSegment(); + this.referenceIndex = 0; + } + else { + this.reference = this.references[this.referenceIndex]; + this.referenceIndex++; + } + } + } + + private void moveToNextSegment() { + this.reference = null; + this.references = null; + if (this.segmentIndex < ConcurrentReferenceHashMap.this.segments.length) { + this.references = ConcurrentReferenceHashMap.this.segments[this.segmentIndex].references; + this.segmentIndex++; + } + } + + public void remove() { + Assert.state(this.last != null); + ConcurrentReferenceHashMap.this.remove(this.last.getKey()); + } + } + + + /** + * The types of restructuring that can be performed. + */ + protected static enum Restructure { + + WHEN_NECESSARY, NEVER + } + + + /** + * Strategy class used to manage {@link Reference}s. This class can be overridden if + * alternative reference types need to be supported. + */ + protected class ReferenceManager { + + private final ReferenceQueue<Entry<K, V>> queue = new ReferenceQueue<Entry<K, V>>(); + + /** + * Factory method used to create a new {@link Reference}. + * @param entry the entry contained in the reference + * @param hash the hash + * @param next the next reference in the chain or {@code null} + * @return a new {@link Reference} + */ + public Reference<K, V> createReference(Entry<K, V> entry, int hash, Reference<K, V> next) { + if (ConcurrentReferenceHashMap.this.referenceType == ReferenceType.WEAK) { + return new WeakEntryReference<K, V>(entry, hash, next, this.queue); + } + return new SoftEntryReference<K, V>(entry, hash, next, this.queue); + } + + /** + * Return any reference that has been garbage collected and can be purged from the + * underlying structure or {@code null} if no references need purging. This + * method must be thread safe and ideally should not block when returning + * {@code null}. References should be returned once and only once. + * @return a reference to purge or {@code null} + */ + @SuppressWarnings("unchecked") + public Reference<K, V> pollForPurge() { + return (Reference<K, V>) this.queue.poll(); + } + } + + + /** + * Internal {@link Reference} implementation for {@link SoftReference}s. + */ + private static final class SoftEntryReference<K, V> extends SoftReference<Entry<K, V>> implements Reference<K, V> { + + private final int hash; + + private final Reference<K, V> nextReference; + + public SoftEntryReference(Entry<K, V> entry, int hash, Reference<K, V> next, ReferenceQueue<Entry<K, V>> queue) { + super(entry, queue); + this.hash = hash; + this.nextReference = next; + } + + public int getHash() { + return this.hash; + } + + public Reference<K, V> getNext() { + return this.nextReference; + } + + public void release() { + enqueue(); + clear(); + } + } + + + /** + * Internal {@link Reference} implementation for {@link WeakReference}s. + */ + private static final class WeakEntryReference<K, V> extends WeakReference<Entry<K, V>> implements Reference<K, V> { + + private final int hash; + + private final Reference<K, V> nextReference; + + public WeakEntryReference(Entry<K, V> entry, int hash, Reference<K, V> next, ReferenceQueue<Entry<K, V>> queue) { + super(entry, queue); + this.hash = hash; + this.nextReference = next; + } + + public int getHash() { + return this.hash; + } + + public Reference<K, V> getNext() { + return this.nextReference; + } + + public void release() { + enqueue(); + clear(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/CustomizableThreadCreator.java b/spring-core/src/main/java/org/springframework/util/CustomizableThreadCreator.java new file mode 100644 index 00000000..969b5e3d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CustomizableThreadCreator.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2014 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.util; + +import java.io.Serializable; + +/** + * Simple customizable helper class for creating new {@link Thread} instances. + * Provides various bean properties: thread name prefix, thread priority, etc. + * + * <p>Serves as base class for thread factories such as + * {@link org.springframework.scheduling.concurrent.CustomizableThreadFactory}. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see org.springframework.scheduling.concurrent.CustomizableThreadFactory + */ +@SuppressWarnings("serial") +public class CustomizableThreadCreator implements Serializable { + + private String threadNamePrefix; + + private int threadPriority = Thread.NORM_PRIORITY; + + private boolean daemon = false; + + private ThreadGroup threadGroup; + + private int threadCount = 0; + + private final Object threadCountMonitor = new SerializableMonitor(); + + + /** + * Create a new CustomizableThreadCreator with default thread name prefix. + */ + public CustomizableThreadCreator() { + this.threadNamePrefix = getDefaultThreadNamePrefix(); + } + + /** + * Create a new CustomizableThreadCreator with the given thread name prefix. + * @param threadNamePrefix the prefix to use for the names of newly created threads + */ + public CustomizableThreadCreator(String threadNamePrefix) { + this.threadNamePrefix = (threadNamePrefix != null ? threadNamePrefix : getDefaultThreadNamePrefix()); + } + + + /** + * Specify the prefix to use for the names of newly created threads. + * Default is "SimpleAsyncTaskExecutor-". + */ + public void setThreadNamePrefix(String threadNamePrefix) { + this.threadNamePrefix = (threadNamePrefix != null ? threadNamePrefix : getDefaultThreadNamePrefix()); + } + + /** + * Return the thread name prefix to use for the names of newly + * created threads. + */ + public String getThreadNamePrefix() { + return this.threadNamePrefix; + } + + /** + * Set the priority of the threads that this factory creates. + * Default is 5. + * @see java.lang.Thread#NORM_PRIORITY + */ + public void setThreadPriority(int threadPriority) { + this.threadPriority = threadPriority; + } + + /** + * Return the priority of the threads that this factory creates. + */ + public int getThreadPriority() { + return this.threadPriority; + } + + /** + * Set whether this factory is supposed to create daemon threads, + * just executing as long as the application itself is running. + * <p>Default is "false": Concrete factories usually support explicit cancelling. + * Hence, if the application shuts down, Runnables will by default finish their + * execution. + * <p>Specify "true" for eager shutdown of threads which still actively execute + * a {@link Runnable} at the time that the application itself shuts down. + * @see java.lang.Thread#setDaemon + */ + public void setDaemon(boolean daemon) { + this.daemon = daemon; + } + + /** + * Return whether this factory should create daemon threads. + */ + public boolean isDaemon() { + return this.daemon; + } + + /** + * Specify the name of the thread group that threads should be created in. + * @see #setThreadGroup + */ + public void setThreadGroupName(String name) { + this.threadGroup = new ThreadGroup(name); + } + + /** + * Specify the thread group that threads should be created in. + * @see #setThreadGroupName + */ + public void setThreadGroup(ThreadGroup threadGroup) { + this.threadGroup = threadGroup; + } + + /** + * Return the thread group that threads should be created in + * (or {@code null} for the default group). + */ + public ThreadGroup getThreadGroup() { + return this.threadGroup; + } + + + /** + * Template method for the creation of a new {@link Thread}. + * <p>The default implementation creates a new Thread for the given + * {@link Runnable}, applying an appropriate thread name. + * @param runnable the Runnable to execute + * @see #nextThreadName() + */ + public Thread createThread(Runnable runnable) { + Thread thread = new Thread(getThreadGroup(), runnable, nextThreadName()); + thread.setPriority(getThreadPriority()); + thread.setDaemon(isDaemon()); + return thread; + } + + /** + * Return the thread name to use for a newly created {@link Thread}. + * <p>The default implementation returns the specified thread name prefix + * with an increasing thread count appended: e.g. "SimpleAsyncTaskExecutor-0". + * @see #getThreadNamePrefix() + */ + protected String nextThreadName() { + int threadNumber = 0; + synchronized (this.threadCountMonitor) { + this.threadCount++; + threadNumber = this.threadCount; + } + return getThreadNamePrefix() + threadNumber; + } + + /** + * Build the default thread name prefix for this factory. + * @return the default thread name prefix (never {@code null}) + */ + protected String getDefaultThreadNamePrefix() { + return ClassUtils.getShortName(getClass()) + "-"; + } + + + /** + * Empty class used for a serializable monitor object. + */ + private static class SerializableMonitor implements Serializable { + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java b/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java new file mode 100644 index 00000000..1337a5a5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java @@ -0,0 +1,233 @@ +/* + * Copyright 2002-2013 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.util; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.Date; +import java.util.Enumeration; +import java.util.Properties; + +/** + * Default implementation of the {@link PropertiesPersister} interface. + * Follows the native parsing of {@code java.util.Properties}. + * + * <p>Allows for reading from any Reader and writing to any Writer, for example + * to specify a charset for a properties file. This is a capability that standard + * {@code java.util.Properties} unfortunately lacked up until JDK 1.5: + * You were only able to load files using the ISO-8859-1 charset there. + * + * <p>Loading from and storing to a stream delegates to {@code Properties.load} + * and {@code Properties.store}, respectively, to be fully compatible with + * the Unicode conversion as implemented by the JDK Properties class. On JDK 1.6, + * {@code Properties.load/store} will also be used for readers/writers, + * effectively turning this class into a plain backwards compatibility adapter. + * + * <p>The persistence code that works with Reader/Writer follows the JDK's parsing + * strategy but does not implement Unicode conversion, because the Reader/Writer + * should already apply proper decoding/encoding of characters. If you use prefer + * to escape unicode characters in your properties files, do <i>not</i> specify + * an encoding for a Reader/Writer (like ReloadableResourceBundleMessageSource's + * "defaultEncoding" and "fileEncodings" properties). + * + * @author Juergen Hoeller + * @since 10.03.2004 + * @see java.util.Properties + * @see java.util.Properties#load + * @see java.util.Properties#store + */ +public class DefaultPropertiesPersister implements PropertiesPersister { + + // Determine whether Properties.load(Reader) is available (on JDK 1.6+) + private static final boolean loadFromReaderAvailable = + ClassUtils.hasMethod(Properties.class, "load", new Class[] {Reader.class}); + + // Determine whether Properties.store(Writer, String) is available (on JDK 1.6+) + private static final boolean storeToWriterAvailable = + ClassUtils.hasMethod(Properties.class, "store", new Class[] {Writer.class, String.class}); + + + public void load(Properties props, InputStream is) throws IOException { + props.load(is); + } + + public void load(Properties props, Reader reader) throws IOException { + if (loadFromReaderAvailable) { + // On JDK 1.6+ + props.load(reader); + } + else { + // Fall back to manual parsing. + doLoad(props, reader); + } + } + + protected void doLoad(Properties props, Reader reader) throws IOException { + BufferedReader in = new BufferedReader(reader); + while (true) { + String line = in.readLine(); + if (line == null) { + return; + } + line = StringUtils.trimLeadingWhitespace(line); + if (line.length() > 0) { + char firstChar = line.charAt(0); + if (firstChar != '#' && firstChar != '!') { + while (endsWithContinuationMarker(line)) { + String nextLine = in.readLine(); + line = line.substring(0, line.length() - 1); + if (nextLine != null) { + line += StringUtils.trimLeadingWhitespace(nextLine); + } + } + int separatorIndex = line.indexOf("="); + if (separatorIndex == -1) { + separatorIndex = line.indexOf(":"); + } + String key = (separatorIndex != -1 ? line.substring(0, separatorIndex) : line); + String value = (separatorIndex != -1) ? line.substring(separatorIndex + 1) : ""; + key = StringUtils.trimTrailingWhitespace(key); + value = StringUtils.trimLeadingWhitespace(value); + props.put(unescape(key), unescape(value)); + } + } + } + } + + protected boolean endsWithContinuationMarker(String line) { + boolean evenSlashCount = true; + int index = line.length() - 1; + while (index >= 0 && line.charAt(index) == '\\') { + evenSlashCount = !evenSlashCount; + index--; + } + return !evenSlashCount; + } + + protected String unescape(String str) { + StringBuilder result = new StringBuilder(str.length()); + for (int index = 0; index < str.length();) { + char c = str.charAt(index++); + if (c == '\\') { + c = str.charAt(index++); + if (c == 't') { + c = '\t'; + } + else if (c == 'r') { + c = '\r'; + } + else if (c == 'n') { + c = '\n'; + } + else if (c == 'f') { + c = '\f'; + } + } + result.append(c); + } + return result.toString(); + } + + + public void store(Properties props, OutputStream os, String header) throws IOException { + props.store(os, header); + } + + public void store(Properties props, Writer writer, String header) throws IOException { + if (storeToWriterAvailable) { + // On JDK 1.6+ + props.store(writer, header); + } + else { + // Fall back to manual parsing. + doStore(props, writer, header); + } + } + + protected void doStore(Properties props, Writer writer, String header) throws IOException { + BufferedWriter out = new BufferedWriter(writer); + if (header != null) { + out.write("#" + header); + out.newLine(); + } + out.write("#" + new Date()); + out.newLine(); + for (Enumeration keys = props.keys(); keys.hasMoreElements();) { + String key = (String) keys.nextElement(); + String val = props.getProperty(key); + out.write(escape(key, true) + "=" + escape(val, false)); + out.newLine(); + } + out.flush(); + } + + protected String escape(String str, boolean isKey) { + int len = str.length(); + StringBuilder result = new StringBuilder(len * 2); + for (int index = 0; index < len; index++) { + char c = str.charAt(index); + switch (c) { + case ' ': + if (index == 0 || isKey) { + result.append('\\'); + } + result.append(' '); + break; + case '\\': + result.append("\\\\"); + break; + case '\t': + result.append("\\t"); + break; + case '\n': + result.append("\\n"); + break; + case '\r': + result.append("\\r"); + break; + case '\f': + result.append("\\f"); + break; + default: + if ("=: \t\r\n\f#!".indexOf(c) != -1) { + result.append('\\'); + } + result.append(c); + } + } + return result.toString(); + } + + + public void loadFromXml(Properties props, InputStream is) throws IOException { + props.loadFromXML(is); + } + + public void storeToXml(Properties props, OutputStream os, String header) throws IOException { + props.storeToXML(os, header); + } + + public void storeToXml(Properties props, OutputStream os, String header, String encoding) throws IOException { + props.storeToXML(os, header, encoding); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/DigestUtils.java b/spring-core/src/main/java/org/springframework/util/DigestUtils.java new file mode 100644 index 00000000..a4ca743b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/DigestUtils.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2012 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.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Miscellaneous methods for calculating digests. + * <p>Mainly for internal use within the framework; consider + * <a href="http://commons.apache.org/codec/">Apache Commons Codec</a> for a + * more comprehensive suite of digest utilities. + * + * @author Arjen Poutsma + * @since 3.0 + * @see org.apache.commons.codec.digest.DigestUtils + */ +public abstract class DigestUtils { + + private static final String MD5_ALGORITHM_NAME = "MD5"; + + private static final char[] HEX_CHARS = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + /** + * Calculate the MD5 digest of the given bytes. + * @param bytes the bytes to calculate the digest over + * @return the digest + */ + public static byte[] md5Digest(byte[] bytes) { + return digest(MD5_ALGORITHM_NAME, 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 + */ + public static String md5DigestAsHex(byte[] bytes) { + return digestAsHexString(MD5_ALGORITHM_NAME, bytes); + } + + /** + * Append a hexadecimal string representation of the MD5 digest of the given + * bytes to the given {@link StringBuilder}. + * @param bytes the bytes to calculate the digest over + * @param builder the string builder to append the digest to + * @return the given string builder + */ + public static StringBuilder appendMd5DigestAsHex(byte[] bytes, StringBuilder builder) { + return appendDigestAsHex(MD5_ALGORITHM_NAME, bytes, builder); + } + + /** + * Creates a new {@link MessageDigest} with the given algorithm. Necessary + * because {@code MessageDigest} is not thread-safe. + */ + private static MessageDigest getDigest(String algorithm) { + try { + return MessageDigest.getInstance(algorithm); + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("Could not find MessageDigest with algorithm \"" + algorithm + "\"", ex); + } + } + + private static byte[] digest(String algorithm, byte[] bytes) { + return getDigest(algorithm).digest(bytes); + } + + private static String digestAsHexString(String algorithm, byte[] bytes) { + char[] hexDigest = digestAsHexChars(algorithm, bytes); + return new String(hexDigest); + } + + private static StringBuilder appendDigestAsHex(String algorithm, byte[] bytes, StringBuilder builder) { + char[] hexDigest = digestAsHexChars(algorithm, bytes); + return builder.append(hexDigest); + } + + private static char[] digestAsHexChars(String algorithm, byte[] bytes) { + byte[] digest = digest(algorithm, bytes); + return encodeHex(digest); + } + + private static char[] encodeHex(byte[] bytes) { + char chars[] = new char[32]; + for (int i = 0; i < chars.length; i = i + 2) { + byte b = bytes[i / 2]; + chars[i] = HEX_CHARS[(b >>> 0x4) & 0xf]; + chars[i + 1] = HEX_CHARS[b & 0xf]; + } + return chars; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ErrorHandler.java b/spring-core/src/main/java/org/springframework/util/ErrorHandler.java new file mode 100644 index 00000000..a43ee8b6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ErrorHandler.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2012 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.util; + +/** + * A strategy for handling errors. This is especially useful for handling + * errors that occur during asynchronous execution of tasks that have been + * submitted to a TaskScheduler. In such cases, it may not be possible to + * throw the error to the original caller. + * + * @author Mark Fisher + * @since 3.0 + */ +public interface ErrorHandler { + + /** + * Handle the given error, possibly rethrowing it as a fatal exception. + */ + void handleError(Throwable t); + +} diff --git a/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java b/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java new file mode 100644 index 00000000..2c14da3b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java @@ -0,0 +1,236 @@ +/* + * Copyright 2002-2013 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.util; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; + +/** + * Simple utility methods for file and stream copying. All copy methods use a block size + * of 4096 bytes, and close all affected streams when done. A variation of the copy + * methods from this class that leave streams open can be found in {@link StreamUtils}. + * + * <p>Mainly for use within the framework, but also useful for application code. + * + * @author Juergen Hoeller + * @since 06.10.2003 + * @see StreamUtils + */ +public abstract class FileCopyUtils { + + public static final int BUFFER_SIZE = StreamUtils.BUFFER_SIZE; + + + //--------------------------------------------------------------------- + // Copy methods for java.io.File + //--------------------------------------------------------------------- + + /** + * Copy the contents of the given input File to the given output File. + * @param in the file to copy from + * @param out the file to copy to + * @return the number of bytes copied + * @throws IOException in case of I/O errors + */ + public static int copy(File in, File out) throws IOException { + Assert.notNull(in, "No input File specified"); + Assert.notNull(out, "No output File specified"); + return copy(new BufferedInputStream(new FileInputStream(in)), + new BufferedOutputStream(new FileOutputStream(out))); + } + + /** + * Copy the contents of the given byte array to the given output File. + * @param in the byte array to copy from + * @param out the file to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(byte[] in, File out) throws IOException { + Assert.notNull(in, "No input byte array specified"); + Assert.notNull(out, "No output File specified"); + ByteArrayInputStream inStream = new ByteArrayInputStream(in); + OutputStream outStream = new BufferedOutputStream(new FileOutputStream(out)); + copy(inStream, outStream); + } + + /** + * Copy the contents of the given input File into a new byte array. + * @param in the file to copy from + * @return the new byte array that has been copied to + * @throws IOException in case of I/O errors + */ + public static byte[] copyToByteArray(File in) throws IOException { + Assert.notNull(in, "No input File specified"); + return copyToByteArray(new BufferedInputStream(new FileInputStream(in))); + } + + + //--------------------------------------------------------------------- + // Copy methods for java.io.InputStream / java.io.OutputStream + //--------------------------------------------------------------------- + + /** + * Copy the contents of the given InputStream to the given OutputStream. + * Closes both streams when done. + * @param in the stream to copy from + * @param out the stream to copy to + * @return the number of bytes copied + * @throws IOException in case of I/O errors + */ + public static int copy(InputStream in, OutputStream out) throws IOException { + Assert.notNull(in, "No InputStream specified"); + Assert.notNull(out, "No OutputStream specified"); + try { + return StreamUtils.copy(in, out); + } + finally { + try { + in.close(); + } + catch (IOException ex) { + } + try { + out.close(); + } + catch (IOException ex) { + } + } + } + + /** + * Copy the contents of the given byte array to the given OutputStream. + * Closes the stream when done. + * @param in the byte array to copy from + * @param out the OutputStream to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(byte[] in, OutputStream out) throws IOException { + Assert.notNull(in, "No input byte array specified"); + Assert.notNull(out, "No OutputStream specified"); + try { + out.write(in); + } + finally { + try { + out.close(); + } + catch (IOException ex) { + } + } + } + + /** + * Copy the contents of the given InputStream into a new byte array. + * Closes the stream when done. + * @param in the stream to copy from + * @return the new byte array that has been copied to + * @throws IOException in case of I/O errors + */ + public static byte[] copyToByteArray(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE); + copy(in, out); + return out.toByteArray(); + } + + + //--------------------------------------------------------------------- + // Copy methods for java.io.Reader / java.io.Writer + //--------------------------------------------------------------------- + + /** + * Copy the contents of the given Reader to the given Writer. + * Closes both when done. + * @param in the Reader to copy from + * @param out the Writer to copy to + * @return the number of characters copied + * @throws IOException in case of I/O errors + */ + public static int copy(Reader in, Writer out) throws IOException { + Assert.notNull(in, "No Reader specified"); + Assert.notNull(out, "No Writer specified"); + try { + int byteCount = 0; + char[] buffer = new char[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + byteCount += bytesRead; + } + out.flush(); + return byteCount; + } + finally { + try { + in.close(); + } + catch (IOException ex) { + } + try { + out.close(); + } + catch (IOException ex) { + } + } + } + + /** + * Copy the contents of the given String to the given output Writer. + * Closes the writer when done. + * @param in the String to copy from + * @param out the Writer to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(String in, Writer out) throws IOException { + Assert.notNull(in, "No input String specified"); + Assert.notNull(out, "No Writer specified"); + try { + out.write(in); + } + finally { + try { + out.close(); + } + catch (IOException ex) { + } + } + } + + /** + * Copy the contents of the given Reader into a String. + * Closes the reader when done. + * @param in the reader to copy from + * @return the String that has been copied to + * @throws IOException in case of I/O errors + */ + public static String copyToString(Reader in) throws IOException { + StringWriter out = new StringWriter(); + copy(in, out); + return out.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java b/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java new file mode 100644 index 00000000..529b4f01 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2012 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.util; + +import java.io.File; +import java.io.IOException; + +/** + * Utility methods for working with the file system. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.5.3 + */ +public abstract class FileSystemUtils { + + /** + * Delete the supplied {@link File} - for directories, + * recursively delete any nested directories or files as well. + * @param root the root {@code File} to delete + * @return {@code true} if the {@code File} was deleted, + * otherwise {@code false} + */ + public static boolean deleteRecursively(File root) { + if (root != null && root.exists()) { + if (root.isDirectory()) { + File[] children = root.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursively(child); + } + } + } + return root.delete(); + } + return false; + } + + /** + * Recursively copy the contents of the {@code src} file/directory + * to the {@code dest} file/directory. + * @param src the source directory + * @param dest the destination directory + * @throws IOException in the case of I/O errors + */ + public static void copyRecursively(File src, File dest) throws IOException { + Assert.isTrue(src != null && (src.isDirectory() || src.isFile()), "Source File must denote a directory or file"); + Assert.notNull(dest, "Destination File must not be null"); + doCopyRecursively(src, dest); + } + + /** + * Actually copy the contents of the {@code src} file/directory + * to the {@code dest} file/directory. + * @param src the source directory + * @param dest the destination directory + * @throws IOException in the case of I/O errors + */ + private static void doCopyRecursively(File src, File dest) throws IOException { + if (src.isDirectory()) { + dest.mkdir(); + File[] entries = src.listFiles(); + if (entries == null) { + throw new IOException("Could not list files in directory: " + src); + } + for (File entry : entries) { + doCopyRecursively(entry, new File(dest, entry.getName())); + } + } + else if (src.isFile()) { + try { + dest.createNewFile(); + } + catch (IOException ex) { + IOException ioex = new IOException("Failed to create file: " + dest); + ioex.initCause(ex); + throw ioex; + } + FileCopyUtils.copy(src, dest); + } + else { + // Special File handle: neither a file not a directory. + // Simply skip it when contained in nested directory... + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java new file mode 100644 index 00000000..62921593 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2012 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.util; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; + +/** + * {@link LinkedHashMap} variant that stores String keys in a case-insensitive + * manner, for example for key-based access in a results table. + * + * <p>Preserves the original order as well as the original casing of keys, + * while allowing for contains, get and remove calls with any case of key. + * + * <p>Does <i>not</i> support {@code null} keys. + * + * @author Juergen Hoeller + * @since 3.0 + */ +@SuppressWarnings("serial") +public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> { + + private final Map<String, String> caseInsensitiveKeys; + + private final Locale locale; + + + /** + * Create a new LinkedCaseInsensitiveMap for the default Locale. + * @see java.lang.String#toLowerCase() + */ + public LinkedCaseInsensitiveMap() { + this(null); + } + + /** + * Create a new LinkedCaseInsensitiveMap that stores lower-case keys + * according to the given Locale. + * @param locale the Locale to use for lower-case conversion + * @see java.lang.String#toLowerCase(java.util.Locale) + */ + public LinkedCaseInsensitiveMap(Locale locale) { + super(); + this.caseInsensitiveKeys = new HashMap<String, String>(); + this.locale = (locale != null ? locale : Locale.getDefault()); + } + + /** + * Create a new LinkedCaseInsensitiveMap that wraps a {@link LinkedHashMap} + * with the given initial capacity and stores lower-case keys according + * to the default Locale. + * @param initialCapacity the initial capacity + * @see java.lang.String#toLowerCase() + */ + public LinkedCaseInsensitiveMap(int initialCapacity) { + this(initialCapacity, null); + } + + /** + * Create a new LinkedCaseInsensitiveMap that wraps a {@link LinkedHashMap} + * with the given initial capacity and stores lower-case keys according + * to the given Locale. + * @param initialCapacity the initial capacity + * @param locale the Locale to use for lower-case conversion + * @see java.lang.String#toLowerCase(java.util.Locale) + */ + public LinkedCaseInsensitiveMap(int initialCapacity, Locale locale) { + super(initialCapacity); + this.caseInsensitiveKeys = new HashMap<String, String>(initialCapacity); + this.locale = (locale != null ? locale : Locale.getDefault()); + } + + + @Override + public V put(String key, V value) { + String oldKey = this.caseInsensitiveKeys.put(convertKey(key), key); + if (oldKey != null && !oldKey.equals(key)) { + super.remove(oldKey); + } + return super.put(key, value); + } + + @Override + public void putAll(Map<? extends String, ? extends V> map) { + if (map.isEmpty()) { + return; + } + for (Map.Entry<? extends String, ? extends V> entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public boolean containsKey(Object key) { + return (key instanceof String && this.caseInsensitiveKeys.containsKey(convertKey((String) key))); + } + + @Override + public V get(Object key) { + if (key instanceof String) { + return super.get(this.caseInsensitiveKeys.get(convertKey((String) key))); + } + else { + return null; + } + } + + @Override + public V remove(Object key) { + if (key instanceof String ) { + return super.remove(this.caseInsensitiveKeys.remove(convertKey((String) key))); + } + else { + return null; + } + } + + @Override + public void clear() { + this.caseInsensitiveKeys.clear(); + super.clear(); + } + + + /** + * Convert the given key to a case-insensitive key. + * <p>The default implementation converts the key + * to lower-case according to this Map's Locale. + * @param key the user-specified key + * @return the key to use for storing + * @see java.lang.String#toLowerCase(java.util.Locale) + */ + protected String convertKey(String key) { + return key.toLowerCase(this.locale); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java b/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java new file mode 100644 index 00000000..730047f7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2010 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.util; + +import java.io.Serializable; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Simple implementation of {@link MultiValueMap} that wraps a {@link LinkedHashMap}, + * storing multiple values in a {@link LinkedList}. + * + * <p>This Map implementation is generally not thread-safe. It is primarily designed + * for data structures exposed from request objects, for use in a single thread only. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + */ +public class LinkedMultiValueMap<K, V> implements MultiValueMap<K, V>, Serializable { + + private static final long serialVersionUID = 3801124242820219131L; + + private final Map<K, List<V>> targetMap; + + + /** + * Create a new LinkedMultiValueMap that wraps a {@link LinkedHashMap}. + */ + public LinkedMultiValueMap() { + this.targetMap = new LinkedHashMap<K, List<V>>(); + } + + /** + * Create a new LinkedMultiValueMap that wraps a {@link LinkedHashMap} + * with the given initial capacity. + * @param initialCapacity the initial capacity + */ + public LinkedMultiValueMap(int initialCapacity) { + this.targetMap = new LinkedHashMap<K, List<V>>(initialCapacity); + } + + /** + * Copy constructor: Create a new LinkedMultiValueMap with the same mappings + * as the specified Map. + * @param otherMap the Map whose mappings are to be placed in this Map + */ + public LinkedMultiValueMap(Map<K, List<V>> otherMap) { + this.targetMap = new LinkedHashMap<K, List<V>>(otherMap); + } + + + // MultiValueMap implementation + + public void add(K key, V value) { + List<V> values = this.targetMap.get(key); + if (values == null) { + values = new LinkedList<V>(); + this.targetMap.put(key, values); + } + values.add(value); + } + + public V getFirst(K key) { + List<V> values = this.targetMap.get(key); + return (values != null ? values.get(0) : null); + } + + public void set(K key, V value) { + List<V> values = new LinkedList<V>(); + values.add(value); + this.targetMap.put(key, values); + } + + public void setAll(Map<K, V> values) { + for (Entry<K, V> entry : values.entrySet()) { + set(entry.getKey(), entry.getValue()); + } + } + + public Map<K, V> toSingleValueMap() { + LinkedHashMap<K, V> singleValueMap = new LinkedHashMap<K,V>(this.targetMap.size()); + for (Entry<K, List<V>> entry : targetMap.entrySet()) { + singleValueMap.put(entry.getKey(), entry.getValue().get(0)); + } + return singleValueMap; + } + + + // Map implementation + + public int size() { + return this.targetMap.size(); + } + + public boolean isEmpty() { + return this.targetMap.isEmpty(); + } + + public boolean containsKey(Object key) { + return this.targetMap.containsKey(key); + } + + public boolean containsValue(Object value) { + return this.targetMap.containsValue(value); + } + + public List<V> get(Object key) { + return this.targetMap.get(key); + } + + public List<V> put(K key, List<V> value) { + return this.targetMap.put(key, value); + } + + public List<V> remove(Object key) { + return this.targetMap.remove(key); + } + + public void putAll(Map<? extends K, ? extends List<V>> m) { + this.targetMap.putAll(m); + } + + public void clear() { + this.targetMap.clear(); + } + + public Set<K> keySet() { + return this.targetMap.keySet(); + } + + public Collection<List<V>> values() { + return this.targetMap.values(); + } + + public Set<Entry<K, List<V>>> entrySet() { + return this.targetMap.entrySet(); + } + + + @Override + public boolean equals(Object obj) { + return this.targetMap.equals(obj); + } + + @Override + public int hashCode() { + return this.targetMap.hashCode(); + } + + @Override + public String toString() { + return this.targetMap.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/Log4jConfigurer.java b/spring-core/src/main/java/org/springframework/util/Log4jConfigurer.java new file mode 100644 index 00000000..655bc0ea --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/Log4jConfigurer.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2012 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.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.URL; + +import org.apache.log4j.LogManager; +import org.apache.log4j.PropertyConfigurator; +import org.apache.log4j.xml.DOMConfigurator; + +/** + * Convenience class that features simple methods for custom log4j configuration. + * + * <p>Only needed for non-default log4j initialization, for example with a custom + * config location or a refresh interval. By default, log4j will simply read its + * configuration from a "log4j.properties" or "log4j.xml" file in the root of + * the classpath. + * + * <p>For web environments, the analogous Log4jWebConfigurer class can be found + * in the web package, reading in its configuration from context-params in + * {@code web.xml}. In a J2EE web application, log4j is usually set up + * via Log4jConfigListener or Log4jConfigServlet, delegating to + * Log4jWebConfigurer underneath. + * + * @author Juergen Hoeller + * @since 13.03.2003 + * @see org.springframework.web.util.Log4jWebConfigurer + * @see org.springframework.web.util.Log4jConfigListener + */ +public abstract class Log4jConfigurer { + + /** Pseudo URL prefix for loading from the class path: "classpath:" */ + public static final String CLASSPATH_URL_PREFIX = "classpath:"; + + /** Extension that indicates a log4j XML config file: ".xml" */ + public static final String XML_FILE_EXTENSION = ".xml"; + + + /** + * Initialize log4j from the given file location, with no config file refreshing. + * Assumes an XML file in case of a ".xml" file extension, and a properties file + * otherwise. + * @param location the location of the config file: either a "classpath:" location + * (e.g. "classpath:myLog4j.properties"), an absolute file URL + * (e.g. "file:C:/log4j.properties), or a plain absolute path in the file system + * (e.g. "C:/log4j.properties") + * @throws FileNotFoundException if the location specifies an invalid file path + */ + public static void initLogging(String location) throws FileNotFoundException { + String resolvedLocation = SystemPropertyUtils.resolvePlaceholders(location); + URL url = ResourceUtils.getURL(resolvedLocation); + if (resolvedLocation.toLowerCase().endsWith(XML_FILE_EXTENSION)) { + DOMConfigurator.configure(url); + } + else { + PropertyConfigurator.configure(url); + } + } + + /** + * Initialize log4j from the given location, with the given refresh interval + * for the config file. Assumes an XML file in case of a ".xml" file extension, + * and a properties file otherwise. + * <p>Log4j's watchdog thread will asynchronously check whether the timestamp + * of the config file has changed, using the given interval between checks. + * A refresh interval of 1000 milliseconds (one second), which allows to + * do on-demand log level changes with immediate effect, is not unfeasible. + * <p><b>WARNING:</b> Log4j's watchdog thread does not terminate until VM shutdown; + * in particular, it does not terminate on LogManager shutdown. Therefore, it is + * recommended to <i>not</i> use config file refreshing in a production J2EE + * environment; the watchdog thread would not stop on application shutdown there. + * @param location the location of the config file: either a "classpath:" location + * (e.g. "classpath:myLog4j.properties"), an absolute file URL + * (e.g. "file:C:/log4j.properties), or a plain absolute path in the file system + * (e.g. "C:/log4j.properties") + * @param refreshInterval interval between config file refresh checks, in milliseconds + * @throws FileNotFoundException if the location specifies an invalid file path + */ + public static void initLogging(String location, long refreshInterval) throws FileNotFoundException { + String resolvedLocation = SystemPropertyUtils.resolvePlaceholders(location); + File file = ResourceUtils.getFile(resolvedLocation); + if (!file.exists()) { + throw new FileNotFoundException("Log4j config file [" + resolvedLocation + "] not found"); + } + if (resolvedLocation.toLowerCase().endsWith(XML_FILE_EXTENSION)) { + DOMConfigurator.configureAndWatch(file.getAbsolutePath(), refreshInterval); + } + else { + PropertyConfigurator.configureAndWatch(file.getAbsolutePath(), refreshInterval); + } + } + + /** + * Shut down log4j, properly releasing all file locks. + * <p>This isn't strictly necessary, but recommended for shutting down + * log4j in a scenario where the host VM stays alive (for example, when + * shutting down an application in a J2EE environment). + */ + public static void shutdownLogging() { + LogManager.shutdown(); + } + + /** + * Set the specified system property to the current working directory. + * <p>This can be used e.g. for test environments, for applications that leverage + * Log4jWebConfigurer's "webAppRootKey" support in a web environment. + * @param key system property key to use, as expected in Log4j configuration + * (for example: "demo.root", used as "${demo.root}/WEB-INF/demo.log") + * @see org.springframework.web.util.Log4jWebConfigurer + */ + public static void setWorkingDirSystemProperty(String key) { + System.setProperty(key, new File("").getAbsolutePath()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/MethodInvoker.java b/spring-core/src/main/java/org/springframework/util/MethodInvoker.java new file mode 100644 index 00000000..d2331caf --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/MethodInvoker.java @@ -0,0 +1,323 @@ +/* + * Copyright 2002-2013 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.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * Helper class that allows for specifying a method to invoke in a declarative + * fashion, be it static or non-static. + * + * <p>Usage: Specify "targetClass"/"targetMethod" or "targetObject"/"targetMethod", + * optionally specify arguments, prepare the invoker. Afterwards, you may + * invoke the method any number of times, obtaining the invocation result. + * + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @since 19.02.2004 + * @see #prepare + * @see #invoke + */ +public class MethodInvoker { + + private Class<?> targetClass; + + private Object targetObject; + + private String targetMethod; + + private String staticMethod; + + private Object[] arguments = new Object[0]; + + /** The method we will call */ + private Method methodObject; + + + /** + * Set the target class on which to call the target method. + * Only necessary when the target method is static; else, + * a target object needs to be specified anyway. + * @see #setTargetObject + * @see #setTargetMethod + */ + public void setTargetClass(Class<?> targetClass) { + this.targetClass = targetClass; + } + + /** + * Return the target class on which to call the target method. + */ + public Class<?> getTargetClass() { + return this.targetClass; + } + + /** + * Set the target object on which to call the target method. + * Only necessary when the target method is not static; + * else, a target class is sufficient. + * @see #setTargetClass + * @see #setTargetMethod + */ + public void setTargetObject(Object targetObject) { + this.targetObject = targetObject; + if (targetObject != null) { + this.targetClass = targetObject.getClass(); + } + } + + /** + * Return the target object on which to call the target method. + */ + public Object getTargetObject() { + return this.targetObject; + } + + /** + * Set the name of the method to be invoked. + * Refers to either a static method or a non-static method, + * depending on a target object being set. + * @see #setTargetClass + * @see #setTargetObject + */ + public void setTargetMethod(String targetMethod) { + this.targetMethod = targetMethod; + } + + /** + * Return the name of the method to be invoked. + */ + public String getTargetMethod() { + return this.targetMethod; + } + + /** + * Set a fully qualified static method name to invoke, + * e.g. "example.MyExampleClass.myExampleMethod". + * Convenient alternative to specifying targetClass and targetMethod. + * @see #setTargetClass + * @see #setTargetMethod + */ + public void setStaticMethod(String staticMethod) { + this.staticMethod = staticMethod; + } + + /** + * Set arguments for the method invocation. If this property is not set, + * or the Object array is of length 0, a method with no arguments is assumed. + */ + public void setArguments(Object[] arguments) { + this.arguments = (arguments != null ? arguments : new Object[0]); + } + + /** + * Return the arguments for the method invocation. + */ + public Object[] getArguments() { + return this.arguments; + } + + + /** + * Prepare the specified method. + * The method can be invoked any number of times afterwards. + * @see #getPreparedMethod + * @see #invoke + */ + public void prepare() throws ClassNotFoundException, NoSuchMethodException { + if (this.staticMethod != null) { + int lastDotIndex = this.staticMethod.lastIndexOf('.'); + if (lastDotIndex == -1 || lastDotIndex == this.staticMethod.length()) { + throw new IllegalArgumentException( + "staticMethod must be a fully qualified class plus method name: " + + "e.g. 'example.MyExampleClass.myExampleMethod'"); + } + String className = this.staticMethod.substring(0, lastDotIndex); + String methodName = this.staticMethod.substring(lastDotIndex + 1); + this.targetClass = resolveClassName(className); + this.targetMethod = methodName; + } + + Class<?> targetClass = getTargetClass(); + String targetMethod = getTargetMethod(); + if (targetClass == null) { + throw new IllegalArgumentException("Either 'targetClass' or 'targetObject' is required"); + } + if (targetMethod == null) { + throw new IllegalArgumentException("Property 'targetMethod' is required"); + } + + Object[] arguments = getArguments(); + Class<?>[] argTypes = new Class<?>[arguments.length]; + for (int i = 0; i < arguments.length; ++i) { + argTypes[i] = (arguments[i] != null ? arguments[i].getClass() : Object.class); + } + + // Try to get the exact method first. + try { + this.methodObject = targetClass.getMethod(targetMethod, argTypes); + } + catch (NoSuchMethodException ex) { + // Just rethrow exception if we can't get any match. + this.methodObject = findMatchingMethod(); + if (this.methodObject == null) { + throw ex; + } + } + } + + /** + * Resolve the given class name into a Class. + * <p>The default implementations uses {@code ClassUtils.forName}, + * using the thread context class loader. + * @param className the class name to resolve + * @return the resolved Class + * @throws ClassNotFoundException if the class name was invalid + */ + protected Class<?> resolveClassName(String className) throws ClassNotFoundException { + return ClassUtils.forName(className, ClassUtils.getDefaultClassLoader()); + } + + /** + * Find a matching method with the specified name for the specified arguments. + * @return a matching method, or {@code null} if none + * @see #getTargetClass() + * @see #getTargetMethod() + * @see #getArguments() + */ + protected Method findMatchingMethod() { + String targetMethod = getTargetMethod(); + Object[] arguments = getArguments(); + int argCount = arguments.length; + + Method[] candidates = ReflectionUtils.getAllDeclaredMethods(getTargetClass()); + int minTypeDiffWeight = Integer.MAX_VALUE; + Method matchingMethod = null; + + for (Method candidate : candidates) { + if (candidate.getName().equals(targetMethod)) { + Class<?>[] paramTypes = candidate.getParameterTypes(); + if (paramTypes.length == argCount) { + int typeDiffWeight = getTypeDifferenceWeight(paramTypes, arguments); + if (typeDiffWeight < minTypeDiffWeight) { + minTypeDiffWeight = typeDiffWeight; + matchingMethod = candidate; + } + } + } + } + + return matchingMethod; + } + + /** + * Return the prepared Method object that will be invoked. + * <p>Can for example be used to determine the return type. + * @return the prepared Method object (never {@code null}) + * @throws IllegalStateException if the invoker hasn't been prepared yet + * @see #prepare + * @see #invoke + */ + public Method getPreparedMethod() throws IllegalStateException { + if (this.methodObject == null) { + throw new IllegalStateException("prepare() must be called prior to invoke() on MethodInvoker"); + } + return this.methodObject; + } + + /** + * Return whether this invoker has been prepared already, + * i.e. whether it allows access to {@link #getPreparedMethod()} already. + */ + public boolean isPrepared() { + return (this.methodObject != null); + } + + /** + * Invoke the specified method. + * <p>The invoker needs to have been prepared before. + * @return the object (possibly null) returned by the method invocation, + * or {@code null} if the method has a void return type + * @throws InvocationTargetException if the target method threw an exception + * @throws IllegalAccessException if the target method couldn't be accessed + * @see #prepare + */ + public Object invoke() throws InvocationTargetException, IllegalAccessException { + // In the static case, target will simply be {@code null}. + Object targetObject = getTargetObject(); + Method preparedMethod = getPreparedMethod(); + if (targetObject == null && !Modifier.isStatic(preparedMethod.getModifiers())) { + throw new IllegalArgumentException("Target method must not be non-static without a target"); + } + ReflectionUtils.makeAccessible(preparedMethod); + return preparedMethod.invoke(targetObject, getArguments()); + } + + + /** + * Algorithm that judges the match between the declared parameter types of a candidate method + * and a specific list of arguments that this method is supposed to be invoked with. + * <p>Determines a weight that represents the class hierarchy difference between types and + * arguments. A direct match, i.e. type Integer -> arg of class Integer, does not increase + * the result - all direct matches means weight 0. A match between type Object and arg of + * class Integer would increase the weight by 2, due to the superclass 2 steps up in the + * hierarchy (i.e. Object) being the last one that still matches the required type Object. + * Type Number and class Integer would increase the weight by 1 accordingly, due to the + * superclass 1 step up the hierarchy (i.e. Number) still matching the required type Number. + * Therefore, with an arg of type Integer, a constructor (Integer) would be preferred to a + * constructor (Number) which would in turn be preferred to a constructor (Object). + * All argument weights get accumulated. + * <p>Note: This is the algorithm used by MethodInvoker itself and also the algorithm + * used for constructor and factory method selection in Spring's bean container (in case + * of lenient constructor resolution which is the default for regular bean definitions). + * @param paramTypes the parameter types to match + * @param args the arguments to match + * @return the accumulated weight for all arguments + */ + public static int getTypeDifferenceWeight(Class<?>[] paramTypes, Object[] args) { + int result = 0; + for (int i = 0; i < paramTypes.length; i++) { + if (!ClassUtils.isAssignableValue(paramTypes[i], args[i])) { + return Integer.MAX_VALUE; + } + if (args[i] != null) { + Class<?> paramType = paramTypes[i]; + Class<?> superClass = args[i].getClass().getSuperclass(); + while (superClass != null) { + if (paramType.equals(superClass)) { + result = result + 2; + superClass = null; + } + else if (ClassUtils.isAssignable(paramType, superClass)) { + result = result + 2; + superClass = superClass.getSuperclass(); + } + else { + superClass = null; + } + } + if (paramType.isInterface()) { + result = result + 1; + } + } + } + return result; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/MultiValueMap.java b/spring-core/src/main/java/org/springframework/util/MultiValueMap.java new file mode 100644 index 00000000..a526af78 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/MultiValueMap.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2012 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.util; + +import java.util.List; +import java.util.Map; + +/** + * Extension of the {@code Map} interface that stores multiple values. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public interface MultiValueMap<K, V> extends Map<K, List<V>> { + + /** + * Return the first value for the given key. + * @param key the key + * @return the first value for the specified key, or {@code null} + */ + V getFirst(K key); + + /** + * Add the given single value to the current list of values for the given key. + * @param key the key + * @param value the value to be added + */ + void add(K key, V value); + + /** + * Set the given single value under the given key. + * @param key the key + * @param value the value to set + */ + void set(K key, V value); + + /** + * Set the given values under. + * @param values the values. + */ + void setAll(Map<K, V> values); + + /** + * Returns the first values contained in this {@code MultiValueMap}. + * @return a single value representation of this map + */ + Map<K, V> toSingleValueMap(); + +} diff --git a/spring-core/src/main/java/org/springframework/util/NumberUtils.java b/spring-core/src/main/java/org/springframework/util/NumberUtils.java new file mode 100644 index 00000000..47f38a0b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/NumberUtils.java @@ -0,0 +1,267 @@ +/* + * Copyright 2002-2012 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.util; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; + +/** + * Miscellaneous utility methods for number conversion and parsing. + * Mainly for internal use within the framework; consider Jakarta's + * Commons Lang for a more comprehensive suite of string utilities. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 1.1.2 + */ +public abstract class NumberUtils { + + /** + * Convert the given number into an instance of the given target class. + * @param number the number to convert + * @param targetClass the target class to convert to + * @return the converted number + * @throws IllegalArgumentException if the target class is not supported + * (i.e. not a standard Number subclass as included in the JDK) + * @see java.lang.Byte + * @see java.lang.Short + * @see java.lang.Integer + * @see java.lang.Long + * @see java.math.BigInteger + * @see java.lang.Float + * @see java.lang.Double + * @see java.math.BigDecimal + */ + @SuppressWarnings("unchecked") + public static <T extends Number> T convertNumberToTargetClass(Number number, Class<T> targetClass) + throws IllegalArgumentException { + + Assert.notNull(number, "Number must not be null"); + Assert.notNull(targetClass, "Target class must not be null"); + + if (targetClass.isInstance(number)) { + return (T) number; + } + else if (targetClass.equals(Byte.class)) { + long value = number.longValue(); + if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { + raiseOverflowException(number, targetClass); + } + return (T) new Byte(number.byteValue()); + } + else if (targetClass.equals(Short.class)) { + long value = number.longValue(); + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + raiseOverflowException(number, targetClass); + } + return (T) new Short(number.shortValue()); + } + else if (targetClass.equals(Integer.class)) { + long value = number.longValue(); + if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) { + raiseOverflowException(number, targetClass); + } + return (T) new Integer(number.intValue()); + } + else if (targetClass.equals(Long.class)) { + return (T) new Long(number.longValue()); + } + else if (targetClass.equals(BigInteger.class)) { + if (number instanceof BigDecimal) { + // do not lose precision - use BigDecimal's own conversion + return (T) ((BigDecimal) number).toBigInteger(); + } + else { + // original value is not a Big* number - use standard long conversion + return (T) BigInteger.valueOf(number.longValue()); + } + } + else if (targetClass.equals(Float.class)) { + return (T) new Float(number.floatValue()); + } + else if (targetClass.equals(Double.class)) { + return (T) new Double(number.doubleValue()); + } + else if (targetClass.equals(BigDecimal.class)) { + // always use BigDecimal(String) here to avoid unpredictability of BigDecimal(double) + // (see BigDecimal javadoc for details) + return (T) new BigDecimal(number.toString()); + } + else { + throw new IllegalArgumentException("Could not convert number [" + number + "] of type [" + + number.getClass().getName() + "] to unknown target class [" + targetClass.getName() + "]"); + } + } + + /** + * 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 + */ + private static void raiseOverflowException(Number number, Class targetClass) { + throw new IllegalArgumentException("Could not convert number [" + number + "] of type [" + + number.getClass().getName() + "] to target class [" + targetClass.getName() + "]: overflow"); + } + + /** + * Parse the given text into a number instance of the given target class, + * using the corresponding {@code decode} / {@code valueOf} methods. + * <p>Trims the input {@code String} before attempting to parse the number. + * Supports numbers in hex format (with leading "0x", "0X" or "#") as well. + * @param text the text to convert + * @param targetClass the target class to parse into + * @return the parsed number + * @throws IllegalArgumentException if the target class is not supported + * (i.e. not a standard Number subclass as included in the JDK) + * @see Byte#decode + * @see Short#decode + * @see Integer#decode + * @see Long#decode + * @see #decodeBigInteger(String) + * @see Float#valueOf + * @see Double#valueOf + * @see java.math.BigDecimal#BigDecimal(String) + */ + @SuppressWarnings("unchecked") + public static <T extends Number> T parseNumber(String text, Class<T> targetClass) { + Assert.notNull(text, "Text must not be null"); + Assert.notNull(targetClass, "Target class must not be null"); + String trimmed = StringUtils.trimAllWhitespace(text); + + if (targetClass.equals(Byte.class)) { + return (T) (isHexNumber(trimmed) ? Byte.decode(trimmed) : Byte.valueOf(trimmed)); + } + else if (targetClass.equals(Short.class)) { + return (T) (isHexNumber(trimmed) ? Short.decode(trimmed) : Short.valueOf(trimmed)); + } + else if (targetClass.equals(Integer.class)) { + return (T) (isHexNumber(trimmed) ? Integer.decode(trimmed) : Integer.valueOf(trimmed)); + } + else if (targetClass.equals(Long.class)) { + return (T) (isHexNumber(trimmed) ? Long.decode(trimmed) : Long.valueOf(trimmed)); + } + else if (targetClass.equals(BigInteger.class)) { + return (T) (isHexNumber(trimmed) ? decodeBigInteger(trimmed) : new BigInteger(trimmed)); + } + else if (targetClass.equals(Float.class)) { + return (T) Float.valueOf(trimmed); + } + else if (targetClass.equals(Double.class)) { + return (T) Double.valueOf(trimmed); + } + else if (targetClass.equals(BigDecimal.class) || targetClass.equals(Number.class)) { + return (T) new BigDecimal(trimmed); + } + else { + throw new IllegalArgumentException( + "Cannot convert String [" + text + "] to target class [" + targetClass.getName() + "]"); + } + } + + /** + * Parse the given text into a number instance of the given target class, + * using the given NumberFormat. Trims the input {@code String} + * before attempting to parse the number. + * @param text the text to convert + * @param targetClass the target class to parse into + * @param numberFormat the NumberFormat to use for parsing (if {@code null}, + * this method falls back to {@code parseNumber(String, Class)}) + * @return the parsed number + * @throws IllegalArgumentException if the target class is not supported + * (i.e. not a standard Number subclass as included in the JDK) + * @see java.text.NumberFormat#parse + * @see #convertNumberToTargetClass + * @see #parseNumber(String, Class) + */ + public static <T extends Number> T parseNumber(String text, Class<T> targetClass, NumberFormat numberFormat) { + if (numberFormat != null) { + Assert.notNull(text, "Text must not be null"); + Assert.notNull(targetClass, "Target class must not be null"); + DecimalFormat decimalFormat = null; + boolean resetBigDecimal = false; + if (numberFormat instanceof DecimalFormat) { + decimalFormat = (DecimalFormat) numberFormat; + if (BigDecimal.class.equals(targetClass) && !decimalFormat.isParseBigDecimal()) { + decimalFormat.setParseBigDecimal(true); + resetBigDecimal = true; + } + } + try { + Number number = numberFormat.parse(StringUtils.trimAllWhitespace(text)); + return convertNumberToTargetClass(number, targetClass); + } + catch (ParseException ex) { + throw new IllegalArgumentException("Could not parse number: " + ex.getMessage()); + } + finally { + if (resetBigDecimal) { + decimalFormat.setParseBigDecimal(false); + } + } + } + else { + return parseNumber(text, targetClass); + } + } + + /** + * Determine whether the given value String indicates a hex number, i.e. needs to be + * passed into {@code Integer.decode} instead of {@code Integer.valueOf} (etc). + */ + private static boolean isHexNumber(String value) { + int index = (value.startsWith("-") ? 1 : 0); + return (value.startsWith("0x", index) || value.startsWith("0X", index) || value.startsWith("#", index)); + } + + /** + * Decode a {@link java.math.BigInteger} from a {@link String} value. + * Supports decimal, hex and octal notation. + * @see BigInteger#BigInteger(String, int) + */ + private static BigInteger decodeBigInteger(String value) { + int radix = 10; + int index = 0; + boolean negative = false; + + // Handle minus sign, if present. + if (value.startsWith("-")) { + negative = true; + index++; + } + + // Handle radix specifier, if present. + if (value.startsWith("0x", index) || value.startsWith("0X", index)) { + index += 2; + radix = 16; + } + else if (value.startsWith("#", index)) { + index++; + radix = 16; + } + else if (value.startsWith("0", index) && value.length() > 1 + index) { + index++; + radix = 8; + } + + BigInteger result = new BigInteger(value.substring(index), radix); + return (negative ? result.negate() : result); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java new file mode 100644 index 00000000..218d43fd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java @@ -0,0 +1,880 @@ +/* + * Copyright 2002-2013 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.util; + +import java.lang.reflect.Array; +import java.util.Arrays; + +/** + * Miscellaneous object utility methods. + * Mainly for internal use within the framework. + * + * <p>Thanks to Alex Ruiz for contributing several enhancements to this class! + * + * @author Juergen Hoeller + * @author Keith Donald + * @author Rod Johnson + * @author Rob Harrop + * @author Chris Beams + * @since 19.03.2004 + */ +public abstract class ObjectUtils { + + private static final int INITIAL_HASH = 7; + private static final int MULTIPLIER = 31; + + private static final String EMPTY_STRING = ""; + private static final String NULL_STRING = "null"; + private static final String ARRAY_START = "{"; + private static final String ARRAY_END = "}"; + private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END; + private static final String ARRAY_ELEMENT_SEPARATOR = ", "; + + + /** + * Return whether the given throwable is a checked exception: + * that is, neither a RuntimeException nor an Error. + * @param ex the throwable to check + * @return whether the throwable is a checked exception + * @see java.lang.Exception + * @see java.lang.RuntimeException + * @see java.lang.Error + */ + public static boolean isCheckedException(Throwable ex) { + return !(ex instanceof RuntimeException || ex instanceof Error); + } + + /** + * Check whether the given exception is compatible with the specified + * exception types, as declared in a throws clause. + * @param ex the exception to check + * @param declaredExceptions the exception types declared in the throws clause + * @return whether the given exception is compatible + */ + public static boolean isCompatibleWithThrowsClause(Throwable ex, Class<?>... declaredExceptions) { + if (!isCheckedException(ex)) { + return true; + } + if (declaredExceptions != null) { + for (Class<?> declaredException : declaredExceptions) { + if (declaredException.isInstance(ex)) { + return true; + } + } + } + return false; + } + + /** + * Determine whether the given object is an array: + * either an Object array or a primitive array. + * @param obj the object to check + */ + public static boolean isArray(Object obj) { + return (obj != null && obj.getClass().isArray()); + } + + /** + * Determine whether the given array is empty: + * i.e. {@code null} or of zero length. + * @param array the array to check + */ + public static boolean isEmpty(Object[] array) { + return (array == null || array.length == 0); + } + + /** + * Check whether the given array contains the given element. + * @param array the array to check (may be {@code null}, + * in which case the return value will always be {@code false}) + * @param element the element to check for + * @return whether the element has been found in the given array + */ + public static boolean containsElement(Object[] array, Object element) { + if (array == null) { + return false; + } + for (Object arrayEle : array) { + if (nullSafeEquals(arrayEle, element)) { + return true; + } + } + return false; + } + + /** + * Check whether the given array of enum constants contains a constant with the given name, + * ignoring case when determining a match. + * @param enumValues the enum values to check, typically the product of a call to MyEnum.values() + * @param constant the constant name to find (must not be null or empty string) + * @return whether the constant has been found in the given array + */ + public static boolean containsConstant(Enum<?>[] enumValues, String constant) { + return containsConstant(enumValues, constant, false); + } + + /** + * Check whether the given array of enum constants contains a constant with the given name. + * @param enumValues the enum values to check, typically the product of a call to MyEnum.values() + * @param constant the constant name to find (must not be null or empty string) + * @param caseSensitive whether case is significant in determining a match + * @return whether the constant has been found in the given array + */ + public static boolean containsConstant(Enum<?>[] enumValues, String constant, boolean caseSensitive) { + for (Enum<?> candidate : enumValues) { + if (caseSensitive ? + candidate.toString().equals(constant) : + candidate.toString().equalsIgnoreCase(constant)) { + return true; + } + } + return false; + } + + /** + * Case insensitive alternative to {@link Enum#valueOf(Class, String)}. + * @param <E> the concrete Enum type + * @param enumValues the array of all Enum constants in question, usually per Enum.values() + * @param constant the constant to get the enum value of + * @throws IllegalArgumentException if the given constant is not found in the given array + * of enum values. Use {@link #containsConstant(Enum[], String)} as a guard to avoid this exception. + */ + public static <E extends Enum<?>> E caseInsensitiveValueOf(E[] enumValues, String constant) { + for (E candidate : enumValues) { + if (candidate.toString().equalsIgnoreCase(constant)) { + return candidate; + } + } + throw new IllegalArgumentException( + String.format("constant [%s] does not exist in enum type %s", + constant, enumValues.getClass().getComponentType().getName())); + } + + /** + * Append the given object to the given array, returning a new array + * consisting of the input array contents plus the given object. + * @param array the array to append to (can be {@code null}) + * @param obj the object to append + * @return the new array (of the same component type; never {@code null}) + */ + public static <A, O extends A> A[] addObjectToArray(A[] array, O obj) { + Class<?> compType = Object.class; + if (array != null) { + compType = array.getClass().getComponentType(); + } + else if (obj != null) { + compType = obj.getClass(); + } + int newArrLength = (array != null ? array.length + 1 : 1); + @SuppressWarnings("unchecked") + A[] newArr = (A[]) Array.newInstance(compType, newArrLength); + if (array != null) { + System.arraycopy(array, 0, newArr, 0, array.length); + } + newArr[newArr.length - 1] = obj; + return newArr; + } + + /** + * Convert the given array (which may be a primitive array) to an + * object array (if necessary of primitive wrapper objects). + * <p>A {@code null} source value will be converted to an + * empty Object array. + * @param source the (potentially primitive) array + * @return the corresponding object array (never {@code null}) + * @throws IllegalArgumentException if the parameter is not an array + */ + public static Object[] toObjectArray(Object source) { + if (source instanceof Object[]) { + return (Object[]) source; + } + if (source == null) { + return new Object[0]; + } + if (!source.getClass().isArray()) { + throw new IllegalArgumentException("Source is not an array: " + source); + } + int length = Array.getLength(source); + if (length == 0) { + return new Object[0]; + } + Class<?> wrapperType = Array.get(source, 0).getClass(); + Object[] newArray = (Object[]) Array.newInstance(wrapperType, length); + for (int i = 0; i < length; i++) { + newArray[i] = Array.get(source, i); + } + return newArray; + } + + + //--------------------------------------------------------------------- + // Convenience methods for content-based equality/hash-code handling + //--------------------------------------------------------------------- + + /** + * Determine if the given objects are equal, returning {@code true} + * if both are {@code null} or {@code false} if only one is + * {@code null}. + * <p>Compares arrays with {@code Arrays.equals}, performing an equality + * check based on the array elements rather than the array reference. + * @param o1 first Object to compare + * @param o2 second Object to compare + * @return whether the given objects are equal + * @see java.util.Arrays#equals + */ + public static boolean nullSafeEquals(Object o1, Object o2) { + if (o1 == o2) { + return true; + } + if (o1 == null || o2 == null) { + return false; + } + if (o1.equals(o2)) { + return true; + } + if (o1.getClass().isArray() && o2.getClass().isArray()) { + if (o1 instanceof Object[] && o2 instanceof Object[]) { + return Arrays.equals((Object[]) o1, (Object[]) o2); + } + if (o1 instanceof boolean[] && o2 instanceof boolean[]) { + return Arrays.equals((boolean[]) o1, (boolean[]) o2); + } + if (o1 instanceof byte[] && o2 instanceof byte[]) { + return Arrays.equals((byte[]) o1, (byte[]) o2); + } + if (o1 instanceof char[] && o2 instanceof char[]) { + return Arrays.equals((char[]) o1, (char[]) o2); + } + if (o1 instanceof double[] && o2 instanceof double[]) { + return Arrays.equals((double[]) o1, (double[]) o2); + } + if (o1 instanceof float[] && o2 instanceof float[]) { + return Arrays.equals((float[]) o1, (float[]) o2); + } + if (o1 instanceof int[] && o2 instanceof int[]) { + return Arrays.equals((int[]) o1, (int[]) o2); + } + if (o1 instanceof long[] && o2 instanceof long[]) { + return Arrays.equals((long[]) o1, (long[]) o2); + } + if (o1 instanceof short[] && o2 instanceof short[]) { + return Arrays.equals((short[]) o1, (short[]) o2); + } + } + return false; + } + + /** + * Return as hash code for the given object; typically the value of + * {@code Object#hashCode()}}. If the object is an array, + * this method will delegate to any of the {@code nullSafeHashCode} + * methods for arrays in this class. If the object is {@code null}, + * this method returns 0. + * @see #nullSafeHashCode(Object[]) + * @see #nullSafeHashCode(boolean[]) + * @see #nullSafeHashCode(byte[]) + * @see #nullSafeHashCode(char[]) + * @see #nullSafeHashCode(double[]) + * @see #nullSafeHashCode(float[]) + * @see #nullSafeHashCode(int[]) + * @see #nullSafeHashCode(long[]) + * @see #nullSafeHashCode(short[]) + */ + public static int nullSafeHashCode(Object obj) { + if (obj == null) { + return 0; + } + if (obj.getClass().isArray()) { + if (obj instanceof Object[]) { + return nullSafeHashCode((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeHashCode((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeHashCode((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeHashCode((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeHashCode((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeHashCode((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeHashCode((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeHashCode((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeHashCode((short[]) obj); + } + } + return obj.hashCode(); + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(Object[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (Object element : array) { + hash = MULTIPLIER * hash + nullSafeHashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(boolean[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (boolean element : array) { + hash = MULTIPLIER * hash + hashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(byte[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (byte element : array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(char[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (char element : array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(double[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (double element : array) { + hash = MULTIPLIER * hash + hashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(float[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (float element : array) { + hash = MULTIPLIER * hash + hashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(int[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (int element : array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(long[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (long element : array) { + hash = MULTIPLIER * hash + hashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(short[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (short element : array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } + + /** + * Return the same value as {@link Boolean#hashCode()}}. + * @see Boolean#hashCode() + */ + public static int hashCode(boolean bool) { + return (bool ? 1231 : 1237); + } + + /** + * Return the same value as {@link Double#hashCode()}}. + * @see Double#hashCode() + */ + public static int hashCode(double dbl) { + return hashCode(Double.doubleToLongBits(dbl)); + } + + /** + * Return the same value as {@link Float#hashCode()}}. + * @see Float#hashCode() + */ + public static int hashCode(float flt) { + return Float.floatToIntBits(flt); + } + + /** + * Return the same value as {@link Long#hashCode()}}. + * @see Long#hashCode() + */ + public static int hashCode(long lng) { + return (int) (lng ^ (lng >>> 32)); + } + + + //--------------------------------------------------------------------- + // Convenience methods for toString output + //--------------------------------------------------------------------- + + /** + * Return a String representation of an object's overall identity. + * @param obj the object (may be {@code null}) + * @return the object's identity as String representation, + * or an empty String if the object was {@code null} + */ + public static String identityToString(Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return obj.getClass().getName() + "@" + getIdentityHexString(obj); + } + + /** + * Return a hex String form of an object's identity hash code. + * @param obj the object + * @return the object's identity code in hex notation + */ + public static String getIdentityHexString(Object obj) { + return Integer.toHexString(System.identityHashCode(obj)); + } + + /** + * Return a content-based String representation if {@code obj} is + * not {@code null}; otherwise returns an empty String. + * <p>Differs from {@link #nullSafeToString(Object)} in that it returns + * an empty String rather than "null" for a {@code null} value. + * @param obj the object to build a display String for + * @return a display String representation of {@code obj} + * @see #nullSafeToString(Object) + */ + public static String getDisplayString(Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return nullSafeToString(obj); + } + + /** + * Determine the class name for the given object. + * <p>Returns {@code "null"} if {@code obj} is {@code null}. + * @param obj the object to introspect (may be {@code null}) + * @return the corresponding class name + */ + public static String nullSafeClassName(Object obj) { + return (obj != null ? obj.getClass().getName() : NULL_STRING); + } + + /** + * Return a String representation of the specified Object. + * <p>Builds a String representation of the contents in case of an array. + * Returns {@code "null"} if {@code obj} is {@code null}. + * @param obj the object to build a String representation for + * @return a String representation of {@code obj} + */ + public static String nullSafeToString(Object obj) { + if (obj == null) { + return NULL_STRING; + } + if (obj instanceof String) { + return (String) obj; + } + if (obj instanceof Object[]) { + return nullSafeToString((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeToString((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeToString((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeToString((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeToString((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeToString((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeToString((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeToString((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeToString((short[]) obj); + } + String str = obj.toString(); + return (str != null ? str : EMPTY_STRING); + } + + /** + * Return a String representation of the contents of the specified array. + * <p>The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(Object[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(String.valueOf(array[i])); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * <p>The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(boolean[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * <p>The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(byte[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * <p>The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(char[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append("'").append(array[i]).append("'"); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * <p>The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(double[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * <p>The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(float[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * <p>The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(int[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * <p>The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(long[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + * <p>The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). Returns + * {@code "null"} if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(short[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/PathMatcher.java b/spring-core/src/main/java/org/springframework/util/PathMatcher.java new file mode 100644 index 00000000..0bea9cd8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/PathMatcher.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2012 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.util; + +import java.util.Comparator; +import java.util.Map; + +/** + * Strategy interface for {@code String}-based path matching. + * + * <p>Used by {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver}, + * {@link org.springframework.web.servlet.handler.AbstractUrlHandlerMapping}, + * {@link org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver}, + * and {@link org.springframework.web.servlet.mvc.WebContentInterceptor}. + * + * <p>The default implementation is {@link AntPathMatcher}, supporting the + * Ant-style pattern syntax. + * + * @author Juergen Hoeller + * @since 1.2 + * @see AntPathMatcher + */ +public interface PathMatcher { + + /** + * Does the given {@code path} represent a pattern that can be matched + * by an implementation of this interface? + * <p>If the return value is {@code false}, then the {@link #match} + * method does not have to be used because direct equality comparisons + * on the static path Strings will lead to the same result. + * @param path the path String to check + * @return {@code true} if the given {@code path} represents a pattern + */ + boolean isPattern(String path); + + /** + * Match the given {@code path} against the given {@code pattern}, + * according to this PathMatcher's matching strategy. + * @param pattern the pattern to match against + * @param path the path String to test + * @return {@code true} if the supplied {@code path} matched, + * {@code false} if it didn't + */ + boolean match(String pattern, String path); + + /** + * Match the given {@code path} against the corresponding part of the given + * {@code pattern}, according to this PathMatcher's matching strategy. + * <p>Determines whether the pattern at least matches as far as the given base + * path goes, assuming that a full path may then match as well. + * @param pattern the pattern to match against + * @param path the path String to test + * @return {@code true} if the supplied {@code path} matched, + * {@code false} if it didn't + */ + boolean matchStart(String pattern, String path); + + /** + * Given a pattern and a full path, determine the pattern-mapped part. + * <p>This method is supposed to find out which part of the path is matched + * dynamically through an actual pattern, that is, it strips off a statically + * defined leading path from the given full path, returning only the actually + * pattern-matched part of the path. + * <p>For example: For "myroot/*.html" as pattern and "myroot/myfile.html" + * as full path, this method should return "myfile.html". The detailed + * determination rules are specified to this PathMatcher's matching strategy. + * <p>A simple implementation may return the given full path as-is in case + * of an actual pattern, and the empty String in case of the pattern not + * containing any dynamic parts (i.e. the {@code pattern} parameter being + * a static path that wouldn't qualify as an actual {@link #isPattern pattern}). + * A sophisticated implementation will differentiate between the static parts + * and the dynamic parts of the given path pattern. + * @param pattern the path pattern + * @param path the full path to introspect + * @return the pattern-mapped part of the given {@code path} + * (never {@code null}) + */ + String extractPathWithinPattern(String pattern, String path); + + /** + * Given a pattern and a full path, extract the URI template variables. URI template + * variables are expressed through curly brackets ('{' and '}'). + * <p>For example: For pattern "/hotels/{hotel}" and path "/hotels/1", this method will + * return a map containing "hotel"->"1". + * @param pattern the path pattern, possibly containing URI templates + * @param path the full path to extract template variables from + * @return a map, containing variable names as keys; variables values as values + */ + Map<String, String> extractUriTemplateVariables(String pattern, String path); + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns + * in order of explicitness for that path. + * <p>The full algorithm used depends on the underlying implementation, but generally, + * the returned {@code Comparator} will + * {@linkplain java.util.Collections#sort(java.util.List, java.util.Comparator) sort} + * a list so that more specific patterns come before generic patterns. + * @param path the full path to use for comparison + * @return a comparator capable of sorting patterns in order of explicitness + */ + Comparator<String> getPatternComparator(String path); + + /** + * Combines two patterns into a new pattern that is returned. + * <p>The full algorithm used for combining the two pattern depends on the underlying implementation. + * @param pattern1 the first pattern + * @param pattern2 the second pattern + * @return the combination of the two patterns + * @throws IllegalArgumentException when the two patterns cannot be combined + */ + String combine(String pattern1, String pattern2); + +} diff --git a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java new file mode 100644 index 00000000..0b7c80a4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2007 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.util; + +/** + * Utility methods for simple pattern matching, in particular for + * Spring's typical "xxx*", "*xxx" and "*xxx*" pattern styles. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class PatternMatchUtils { + + /** + * Match a String against the given pattern, supporting the following simple + * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an + * arbitrary number of pattern parts), as well as direct equality. + * @param pattern the pattern to match against + * @param str the String to match + * @return whether the String matches the given pattern + */ + public static boolean simpleMatch(String pattern, String str) { + if (pattern == null || str == null) { + return false; + } + int firstIndex = pattern.indexOf('*'); + if (firstIndex == -1) { + return pattern.equals(str); + } + if (firstIndex == 0) { + if (pattern.length() == 1) { + return true; + } + int nextIndex = pattern.indexOf('*', firstIndex + 1); + if (nextIndex == -1) { + return str.endsWith(pattern.substring(1)); + } + String part = pattern.substring(1, nextIndex); + int partIndex = str.indexOf(part); + while (partIndex != -1) { + if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()))) { + return true; + } + partIndex = str.indexOf(part, partIndex + 1); + } + return false; + } + return (str.length() >= firstIndex && + pattern.substring(0, firstIndex).equals(str.substring(0, firstIndex)) && + simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex))); + } + + /** + * Match a String against the given patterns, supporting the following simple + * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an + * arbitrary number of pattern parts), as well as direct equality. + * @param patterns the patterns to match against + * @param str the String to match + * @return whether the String matches any of the given patterns + */ + public static boolean simpleMatch(String[] patterns, String str) { + if (patterns != null) { + for (int i = 0; i < patterns.length; i++) { + if (simpleMatch(patterns[i], str)) { + return true; + } + } + } + return false; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/PropertiesPersister.java b/spring-core/src/main/java/org/springframework/util/PropertiesPersister.java new file mode 100644 index 00000000..1e4c3c97 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/PropertiesPersister.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2012 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.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.Properties; + +/** + * Strategy interface for persisting {@code java.util.Properties}, + * allowing for pluggable parsing strategies. + * + * <p>The default implementation is DefaultPropertiesPersister, + * providing the native parsing of {@code java.util.Properties}, + * but allowing for reading from any Reader and writing to any Writer + * (which allows to specify an encoding for a properties file). + * + * <p>As of Spring 1.2.2, this interface also supports properties XML files, + * through the {@code loadFromXml} and {@code storeToXml} methods. + * The default implementations delegate to JDK 1.5's corresponding methods. + * + * @author Juergen Hoeller + * @since 10.03.2004 + * @see DefaultPropertiesPersister + * @see java.util.Properties + */ +public interface PropertiesPersister { + + /** + * Load properties from the given InputStream into the given + * Properties object. + * @param props the Properties object to load into + * @param is the InputStream to load from + * @throws IOException in case of I/O errors + * @see java.util.Properties#load + */ + void load(Properties props, InputStream is) throws IOException; + + /** + * Load properties from the given Reader into the given + * Properties object. + * @param props the Properties object to load into + * @param reader the Reader to load from + * @throws IOException in case of I/O errors + */ + void load(Properties props, Reader reader) throws IOException; + + + /** + * Write the contents of the given Properties object to the + * given OutputStream. + * @param props the Properties object to store + * @param os the OutputStream to write to + * @param header the description of the property list + * @throws IOException in case of I/O errors + * @see java.util.Properties#store + */ + void store(Properties props, OutputStream os, String header) throws IOException; + + /** + * Write the contents of the given Properties object to the + * given Writer. + * @param props the Properties object to store + * @param writer the Writer to write to + * @param header the description of the property list + * @throws IOException in case of I/O errors + */ + void store(Properties props, Writer writer, String header) throws IOException; + + + /** + * Load properties from the given XML InputStream into the + * given Properties object. + * @param props the Properties object to load into + * @param is the InputStream to load from + * @throws IOException in case of I/O errors + * @see java.util.Properties#loadFromXML(java.io.InputStream) + */ + void loadFromXml(Properties props, InputStream is) throws IOException; + + /** + * Write the contents of the given Properties object to the + * given XML OutputStream. + * @param props the Properties object to store + * @param os the OutputStream to write to + * @param header the description of the property list + * @throws IOException in case of I/O errors + * @see java.util.Properties#storeToXML(java.io.OutputStream, String) + */ + void storeToXml(Properties props, OutputStream os, String header) throws IOException; + + /** + * Write the contents of the given Properties object to the + * given XML OutputStream. + * @param props the Properties object to store + * @param os the OutputStream to write to + * @param encoding the encoding to use + * @param header the description of the property list + * @throws IOException in case of I/O errors + * @see java.util.Properties#storeToXML(java.io.OutputStream, String, String) + */ + void storeToXml(Properties props, OutputStream os, String header, String encoding) throws IOException; + +} diff --git a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java new file mode 100644 index 00000000..1ce878f8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java @@ -0,0 +1,224 @@ +/* + * Copyright 2002-2014 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.util; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Utility class for working with Strings that have placeholder values in them. A placeholder takes the form + * {@code ${name}}. Using {@code PropertyPlaceholderHelper} these placeholders can be substituted for + * user-supplied values. <p> Values for substitution can be supplied using a {@link Properties} instance or + * using a {@link PlaceholderResolver}. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 3.0 + */ +public class PropertyPlaceholderHelper { + + private static final Log logger = LogFactory.getLog(PropertyPlaceholderHelper.class); + + private static final Map<String, String> wellKnownSimplePrefixes = new HashMap<String, String>(4); + + static { + wellKnownSimplePrefixes.put("}", "{"); + wellKnownSimplePrefixes.put("]", "["); + wellKnownSimplePrefixes.put(")", "("); + } + + + private final String placeholderPrefix; + + private final String placeholderSuffix; + + private final String simplePrefix; + + private final String valueSeparator; + + private final boolean ignoreUnresolvablePlaceholders; + + + /** + * Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. + * Unresolvable placeholders are ignored. + * @param placeholderPrefix the prefix that denotes the start of a placeholder + * @param placeholderSuffix the suffix that denotes the end of a placeholder + */ + public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix) { + this(placeholderPrefix, placeholderSuffix, null, true); + } + + /** + * Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. + * @param placeholderPrefix the prefix that denotes the start of a placeholder + * @param placeholderSuffix the suffix that denotes the end of a placeholder + * @param valueSeparator the separating character between the placeholder variable + * and the associated default value, if any + * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should + * be ignored ({@code true}) or cause an exception ({@code false}) + */ + public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix, + String valueSeparator, boolean ignoreUnresolvablePlaceholders) { + + Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null"); + Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null"); + this.placeholderPrefix = placeholderPrefix; + this.placeholderSuffix = placeholderSuffix; + String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix); + if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) { + this.simplePrefix = simplePrefixForSuffix; + } + else { + this.simplePrefix = this.placeholderPrefix; + } + this.valueSeparator = valueSeparator; + this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + } + + + /** + * Replaces all placeholders of format {@code ${name}} with the corresponding + * property from the supplied {@link Properties}. + * @param value the value containing the placeholders to be replaced + * @param properties the {@code Properties} to use for replacement + * @return the supplied value with placeholders replaced inline + */ + public String replacePlaceholders(String value, final Properties properties) { + Assert.notNull(properties, "'properties' must not be null"); + return replacePlaceholders(value, new PlaceholderResolver() { + public String resolvePlaceholder(String placeholderName) { + return properties.getProperty(placeholderName); + } + }); + } + + /** + * Replaces all placeholders of format {@code ${name}} with the value returned + * from the supplied {@link PlaceholderResolver}. + * @param value the value containing the placeholders to be replaced + * @param placeholderResolver the {@code PlaceholderResolver} to use for replacement + * @return the supplied value with placeholders replaced inline + */ + public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) { + Assert.notNull(value, "'value' must not be null"); + return parseStringValue(value, placeholderResolver, new HashSet<String>()); + } + + protected String parseStringValue( + String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) { + + StringBuilder result = new StringBuilder(strVal); + + int startIndex = strVal.indexOf(this.placeholderPrefix); + while (startIndex != -1) { + int endIndex = findPlaceholderEndIndex(result, startIndex); + if (endIndex != -1) { + String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex); + String originalPlaceholder = placeholder; + if (!visitedPlaceholders.add(originalPlaceholder)) { + throw new IllegalArgumentException( + "Circular placeholder reference '" + originalPlaceholder + "' in property definitions"); + } + // Recursive invocation, parsing placeholders contained in the placeholder key. + placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); + // Now obtain the value for the fully resolved key... + String propVal = placeholderResolver.resolvePlaceholder(placeholder); + if (propVal == null && this.valueSeparator != null) { + int separatorIndex = placeholder.indexOf(this.valueSeparator); + if (separatorIndex != -1) { + String actualPlaceholder = placeholder.substring(0, separatorIndex); + String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length()); + propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder); + if (propVal == null) { + propVal = defaultValue; + } + } + } + if (propVal != null) { + // Recursive invocation, parsing placeholders contained in the + // previously resolved placeholder value. + propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders); + result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal); + if (logger.isTraceEnabled()) { + logger.trace("Resolved placeholder '" + placeholder + "'"); + } + startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length()); + } + else if (this.ignoreUnresolvablePlaceholders) { + // Proceed with unprocessed value. + startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length()); + } + else { + throw new IllegalArgumentException("Could not resolve placeholder '" + + placeholder + "'" + " in string value \"" + strVal + "\""); + } + visitedPlaceholders.remove(originalPlaceholder); + } + else { + startIndex = -1; + } + } + + return result.toString(); + } + + private int findPlaceholderEndIndex(CharSequence buf, int startIndex) { + int index = startIndex + this.placeholderPrefix.length(); + int withinNestedPlaceholder = 0; + while (index < buf.length()) { + if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) { + if (withinNestedPlaceholder > 0) { + withinNestedPlaceholder--; + index = index + this.placeholderSuffix.length(); + } + else { + return index; + } + } + else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) { + withinNestedPlaceholder++; + index = index + this.simplePrefix.length(); + } + else { + index++; + } + } + return -1; + } + + + /** + * Strategy interface used to resolve replacement values for placeholders contained in Strings. + */ + public static interface PlaceholderResolver { + + /** + * Resolve the supplied placeholder name to the replacement value. + * @param placeholderName the name of the placeholder to resolve + * @return the replacement value, or {@code null} if no replacement is to be made + */ + String resolvePlaceholder(String placeholderName); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java new file mode 100644 index 00000000..502d6827 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -0,0 +1,727 @@ +/* + * Copyright 2002-2014 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.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.UndeclaredThrowableException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Simple utility class for working with the reflection API and handling + * reflection exceptions. + * + * <p>Only intended for internal use. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Rod Johnson + * @author Costin Leau + * @author Sam Brannen + * @author Chris Beams + * @since 1.2.2 + */ +public abstract class ReflectionUtils { + + /** + * Naming prefix for CGLIB-renamed methods. + * @see #isCglibRenamedMethod + */ + private static final String CGLIB_RENAMED_METHOD_PREFIX = "CGLIB$"; + + /** + * Pattern for detecting CGLIB-renamed methods. + * @see #isCglibRenamedMethod + */ + private static final Pattern CGLIB_RENAMED_METHOD_PATTERN = Pattern.compile("(.+)\\$\\d+"); + + /** + * Cache for {@link Class#getDeclaredMethods()}, allowing for fast resolution. + */ + private static final Map<Class<?>, Method[]> declaredMethodsCache = + new ConcurrentReferenceHashMap<Class<?>, Method[]>(256); + + + /** + * Attempt to find a {@link Field field} on the supplied {@link Class} with the + * supplied {@code name}. Searches all superclasses up to {@link Object}. + * @param clazz the class to introspect + * @param name the name of the field + * @return the corresponding Field object, or {@code null} if not found + */ + public static Field findField(Class<?> clazz, String name) { + return findField(clazz, name, null); + } + + /** + * Attempt to find a {@link Field field} on the supplied {@link Class} with the + * supplied {@code name} and/or {@link Class type}. Searches all superclasses + * up to {@link Object}. + * @param clazz the class to introspect + * @param name the name of the field (may be {@code null} if type is specified) + * @param type the type of the field (may be {@code null} if name is specified) + * @return the corresponding Field object, or {@code null} if not found + */ + public static Field findField(Class<?> clazz, String name, Class<?> type) { + Assert.notNull(clazz, "Class must not be null"); + Assert.isTrue(name != null || type != null, "Either name or type of the field must be specified"); + Class<?> searchType = clazz; + while (!Object.class.equals(searchType) && searchType != null) { + Field[] fields = searchType.getDeclaredFields(); + for (Field field : fields) { + if ((name == null || name.equals(field.getName())) && (type == null || type.equals(field.getType()))) { + return field; + } + } + searchType = searchType.getSuperclass(); + } + return null; + } + + /** + * Set the field represented by the supplied {@link Field field object} on the + * specified {@link Object target object} to the specified {@code value}. + * In accordance with {@link Field#set(Object, Object)} semantics, the new value + * is automatically unwrapped if the underlying field has a primitive type. + * <p>Thrown exceptions are handled via a call to {@link #handleReflectionException(Exception)}. + * @param field the field to set + * @param target the target object on which to set the field + * @param value the value to set; may be {@code null} + */ + public static void setField(Field field, Object target, Object value) { + try { + field.set(target, value); + } + catch (IllegalAccessException ex) { + handleReflectionException(ex); + throw new IllegalStateException( + "Unexpected reflection exception - " + ex.getClass().getName() + ": " + ex.getMessage()); + } + } + + /** + * Get the field represented by the supplied {@link Field field object} on the + * specified {@link Object target object}. In accordance with {@link Field#get(Object)} + * semantics, the returned value is automatically wrapped if the underlying field + * has a primitive type. + * <p>Thrown exceptions are handled via a call to {@link #handleReflectionException(Exception)}. + * @param field the field to get + * @param target the target object from which to get the field + * @return the field's current value + */ + public static Object getField(Field field, Object target) { + try { + return field.get(target); + } + catch (IllegalAccessException ex) { + handleReflectionException(ex); + throw new IllegalStateException( + "Unexpected reflection exception - " + ex.getClass().getName() + ": " + ex.getMessage()); + } + } + + /** + * Attempt to find a {@link Method} on the supplied class with the supplied name + * and no parameters. Searches all superclasses up to {@code Object}. + * <p>Returns {@code null} if no {@link Method} can be found. + * @param clazz the class to introspect + * @param name the name of the method + * @return the Method object, or {@code null} if none found + */ + public static Method findMethod(Class<?> clazz, String name) { + return findMethod(clazz, name, new Class<?>[0]); + } + + /** + * Attempt to find a {@link Method} on the supplied class with the supplied name + * and parameter types. Searches all superclasses up to {@code Object}. + * <p>Returns {@code null} if no {@link Method} can be found. + * @param clazz the class to introspect + * @param name the name of the method + * @param paramTypes the parameter types of the method + * (may be {@code null} to indicate any signature) + * @return the Method object, or {@code null} if none found + */ + public static Method findMethod(Class<?> clazz, String name, Class<?>... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(name, "Method name must not be null"); + Class<?> searchType = clazz; + while (searchType != null) { + Method[] methods = (searchType.isInterface() ? searchType.getMethods() : getDeclaredMethods(searchType)); + for (Method method : methods) { + if (name.equals(method.getName()) && + (paramTypes == null || Arrays.equals(paramTypes, method.getParameterTypes()))) { + return method; + } + } + searchType = searchType.getSuperclass(); + } + return null; + } + + /** + * Invoke the specified {@link Method} against the supplied target object with no arguments. + * The target object can be {@code null} when invoking a static {@link Method}. + * <p>Thrown exceptions are handled via a call to {@link #handleReflectionException}. + * @param method the method to invoke + * @param target the target object to invoke the method on + * @return the invocation result, if any + * @see #invokeMethod(java.lang.reflect.Method, Object, Object[]) + */ + public static Object invokeMethod(Method method, Object target) { + return invokeMethod(method, target, new Object[0]); + } + + /** + * Invoke the specified {@link Method} against the supplied target object with the + * supplied arguments. The target object can be {@code null} when invoking a + * static {@link Method}. + * <p>Thrown exceptions are handled via a call to {@link #handleReflectionException}. + * @param method the method to invoke + * @param target the target object to invoke the method on + * @param args the invocation arguments (may be {@code null}) + * @return the invocation result, if any + */ + public static Object invokeMethod(Method method, Object target, Object... args) { + try { + return method.invoke(target, args); + } + catch (Exception ex) { + handleReflectionException(ex); + } + throw new IllegalStateException("Should never get here"); + } + + /** + * Invoke the specified JDBC API {@link Method} against the supplied target + * object with no arguments. + * @param method the method to invoke + * @param target the target object to invoke the method on + * @return the invocation result, if any + * @throws SQLException the JDBC API SQLException to rethrow (if any) + * @see #invokeJdbcMethod(java.lang.reflect.Method, Object, Object[]) + */ + public static Object invokeJdbcMethod(Method method, Object target) throws SQLException { + return invokeJdbcMethod(method, target, new Object[0]); + } + + /** + * Invoke the specified JDBC API {@link Method} against the supplied target + * object with the supplied arguments. + * @param method the method to invoke + * @param target the target object to invoke the method on + * @param args the invocation arguments (may be {@code null}) + * @return the invocation result, if any + * @throws SQLException the JDBC API SQLException to rethrow (if any) + * @see #invokeMethod(java.lang.reflect.Method, Object, Object[]) + */ + public static Object invokeJdbcMethod(Method method, Object target, Object... args) throws SQLException { + try { + return method.invoke(target, args); + } + catch (IllegalAccessException ex) { + handleReflectionException(ex); + } + catch (InvocationTargetException ex) { + if (ex.getTargetException() instanceof SQLException) { + throw (SQLException) ex.getTargetException(); + } + handleInvocationTargetException(ex); + } + throw new IllegalStateException("Should never get here"); + } + + /** + * Handle the given reflection exception. Should only be called if no + * checked exception is expected to be thrown by the target method. + * <p>Throws the underlying RuntimeException or Error in case of an + * InvocationTargetException with such a root cause. Throws an + * IllegalStateException with an appropriate message else. + * @param ex the reflection exception to handle + */ + public static void handleReflectionException(Exception ex) { + if (ex instanceof NoSuchMethodException) { + throw new IllegalStateException("Method not found: " + ex.getMessage()); + } + if (ex instanceof IllegalAccessException) { + throw new IllegalStateException("Could not access method: " + ex.getMessage()); + } + if (ex instanceof InvocationTargetException) { + handleInvocationTargetException((InvocationTargetException) ex); + } + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + throw new UndeclaredThrowableException(ex); + } + + /** + * Handle the given invocation target exception. Should only be called if no + * checked exception is expected to be thrown by the target method. + * <p>Throws the underlying RuntimeException or Error in case of such a root + * cause. Throws an IllegalStateException else. + * @param ex the invocation target exception to handle + */ + public static void handleInvocationTargetException(InvocationTargetException ex) { + rethrowRuntimeException(ex.getTargetException()); + } + + /** + * Rethrow the given {@link Throwable exception}, which is presumably the + * <em>target exception</em> of an {@link InvocationTargetException}. Should + * only be called if no checked exception is expected to be thrown by the + * target method. + * <p>Rethrows the underlying exception cast to an {@link RuntimeException} or + * {@link Error} if appropriate; otherwise, throws an + * {@link IllegalStateException}. + * @param ex the exception to rethrow + * @throws RuntimeException the rethrown exception + */ + public static void rethrowRuntimeException(Throwable ex) { + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + if (ex instanceof Error) { + throw (Error) ex; + } + throw new UndeclaredThrowableException(ex); + } + + /** + * Rethrow the given {@link Throwable exception}, which is presumably the + * <em>target exception</em> of an {@link InvocationTargetException}. Should + * only be called if no checked exception is expected to be thrown by the + * target method. + * <p>Rethrows the underlying exception cast to an {@link Exception} or + * {@link Error} if appropriate; otherwise, throws an + * {@link IllegalStateException}. + * @param ex the exception to rethrow + * @throws Exception the rethrown exception (in case of a checked exception) + */ + public static void rethrowException(Throwable ex) throws Exception { + if (ex instanceof Exception) { + throw (Exception) ex; + } + if (ex instanceof Error) { + throw (Error) ex; + } + throw new UndeclaredThrowableException(ex); + } + + /** + * Determine whether the given method explicitly declares the given + * exception or one of its superclasses, which means that an exception of + * that type can be propagated as-is within a reflective invocation. + * @param method the declaring method + * @param exceptionType the exception to throw + * @return {@code true} if the exception can be thrown as-is; + * {@code false} if it needs to be wrapped + */ + public static boolean declaresException(Method method, Class<?> exceptionType) { + Assert.notNull(method, "Method must not be null"); + Class<?>[] declaredExceptions = method.getExceptionTypes(); + for (Class<?> declaredException : declaredExceptions) { + if (declaredException.isAssignableFrom(exceptionType)) { + return true; + } + } + return false; + } + + /** + * Determine whether the given field is a "public static final" constant. + * @param field the field to check + */ + public static boolean isPublicStaticFinal(Field field) { + int modifiers = field.getModifiers(); + return (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)); + } + + /** + * Determine whether the given method is an "equals" method. + * @see java.lang.Object#equals(Object) + */ + public static boolean isEqualsMethod(Method method) { + if (method == null || !method.getName().equals("equals")) { + return false; + } + Class<?>[] paramTypes = method.getParameterTypes(); + return (paramTypes.length == 1 && paramTypes[0] == Object.class); + } + + /** + * Determine whether the given method is a "hashCode" method. + * @see java.lang.Object#hashCode() + */ + public static boolean isHashCodeMethod(Method method) { + return (method != null && method.getName().equals("hashCode") && method.getParameterTypes().length == 0); + } + + /** + * Determine whether the given method is a "toString" method. + * @see java.lang.Object#toString() + */ + public static boolean isToStringMethod(Method method) { + return (method != null && method.getName().equals("toString") && method.getParameterTypes().length == 0); + } + + /** + * Determine whether the given method is originally declared by {@link java.lang.Object}. + */ + public static boolean isObjectMethod(Method method) { + if (method == null) { + return false; + } + try { + Object.class.getDeclaredMethod(method.getName(), method.getParameterTypes()); + return true; + } + catch (Exception ex) { + return false; + } + } + + /** + * Determine whether the given method is a CGLIB 'renamed' method, + * following the pattern "CGLIB$methodName$0". + * @param renamedMethod the method to check + * @see org.springframework.cglib.proxy.Enhancer#rename + */ + public static boolean isCglibRenamedMethod(Method renamedMethod) { + String name = renamedMethod.getName(); + return (name.startsWith(CGLIB_RENAMED_METHOD_PREFIX) && + CGLIB_RENAMED_METHOD_PATTERN.matcher(name.substring(CGLIB_RENAMED_METHOD_PREFIX.length())).matches()); + } + + /** + * Make the given field accessible, explicitly setting it accessible if + * necessary. The {@code setAccessible(true)} method is only called + * when actually necessary, to avoid unnecessary conflicts with a JVM + * SecurityManager (if active). + * @param field the field to make accessible + * @see java.lang.reflect.Field#setAccessible + */ + public static void makeAccessible(Field field) { + if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) || + Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) { + field.setAccessible(true); + } + } + + /** + * Make the given method accessible, explicitly setting it accessible if + * necessary. The {@code setAccessible(true)} method is only called + * when actually necessary, to avoid unnecessary conflicts with a JVM + * SecurityManager (if active). + * @param method the method to make accessible + * @see java.lang.reflect.Method#setAccessible + */ + public static void makeAccessible(Method method) { + if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) && + !method.isAccessible()) { + method.setAccessible(true); + } + } + + /** + * Make the given constructor accessible, explicitly setting it accessible + * if necessary. The {@code setAccessible(true)} method is only called + * when actually necessary, to avoid unnecessary conflicts with a JVM + * SecurityManager (if active). + * @param ctor the constructor to make accessible + * @see java.lang.reflect.Constructor#setAccessible + */ + public static void makeAccessible(Constructor<?> ctor) { + if ((!Modifier.isPublic(ctor.getModifiers()) || !Modifier.isPublic(ctor.getDeclaringClass().getModifiers())) && + !ctor.isAccessible()) { + ctor.setAccessible(true); + } + } + + /** + * Perform the given callback operation on all matching methods of the given + * class and superclasses. + * <p>The same named method occurring on subclass and superclass will appear + * twice, unless excluded by a {@link MethodFilter}. + * @param clazz class to start looking at + * @param mc the callback to invoke for each method + * @see #doWithMethods(Class, MethodCallback, MethodFilter) + */ + public static void doWithMethods(Class<?> clazz, MethodCallback mc) throws IllegalArgumentException { + doWithMethods(clazz, mc, null); + } + + /** + * Perform the given callback operation on all matching methods of the given + * class and superclasses (or given interface and super-interfaces). + * <p>The same named method occurring on subclass and superclass will appear + * twice, unless excluded by the specified {@link MethodFilter}. + * @param clazz class to start looking at + * @param mc the callback to invoke for each method + * @param mf the filter that determines the methods to apply the callback to + */ + public static void doWithMethods(Class<?> clazz, MethodCallback mc, MethodFilter mf) + throws IllegalArgumentException { + + // Keep backing up the inheritance hierarchy. + Method[] methods = getDeclaredMethods(clazz); + for (Method method : methods) { + if (mf != null && !mf.matches(method)) { + continue; + } + try { + mc.doWith(method); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException("Shouldn't be illegal to access method '" + method.getName() + "': " + ex); + } + } + if (clazz.getSuperclass() != null) { + doWithMethods(clazz.getSuperclass(), mc, mf); + } + else if (clazz.isInterface()) { + for (Class<?> superIfc : clazz.getInterfaces()) { + doWithMethods(superIfc, mc, mf); + } + } + } + + /** + * Get all declared methods on the leaf class and all superclasses. Leaf + * class methods are included first. + */ + public static Method[] getAllDeclaredMethods(Class<?> leafClass) throws IllegalArgumentException { + final List<Method> methods = new ArrayList<Method>(32); + doWithMethods(leafClass, new MethodCallback() { + public void doWith(Method method) { + methods.add(method); + } + }); + return methods.toArray(new Method[methods.size()]); + } + + /** + * Get the unique set of declared methods on the leaf class and all superclasses. Leaf + * class methods are included first and while traversing the superclass hierarchy any methods found + * with signatures matching a method already included are filtered out. + */ + public static Method[] getUniqueDeclaredMethods(Class<?> leafClass) throws IllegalArgumentException { + final List<Method> methods = new ArrayList<Method>(32); + doWithMethods(leafClass, new MethodCallback() { + public void doWith(Method method) { + boolean knownSignature = false; + Method methodBeingOverriddenWithCovariantReturnType = null; + for (Method existingMethod : methods) { + if (method.getName().equals(existingMethod.getName()) && + Arrays.equals(method.getParameterTypes(), existingMethod.getParameterTypes())) { + // Is this a covariant return type situation? + if (existingMethod.getReturnType() != method.getReturnType() && + existingMethod.getReturnType().isAssignableFrom(method.getReturnType())) { + methodBeingOverriddenWithCovariantReturnType = existingMethod; + } + else { + knownSignature = true; + } + break; + } + } + if (methodBeingOverriddenWithCovariantReturnType != null) { + methods.remove(methodBeingOverriddenWithCovariantReturnType); + } + if (!knownSignature && !isCglibRenamedMethod(method)) { + methods.add(method); + } + } + }); + return methods.toArray(new Method[methods.size()]); + } + + /** + * This method retrieves {@link Class#getDeclaredMethods()} from a local cache + * in order to avoid the JVM's SecurityManager check and defensive array copying. + */ + private static Method[] getDeclaredMethods(Class<?> clazz) { + Method[] result = declaredMethodsCache.get(clazz); + if (result == null) { + result = clazz.getDeclaredMethods(); + declaredMethodsCache.put(clazz, result); + } + return result; + } + + /** + * Invoke the given callback on all fields in the target class, going up the + * class hierarchy to get all declared fields. + * @param clazz the target class to analyze + * @param fc the callback to invoke for each field + */ + public static void doWithFields(Class<?> clazz, FieldCallback fc) throws IllegalArgumentException { + doWithFields(clazz, fc, null); + } + + /** + * Invoke the given callback on all fields in the target class, going up the + * class hierarchy to get all declared fields. + * @param clazz the target class to analyze + * @param fc the callback to invoke for each field + * @param ff the filter that determines the fields to apply the callback to + */ + public static void doWithFields(Class<?> clazz, FieldCallback fc, FieldFilter ff) + throws IllegalArgumentException { + + // Keep backing up the inheritance hierarchy. + Class<?> targetClass = clazz; + do { + Field[] fields = targetClass.getDeclaredFields(); + for (Field field : fields) { + // Skip static and final fields. + if (ff != null && !ff.matches(field)) { + continue; + } + try { + fc.doWith(field); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException("Shouldn't be illegal to access field '" + field.getName() + "': " + ex); + } + } + targetClass = targetClass.getSuperclass(); + } + while (targetClass != null && targetClass != Object.class); + } + + /** + * Given the source object and the destination, which must be the same class + * or a subclass, copy all fields, including inherited fields. Designed to + * work on objects with public no-arg constructors. + * @throws IllegalArgumentException if the arguments are incompatible + */ + public static void shallowCopyFieldState(final Object src, final Object dest) throws IllegalArgumentException { + if (src == null) { + throw new IllegalArgumentException("Source for field copy cannot be null"); + } + if (dest == null) { + throw new IllegalArgumentException("Destination for field copy cannot be null"); + } + if (!src.getClass().isAssignableFrom(dest.getClass())) { + throw new IllegalArgumentException("Destination class [" + dest.getClass().getName() + + "] must be same or subclass as source class [" + src.getClass().getName() + "]"); + } + doWithFields(src.getClass(), new FieldCallback() { + public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { + makeAccessible(field); + Object srcValue = field.get(src); + field.set(dest, srcValue); + } + }, COPYABLE_FIELDS); + } + + + /** + * Action to take on each method. + */ + public interface MethodCallback { + + /** + * Perform an operation using the given method. + * @param method the method to operate on + */ + void doWith(Method method) throws IllegalArgumentException, IllegalAccessException; + } + + + /** + * Callback optionally used to filter methods to be operated on by a method callback. + */ + public interface MethodFilter { + + /** + * Determine whether the given method matches. + * @param method the method to check + */ + boolean matches(Method method); + } + + + /** + * Callback interface invoked on each field in the hierarchy. + */ + public interface FieldCallback { + + /** + * Perform an operation using the given field. + * @param field the field to operate on + */ + void doWith(Field field) throws IllegalArgumentException, IllegalAccessException; + } + + + /** + * Callback optionally used to filter fields to be operated on by a field callback. + */ + public interface FieldFilter { + + /** + * Determine whether the given field matches. + * @param field the field to check + */ + boolean matches(Field field); + } + + + /** + * Pre-built FieldFilter that matches all non-static, non-final fields. + */ + public static FieldFilter COPYABLE_FIELDS = new FieldFilter() { + + public boolean matches(Field field) { + return !(Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers())); + } + }; + + + /** + * Pre-built MethodFilter that matches all non-bridge methods. + */ + public static MethodFilter NON_BRIDGED_METHODS = new MethodFilter() { + + public boolean matches(Method method) { + return !method.isBridge(); + } + }; + + + /** + * Pre-built MethodFilter that matches all non-bridge methods + * which are not declared on {@code java.lang.Object}. + */ + public static MethodFilter USER_DECLARED_METHODS = new MethodFilter() { + + public boolean matches(Method method) { + return (!method.isBridge() && method.getDeclaringClass() != Object.class); + } + }; + +} diff --git a/spring-core/src/main/java/org/springframework/util/ResourceUtils.java b/spring-core/src/main/java/org/springframework/util/ResourceUtils.java new file mode 100644 index 00000000..ce552eb0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ResourceUtils.java @@ -0,0 +1,347 @@ +/* + * Copyright 2002-2014 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.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; + +/** + * Utility methods for resolving resource locations to files in the + * file system. Mainly for internal use within the framework. + * + * <p>Consider using Spring's Resource abstraction in the core package + * for handling all kinds of file resources in a uniform manner. + * {@link org.springframework.core.io.ResourceLoader}'s {@code getResource()} + * method can resolve any location to a {@link org.springframework.core.io.Resource} + * object, which in turn allows one to obtain a {@code java.io.File} in the + * file system through its {@code getFile()} method. + * + * <p>The main reason for these utility methods for resource location handling + * is to support {@link Log4jConfigurer}, which must be able to resolve + * resource locations <i>before the logging system has been initialized</i>. + * Spring's {@code Resource} abstraction in the core package, on the other hand, + * already expects the logging system to be available. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see org.springframework.core.io.Resource + * @see org.springframework.core.io.ClassPathResource + * @see org.springframework.core.io.FileSystemResource + * @see org.springframework.core.io.UrlResource + * @see org.springframework.core.io.ResourceLoader + */ +public abstract class ResourceUtils { + + /** Pseudo URL prefix for loading from the class path: "classpath:" */ + public static final String CLASSPATH_URL_PREFIX = "classpath:"; + + /** URL prefix for loading from the file system: "file:" */ + public static final String FILE_URL_PREFIX = "file:"; + + /** URL protocol for a file in the file system: "file" */ + public static final String URL_PROTOCOL_FILE = "file"; + + /** URL protocol for an entry from a jar file: "jar" */ + public static final String URL_PROTOCOL_JAR = "jar"; + + /** URL protocol for an entry from a zip file: "zip" */ + public static final String URL_PROTOCOL_ZIP = "zip"; + + /** URL protocol for an entry from a WebSphere jar file: "wsjar" */ + public static final String URL_PROTOCOL_WSJAR = "wsjar"; + + /** URL protocol for an entry from a JBoss jar file: "vfszip" */ + public static final String URL_PROTOCOL_VFSZIP = "vfszip"; + + /** URL protocol for a JBoss file system resource: "vfsfile" */ + public static final String URL_PROTOCOL_VFSFILE = "vfsfile"; + + /** URL protocol for a general JBoss VFS resource: "vfs" */ + public static final String URL_PROTOCOL_VFS = "vfs"; + + /** URL protocol for an entry from an OC4J jar file: "code-source" */ + public static final String URL_PROTOCOL_CODE_SOURCE = "code-source"; + + /** Separator between JAR URL and file path within the JAR */ + public static final String JAR_URL_SEPARATOR = "!/"; + + + /** + * Return whether the given resource location is a URL: + * either a special "classpath" pseudo URL or a standard URL. + * @param resourceLocation the location String to check + * @return whether the location qualifies as a URL + * @see #CLASSPATH_URL_PREFIX + * @see java.net.URL + */ + public static boolean isUrl(String resourceLocation) { + if (resourceLocation == null) { + return false; + } + if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { + return true; + } + try { + new URL(resourceLocation); + return true; + } + catch (MalformedURLException ex) { + return false; + } + } + + /** + * Resolve the given resource location to a {@code java.net.URL}. + * <p>Does not check whether the URL actually exists; simply returns + * the URL that the given location would correspond to. + * @param resourceLocation the resource location to resolve: either a + * "classpath:" pseudo URL, a "file:" URL, or a plain file path + * @return a corresponding URL object + * @throws FileNotFoundException if the resource cannot be resolved to a URL + */ + public static URL getURL(String resourceLocation) throws FileNotFoundException { + Assert.notNull(resourceLocation, "Resource location must not be null"); + if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { + String path = resourceLocation.substring(CLASSPATH_URL_PREFIX.length()); + ClassLoader cl = ClassUtils.getDefaultClassLoader(); + URL url = (cl != null ? cl.getResource(path) : ClassLoader.getSystemResource(path)); + if (url == null) { + String description = "class path resource [" + path + "]"; + throw new FileNotFoundException( + description + " cannot be resolved to URL because it does not exist"); + } + return url; + } + try { + // try URL + return new URL(resourceLocation); + } + catch (MalformedURLException ex) { + // no URL -> treat as file path + try { + return new File(resourceLocation).toURI().toURL(); + } + catch (MalformedURLException ex2) { + throw new FileNotFoundException("Resource location [" + resourceLocation + + "] is neither a URL not a well-formed file path"); + } + } + } + + /** + * Resolve the given resource location to a {@code java.io.File}, + * i.e. to a file in the file system. + * <p>Does not check whether the file actually exists; simply returns + * the File that the given location would correspond to. + * @param resourceLocation the resource location to resolve: either a + * "classpath:" pseudo URL, a "file:" URL, or a plain file path + * @return a corresponding File object + * @throws FileNotFoundException if the resource cannot be resolved to + * a file in the file system + */ + public static File getFile(String resourceLocation) throws FileNotFoundException { + Assert.notNull(resourceLocation, "Resource location must not be null"); + if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { + String path = resourceLocation.substring(CLASSPATH_URL_PREFIX.length()); + String description = "class path resource [" + path + "]"; + ClassLoader cl = ClassUtils.getDefaultClassLoader(); + URL url = (cl != null ? cl.getResource(path) : ClassLoader.getSystemResource(path)); + if (url == null) { + throw new FileNotFoundException( + description + " cannot be resolved to absolute file path " + + "because it does not reside in the file system"); + } + return getFile(url, description); + } + try { + // try URL + return getFile(new URL(resourceLocation)); + } + catch (MalformedURLException ex) { + // no URL -> treat as file path + return new File(resourceLocation); + } + } + + /** + * Resolve the given resource URL to a {@code java.io.File}, + * i.e. to a file in the file system. + * @param resourceUrl the resource URL to resolve + * @return a corresponding File object + * @throws FileNotFoundException if the URL cannot be resolved to + * a file in the file system + */ + public static File getFile(URL resourceUrl) throws FileNotFoundException { + return getFile(resourceUrl, "URL"); + } + + /** + * Resolve the given resource URL to a {@code java.io.File}, + * i.e. to a file in the file system. + * @param resourceUrl the resource URL to resolve + * @param description a description of the original resource that + * the URL was created for (for example, a class path location) + * @return a corresponding File object + * @throws FileNotFoundException if the URL cannot be resolved to + * a file in the file system + */ + public static File getFile(URL resourceUrl, String description) throws FileNotFoundException { + Assert.notNull(resourceUrl, "Resource URL must not be null"); + if (!URL_PROTOCOL_FILE.equals(resourceUrl.getProtocol())) { + throw new FileNotFoundException( + description + " cannot be resolved to absolute file path " + + "because it does not reside in the file system: " + resourceUrl); + } + try { + return new File(toURI(resourceUrl).getSchemeSpecificPart()); + } + catch (URISyntaxException ex) { + // Fallback for URLs that are not valid URIs (should hardly ever happen). + return new File(resourceUrl.getFile()); + } + } + + /** + * Resolve the given resource URI to a {@code java.io.File}, + * i.e. to a file in the file system. + * @param resourceUri the resource URI to resolve + * @return a corresponding File object + * @throws FileNotFoundException if the URL cannot be resolved to + * a file in the file system + */ + public static File getFile(URI resourceUri) throws FileNotFoundException { + return getFile(resourceUri, "URI"); + } + + /** + * Resolve the given resource URI to a {@code java.io.File}, + * i.e. to a file in the file system. + * @param resourceUri the resource URI to resolve + * @param description a description of the original resource that + * the URI was created for (for example, a class path location) + * @return a corresponding File object + * @throws FileNotFoundException if the URL cannot be resolved to + * a file in the file system + */ + public static File getFile(URI resourceUri, String description) throws FileNotFoundException { + Assert.notNull(resourceUri, "Resource URI must not be null"); + if (!URL_PROTOCOL_FILE.equals(resourceUri.getScheme())) { + throw new FileNotFoundException( + description + " cannot be resolved to absolute file path " + + "because it does not reside in the file system: " + resourceUri); + } + return new File(resourceUri.getSchemeSpecificPart()); + } + + /** + * Determine whether the given URL points to a resource in the file system, + * that is, has protocol "file", "vfsfile" or "vfs". + * @param url the URL to check + * @return whether the URL has been identified as a file system URL + */ + public static boolean isFileURL(URL url) { + String protocol = url.getProtocol(); + return (URL_PROTOCOL_FILE.equals(protocol) || URL_PROTOCOL_VFSFILE.equals(protocol) || + URL_PROTOCOL_VFS.equals(protocol)); + } + + /** + * Determine whether the given URL points to a resource in a jar file, + * that is, has protocol "jar", "zip", "vfszip", "wsjar" or "code-source". + * <p>"zip" and "wsjar" are used by WebLogic Server and WebSphere, respectively, + * but can be treated like jar files. The same applies to "code-source" URLs on + * OC4J, provided that the path contains a jar separator. + * @param url the URL to check + * @return whether the URL has been identified as a JAR URL + */ + public static boolean isJarURL(URL url) { + String protocol = url.getProtocol(); + return (URL_PROTOCOL_JAR.equals(protocol) || URL_PROTOCOL_ZIP.equals(protocol) || + URL_PROTOCOL_VFSZIP.equals(protocol) || URL_PROTOCOL_WSJAR.equals(protocol) || + (URL_PROTOCOL_CODE_SOURCE.equals(protocol) && url.getPath().contains(JAR_URL_SEPARATOR))); + } + + /** + * Extract the URL for the actual jar file from the given URL + * (which may point to a resource in a jar file or to a jar file itself). + * @param jarUrl the original URL + * @return the URL for the actual jar file + * @throws MalformedURLException if no valid jar file URL could be extracted + */ + public static URL extractJarFileURL(URL jarUrl) throws MalformedURLException { + String urlFile = jarUrl.getFile(); + int separatorIndex = urlFile.indexOf(JAR_URL_SEPARATOR); + if (separatorIndex != -1) { + String jarFile = urlFile.substring(0, separatorIndex); + try { + return new URL(jarFile); + } + catch (MalformedURLException ex) { + // Probably no protocol in original jar URL, like "jar:C:/mypath/myjar.jar". + // This usually indicates that the jar file resides in the file system. + if (!jarFile.startsWith("/")) { + jarFile = "/" + jarFile; + } + return new URL(FILE_URL_PREFIX + jarFile); + } + } + else { + return jarUrl; + } + } + + /** + * Create a URI instance for the given URL, + * replacing spaces with "%20" URI encoding first. + * <p>Furthermore, this method works on JDK 1.4 as well, + * in contrast to the {@code URL.toURI()} method. + * @param url the URL to convert into a URI instance + * @return the URI instance + * @throws URISyntaxException if the URL wasn't a valid URI + * @see java.net.URL#toURI() + */ + public static URI toURI(URL url) throws URISyntaxException { + return toURI(url.toString()); + } + + /** + * Create a URI instance for the given location String, + * replacing spaces with "%20" URI encoding first. + * @param location the location String to convert into a URI instance + * @return the URI instance + * @throws URISyntaxException if the location wasn't a valid URI + */ + public static URI toURI(String location) throws URISyntaxException { + return new URI(StringUtils.replace(location, " ", "%20")); + } + + /** + * Set the {@link URLConnection#setUseCaches "useCaches"} flag on the + * given connection, preferring {@code false} but leaving the + * flag at {@code true} for JNLP based resources. + * @param con the URLConnection to set the flag on + */ + public static void useCachesIfNecessary(URLConnection con) { + con.setUseCaches(con.getClass().getSimpleName().startsWith("JNLP")); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/SerializationUtils.java b/spring-core/src/main/java/org/springframework/util/SerializationUtils.java new file mode 100644 index 00000000..f85b1c72 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/SerializationUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2014 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.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +/** + * Static utilities for serialization and deserialization. + * + * @author Dave Syer + * @since 3.0.5 + */ +public abstract class SerializationUtils { + + /** + * Serialize the given object to a byte array. + * @param object the object to serialize + * @return an array of bytes representing the object in a portable fashion + */ + public static byte[] serialize(Object object) { + if (object == null) { + return null; + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + try { + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(object); + oos.flush(); + } + catch (IOException ex) { + throw new IllegalArgumentException("Failed to serialize object of type: " + object.getClass(), ex); + } + return baos.toByteArray(); + } + + /** + * Deserialize the byte array into an object. + * @param bytes a serialized object + * @return the result of deserializing the bytes + */ + public static Object deserialize(byte[] bytes) { + if (bytes == null) { + return null; + } + try { + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); + return ois.readObject(); + } + catch (IOException ex) { + throw new IllegalArgumentException("Failed to deserialize object", ex); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Failed to deserialize object type", ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/StopWatch.java b/spring-core/src/main/java/org/springframework/util/StopWatch.java new file mode 100644 index 00000000..0c974991 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/StopWatch.java @@ -0,0 +1,309 @@ +/* + * Copyright 2002-2014 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.util; + +import java.text.NumberFormat; +import java.util.LinkedList; +import java.util.List; + +/** + * Simple stop watch, allowing for timing of a number of tasks, + * exposing total running time and running time for each named task. + * + * <p>Conceals use of {@code System.currentTimeMillis()}, improving the + * readability of application code and reducing the likelihood of calculation errors. + * + * <p>Note that this object is not designed to be thread-safe and does not + * use synchronization. + * + * <p>This class is normally used to verify performance during proof-of-concepts + * and in development, rather than as part of production applications. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @since May 2, 2001 + */ +public class StopWatch { + + /** + * Identifier of this stop watch. + * Handy when we have output from multiple stop watches + * and need to distinguish between them in log or console output. + */ + private final String id; + + private boolean keepTaskList = true; + + private final List<TaskInfo> taskList = new LinkedList<TaskInfo>(); + + /** Start time of the current task */ + private long startTimeMillis; + + /** Is the stop watch currently running? */ + private boolean running; + + /** Name of the current task */ + private String currentTaskName; + + private TaskInfo lastTaskInfo; + + private int taskCount; + + /** Total running time */ + private long totalTimeMillis; + + + /** + * Construct a new stop watch. Does not start any task. + */ + public StopWatch() { + this.id = ""; + } + + /** + * Construct a new stop watch with the given id. + * Does not start any task. + * @param id identifier for this stop watch. + * Handy when we have output from multiple stop watches + * and need to distinguish between them. + */ + public StopWatch(String id) { + this.id = id; + } + + + /** + * Determine whether the TaskInfo array is built over time. Set this to + * "false" when using a StopWatch for millions of intervals, or the task + * info structure will consume excessive memory. Default is "true". + */ + public void setKeepTaskList(boolean keepTaskList) { + this.keepTaskList = keepTaskList; + } + + + /** + * Start an unnamed task. The results are undefined if {@link #stop()} + * or timing methods are called without invoking this method. + * @see #stop() + */ + public void start() throws IllegalStateException { + start(""); + } + + /** + * Start a named task. The results are undefined if {@link #stop()} + * or timing methods are called without invoking this method. + * @param taskName the name of the task to start + * @see #stop() + */ + public void start(String taskName) throws IllegalStateException { + if (this.running) { + throw new IllegalStateException("Can't start StopWatch: it's already running"); + } + this.startTimeMillis = System.currentTimeMillis(); + this.running = true; + this.currentTaskName = taskName; + } + + /** + * Stop the current task. The results are undefined if timing + * methods are called without invoking at least one pair + * {@link #start()} / {@link #stop()} methods. + * @see #start() + */ + public void stop() throws IllegalStateException { + if (!this.running) { + throw new IllegalStateException("Can't stop StopWatch: it's not running"); + } + long lastTime = System.currentTimeMillis() - this.startTimeMillis; + this.totalTimeMillis += lastTime; + this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime); + if (this.keepTaskList) { + this.taskList.add(lastTaskInfo); + } + ++this.taskCount; + this.running = false; + this.currentTaskName = null; + } + + /** + * Return whether the stop watch is currently running. + */ + public boolean isRunning() { + return this.running; + } + + + /** + * Return the time taken by the last task. + */ + public long getLastTaskTimeMillis() throws IllegalStateException { + if (this.lastTaskInfo == null) { + throw new IllegalStateException("No tasks run: can't get last task interval"); + } + return this.lastTaskInfo.getTimeMillis(); + } + + /** + * Return the name of the last task. + */ + public String getLastTaskName() throws IllegalStateException { + if (this.lastTaskInfo == null) { + throw new IllegalStateException("No tasks run: can't get last task name"); + } + return this.lastTaskInfo.getTaskName(); + } + + /** + * Return the last task as a TaskInfo object. + */ + public TaskInfo getLastTaskInfo() throws IllegalStateException { + if (this.lastTaskInfo == null) { + throw new IllegalStateException("No tasks run: can't get last task info"); + } + return this.lastTaskInfo; + } + + + /** + * Return the total time in milliseconds for all tasks. + */ + public long getTotalTimeMillis() { + return this.totalTimeMillis; + } + + /** + * Return the total time in seconds for all tasks. + */ + public double getTotalTimeSeconds() { + return this.totalTimeMillis / 1000.0; + } + + /** + * Return the number of tasks timed. + */ + public int getTaskCount() { + return this.taskCount; + } + + /** + * Return an array of the data for tasks performed. + */ + public TaskInfo[] getTaskInfo() { + if (!this.keepTaskList) { + throw new UnsupportedOperationException("Task info is not being kept!"); + } + return this.taskList.toArray(new TaskInfo[this.taskList.size()]); + } + + + /** + * Return a short description of the total running time. + */ + public String shortSummary() { + return "StopWatch '" + this.id + "': running time (millis) = " + getTotalTimeMillis(); + } + + /** + * Return a string with a table describing all tasks performed. + * For custom reporting, call getTaskInfo() and use the task info directly. + */ + public String prettyPrint() { + StringBuilder sb = new StringBuilder(shortSummary()); + sb.append('\n'); + if (!this.keepTaskList) { + sb.append("No task info kept"); + } + else { + sb.append("-----------------------------------------\n"); + sb.append("ms % Task name\n"); + sb.append("-----------------------------------------\n"); + NumberFormat nf = NumberFormat.getNumberInstance(); + nf.setMinimumIntegerDigits(5); + nf.setGroupingUsed(false); + NumberFormat pf = NumberFormat.getPercentInstance(); + pf.setMinimumIntegerDigits(3); + pf.setGroupingUsed(false); + for (TaskInfo task : getTaskInfo()) { + sb.append(nf.format(task.getTimeMillis())).append(" "); + sb.append(pf.format(task.getTimeSeconds() / getTotalTimeSeconds())).append(" "); + sb.append(task.getTaskName()).append("\n"); + } + } + return sb.toString(); + } + + /** + * Return an informative string describing all tasks performed + * For custom reporting, call {@code getTaskInfo()} and use the task info directly. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(shortSummary()); + if (this.keepTaskList) { + for (TaskInfo task : getTaskInfo()) { + sb.append("; [").append(task.getTaskName()).append("] took ").append(task.getTimeMillis()); + long percent = Math.round((100.0 * task.getTimeSeconds()) / getTotalTimeSeconds()); + sb.append(" = ").append(percent).append("%"); + } + } + else { + sb.append("; no task info kept"); + } + return sb.toString(); + } + + + /** + * Inner class to hold data about one task executed within the stop watch. + */ + public static final class TaskInfo { + + private final String taskName; + + private final long timeMillis; + + TaskInfo(String taskName, long timeMillis) { + this.taskName = taskName; + this.timeMillis = timeMillis; + } + + /** + * Return the name of this task. + */ + public String getTaskName() { + return this.taskName; + } + + /** + * Return the time in milliseconds this task took. + */ + public long getTimeMillis() { + return this.timeMillis; + } + + /** + * Return the time in seconds this task took. + */ + public double getTimeSeconds() { + return this.timeMillis / 1000.0; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/StreamUtils.java b/spring-core/src/main/java/org/springframework/util/StreamUtils.java new file mode 100644 index 00000000..cc3107d8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/StreamUtils.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2013 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.util; + +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; + + +/** + * Simple utility methods for dealing with streams. The copy methods of this class are + * similar to those defined in {@link FileCopyUtils} except that all affected streams are + * left open when done. All copy methods use a block size of 4096 bytes. + * + * <p>Mainly for use within the framework, but also useful for application code. + * + * @author Juergen Hoeller + * @author Phillip Webb + * @since 3.2.2 + * @see FileCopyUtils + */ +public abstract class StreamUtils { + + public static final int BUFFER_SIZE = 4096; + + + /** + * Copy the contents of the given InputStream into a new byte array. + * Leaves the stream open when done. + * @param in the stream to copy from + * @return the new byte array that has been copied to + * @throws IOException in case of I/O errors + */ + public static byte[] copyToByteArray(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE); + copy(in, out); + return out.toByteArray(); + } + + /** + * Copy the contents of the given InputStream into a String. + * Leaves the stream open when done. + * @param in the InputStream to copy from + * @return the String that has been copied to + * @throws IOException in case of I/O errors + */ + public static String copyToString(InputStream in, Charset charset) throws IOException { + Assert.notNull(in, "No InputStream specified"); + StringBuilder out = new StringBuilder(); + InputStreamReader reader = new InputStreamReader(in, charset); + char[] buffer = new char[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = reader.read(buffer)) != -1) { + out.append(buffer, 0, bytesRead); + } + return out.toString(); + } + + /** + * Copy the contents of the given byte array to the given OutputStream. + * Leaves the stream open when done. + * @param in the byte array to copy from + * @param out the OutputStream to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(byte[] in, OutputStream out) throws IOException { + Assert.notNull(in, "No input byte array specified"); + Assert.notNull(out, "No OutputStream specified"); + out.write(in); + } + + /** + * Copy the contents of the given String to the given output OutputStream. + * Leaves the stream open when done. + * @param in the String to copy from + * @param charset the Charset + * @param out the OutputStream to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(String in, Charset charset, OutputStream out) throws IOException { + Assert.notNull(in, "No input String specified"); + Assert.notNull(charset, "No charset specified"); + Assert.notNull(out, "No OutputStream specified"); + Writer writer = new OutputStreamWriter(out, charset); + writer.write(in); + writer.flush(); + } + + /** + * Copy the contents of the given InputStream to the given OutputStream. + * Leaves both streams open when done. + * @param in the InputStream to copy from + * @param out the OutputStream to copy to + * @return the number of bytes copied + * @throws IOException in case of I/O errors + */ + public static int copy(InputStream in, OutputStream out) throws IOException { + Assert.notNull(in, "No InputStream specified"); + Assert.notNull(out, "No OutputStream specified"); + int byteCount = 0; + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + byteCount += bytesRead; + } + out.flush(); + return byteCount; + } + + /** + * Returns a variant of the given {@link InputStream} where calling + * {@link InputStream#close() close()} has no effect. + * @param in the InputStream to decorate + * @return a version of the InputStream that ignores calls to close + */ + public static InputStream nonClosing(InputStream in) { + Assert.notNull(in, "No InputStream specified"); + return new NonClosingInputStream(in); + } + + /** + * Returns a variant of the given {@link OutputStream} where calling + * {@link OutputStream#close() close()} has no effect. + * @param out the OutputStream to decorate + * @return a version of the OutputStream that ignores calls to close + */ + public static OutputStream nonClosing(OutputStream out) { + Assert.notNull(out, "No OutputStream specified"); + return new NonClosingOutputStream(out); + } + + + private static class NonClosingInputStream extends FilterInputStream { + + public NonClosingInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + } + } + + + private static class NonClosingOutputStream extends FilterOutputStream { + + public NonClosingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(byte[] b, int off, int let) throws IOException { + // It is critical that we override this method for performance + out.write(b, off, let); + } + + @Override + public void close() throws IOException { + } + } +} diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java new file mode 100644 index 00000000..0ad561da --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -0,0 +1,1162 @@ +/* + * Copyright 2002-2014 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.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.TreeSet; + +/** + * Miscellaneous {@link String} utility methods. + * + * <p>Mainly for internal use within the framework; consider + * <a href="http://jakarta.apache.org/commons/lang/">Jakarta's Commons Lang</a> + * for a more comprehensive suite of String utilities. + * + * <p>This class delivers some simple functionality that should really + * be provided by the core Java {@code String} and {@link StringBuilder} + * classes, such as the ability to {@link #replace} all occurrences of a given + * substring in a target string. It also provides easy-to-use methods to convert + * between delimited strings, such as CSV strings, and collections and arrays. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Keith Donald + * @author Rob Harrop + * @author Rick Evans + * @author Arjen Poutsma + * @since 16 April 2001 + */ +public abstract class StringUtils { + + private static final String FOLDER_SEPARATOR = "/"; + + private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; + + private static final String TOP_PATH = ".."; + + private static final String CURRENT_PATH = "."; + + private static final char EXTENSION_SEPARATOR = '.'; + + + //--------------------------------------------------------------------- + // General convenience methods for working with Strings + //--------------------------------------------------------------------- + + /** + * Check whether the given String is empty. + * <p>This method accepts any Object as an argument, comparing it to + * {@code null} and the empty String. As a consequence, this method + * will never return {@code true} for a non-null non-String object. + * <p>The Object signature is useful for general attribute handling code + * that commonly deals with Strings but generally has to iterate over + * Objects since attributes may e.g. be primitive value objects as well. + * @param str the candidate String + * @since 3.2.1 + */ + public static boolean isEmpty(Object str) { + return (str == null || "".equals(str)); + } + + /** + * Check that the given CharSequence is neither {@code null} nor of length 0. + * Note: Will return {@code true} for a CharSequence that purely consists of whitespace. + * <p><pre class="code"> + * StringUtils.hasLength(null) = false + * StringUtils.hasLength("") = false + * StringUtils.hasLength(" ") = true + * StringUtils.hasLength("Hello") = true + * </pre> + * @param str the CharSequence to check (may be {@code null}) + * @return {@code true} if the CharSequence is not null and has length + * @see #hasText(String) + */ + public static boolean hasLength(CharSequence str) { + return (str != null && str.length() > 0); + } + + /** + * Check that the given String is neither {@code null} nor of length 0. + * Note: Will return {@code true} for a String that purely consists of whitespace. + * @param str the String to check (may be {@code null}) + * @return {@code true} if the String is not null and has length + * @see #hasLength(CharSequence) + */ + public static boolean hasLength(String str) { + return hasLength((CharSequence) str); + } + + /** + * Check whether the given CharSequence has actual text. + * More specifically, returns {@code true} if the string not {@code null}, + * its length is greater than 0, and it contains at least one non-whitespace character. + * <p><pre class="code"> + * StringUtils.hasText(null) = false + * StringUtils.hasText("") = false + * StringUtils.hasText(" ") = false + * StringUtils.hasText("12345") = true + * StringUtils.hasText(" 12345 ") = true + * </pre> + * @param str the CharSequence to check (may be {@code null}) + * @return {@code true} if the CharSequence is not {@code null}, + * its length is greater than 0, and it does not contain whitespace only + * @see Character#isWhitespace + */ + public static boolean hasText(CharSequence str) { + if (!hasLength(str)) { + return false; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given String has actual text. + * More specifically, returns {@code true} if the string not {@code null}, + * its length is greater than 0, and it contains at least one non-whitespace character. + * @param str the String to check (may be {@code null}) + * @return {@code true} if the String is not {@code null}, its length is + * greater than 0, and it does not contain whitespace only + * @see #hasText(CharSequence) + */ + public static boolean hasText(String str) { + return hasText((CharSequence) str); + } + + /** + * Check whether the given CharSequence contains any whitespace characters. + * @param str the CharSequence to check (may be {@code null}) + * @return {@code true} if the CharSequence is not empty and + * contains at least 1 whitespace character + * @see Character#isWhitespace + */ + public static boolean containsWhitespace(CharSequence str) { + if (!hasLength(str)) { + return false; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given String contains any whitespace characters. + * @param str the String to check (may be {@code null}) + * @return {@code true} if the String is not empty and + * contains at least 1 whitespace character + * @see #containsWhitespace(CharSequence) + */ + public static boolean containsWhitespace(String str) { + return containsWhitespace((CharSequence) str); + } + + /** + * Trim leading and trailing whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Trim <i>all</i> whitespace from the given String: + * leading, trailing, and in between characters. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimAllWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + int index = 0; + while (sb.length() > index) { + if (Character.isWhitespace(sb.charAt(index))) { + sb.deleteCharAt(index); + } + else { + index++; + } + } + return sb.toString(); + } + + /** + * Trim leading whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimLeadingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * Trim trailing whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimTrailingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Trim all occurrences of the supplied leading character from the given String. + * @param str the String to check + * @param leadingCharacter the leading character to be trimmed + * @return the trimmed String + */ + public static String trimLeadingCharacter(String str, char leadingCharacter) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && sb.charAt(0) == leadingCharacter) { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * Trim all occurrences of the supplied trailing character from the given String. + * @param str the String to check + * @param trailingCharacter the trailing character to be trimmed + * @return the trimmed String + */ + public static String trimTrailingCharacter(String str, char trailingCharacter) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && sb.charAt(sb.length() - 1) == trailingCharacter) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + + /** + * Test if the given String starts with the specified prefix, + * ignoring upper/lower case. + * @param str the String to check + * @param prefix the prefix to look for + * @see java.lang.String#startsWith + */ + public static boolean startsWithIgnoreCase(String str, String prefix) { + if (str == null || prefix == null) { + return false; + } + if (str.startsWith(prefix)) { + return true; + } + if (str.length() < prefix.length()) { + return false; + } + String lcStr = str.substring(0, prefix.length()).toLowerCase(); + String lcPrefix = prefix.toLowerCase(); + return lcStr.equals(lcPrefix); + } + + /** + * Test if the given String ends with the specified suffix, + * ignoring upper/lower case. + * @param str the String to check + * @param suffix the suffix to look for + * @see java.lang.String#endsWith + */ + public static boolean endsWithIgnoreCase(String str, String suffix) { + if (str == null || suffix == null) { + return false; + } + if (str.endsWith(suffix)) { + return true; + } + if (str.length() < suffix.length()) { + return false; + } + + String lcStr = str.substring(str.length() - suffix.length()).toLowerCase(); + String lcSuffix = suffix.toLowerCase(); + return lcStr.equals(lcSuffix); + } + + /** + * Test whether the given string matches the given substring + * at the given index. + * @param str the original string (or StringBuilder) + * @param index the index in the original string to start matching against + * @param substring the substring to match at the given index + */ + public static boolean substringMatch(CharSequence str, int index, CharSequence substring) { + for (int j = 0; j < substring.length(); j++) { + int i = index + j; + if (i >= str.length() || str.charAt(i) != substring.charAt(j)) { + return false; + } + } + return true; + } + + /** + * Count the occurrences of the substring in string s. + * @param str string to search in. Return 0 if this is null. + * @param sub string to search for. Return 0 if this is null. + */ + public static int countOccurrencesOf(String str, String sub) { + if (str == null || sub == null || str.length() == 0 || sub.length() == 0) { + return 0; + } + int count = 0; + int pos = 0; + int idx; + while ((idx = str.indexOf(sub, pos)) != -1) { + ++count; + pos = idx + sub.length(); + } + return count; + } + + /** + * Replace all occurrences of a substring within a string with + * another string. + * @param inString String to examine + * @param oldPattern String to replace + * @param newPattern String to insert + * @return a String with the replacements + */ + public static String replace(String inString, String oldPattern, String newPattern) { + if (!hasLength(inString) || !hasLength(oldPattern) || newPattern == null) { + return inString; + } + StringBuilder sb = new StringBuilder(); + int pos = 0; // our position in the old string + int index = inString.indexOf(oldPattern); + // the index of an occurrence we've found, or -1 + int patLen = oldPattern.length(); + while (index >= 0) { + sb.append(inString.substring(pos, index)); + sb.append(newPattern); + pos = index + patLen; + index = inString.indexOf(oldPattern, pos); + } + sb.append(inString.substring(pos)); + // remember to append any characters to the right of a match + return sb.toString(); + } + + /** + * Delete all occurrences of the given substring. + * @param inString the original String + * @param pattern the pattern to delete all occurrences of + * @return the resulting String + */ + public static String delete(String inString, String pattern) { + return replace(inString, pattern, ""); + } + + /** + * Delete any character in a given String. + * @param inString the original String + * @param charsToDelete a set of characters to delete. + * E.g. "az\n" will delete 'a's, 'z's and new lines. + * @return the resulting String + */ + public static String deleteAny(String inString, String charsToDelete) { + if (!hasLength(inString) || !hasLength(charsToDelete)) { + return inString; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < inString.length(); i++) { + char c = inString.charAt(i); + if (charsToDelete.indexOf(c) == -1) { + sb.append(c); + } + } + return sb.toString(); + } + + + //--------------------------------------------------------------------- + // Convenience methods for working with formatted Strings + //--------------------------------------------------------------------- + + /** + * Quote the given String with single quotes. + * @param str the input String (e.g. "myString") + * @return the quoted String (e.g. "'myString'"), + * or {@code null} if the input was {@code null} + */ + public static String quote(String str) { + return (str != null ? "'" + str + "'" : null); + } + + /** + * Turn the given Object into a String with single quotes + * if it is a String; keeping the Object as-is else. + * @param obj the input Object (e.g. "myString") + * @return the quoted String (e.g. "'myString'"), + * or the input object as-is if not a String + */ + public static Object quoteIfString(Object obj) { + return (obj instanceof String ? quote((String) obj) : obj); + } + + /** + * Unqualify a string qualified by a '.' dot character. For example, + * "this.name.is.qualified", returns "qualified". + * @param qualifiedName the qualified name + */ + public static String unqualify(String qualifiedName) { + return unqualify(qualifiedName, '.'); + } + + /** + * Unqualify a string qualified by a separator character. For example, + * "this:name:is:qualified" returns "qualified" if using a ':' separator. + * @param qualifiedName the qualified name + * @param separator the separator + */ + public static String unqualify(String qualifiedName, char separator) { + return qualifiedName.substring(qualifiedName.lastIndexOf(separator) + 1); + } + + /** + * Capitalize a {@code String}, changing the first letter to + * upper case as per {@link Character#toUpperCase(char)}. + * No other letters are changed. + * @param str the String to capitalize, may be {@code null} + * @return the capitalized String, {@code null} if null + */ + public static String capitalize(String str) { + return changeFirstCharacterCase(str, true); + } + + /** + * Uncapitalize a {@code String}, changing the first letter to + * lower case as per {@link Character#toLowerCase(char)}. + * No other letters are changed. + * @param str the String to uncapitalize, may be {@code null} + * @return the uncapitalized String, {@code null} if null + */ + public static String uncapitalize(String str) { + return changeFirstCharacterCase(str, false); + } + + private static String changeFirstCharacterCase(String str, boolean capitalize) { + if (str == null || str.length() == 0) { + return str; + } + StringBuilder sb = new StringBuilder(str.length()); + if (capitalize) { + sb.append(Character.toUpperCase(str.charAt(0))); + } + else { + sb.append(Character.toLowerCase(str.charAt(0))); + } + sb.append(str.substring(1)); + return sb.toString(); + } + + /** + * Extract the filename from the given path, + * e.g. "mypath/myfile.txt" -> "myfile.txt". + * @param path the file path (may be {@code null}) + * @return the extracted filename, or {@code null} if none + */ + public static String getFilename(String path) { + if (path == null) { + return null; + } + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path); + } + + /** + * Extract the filename extension from the given path, + * e.g. "mypath/myfile.txt" -> "txt". + * @param path the file path (may be {@code null}) + * @return the extracted filename extension, or {@code null} if none + */ + public static String getFilenameExtension(String path) { + if (path == null) { + return null; + } + int extIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + if (extIndex == -1) { + return null; + } + int folderIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (folderIndex > extIndex) { + return null; + } + return path.substring(extIndex + 1); + } + + /** + * Strip the filename extension from the given path, + * e.g. "mypath/myfile.txt" -> "mypath/myfile". + * @param path the file path (may be {@code null}) + * @return the path with stripped filename extension, + * or {@code null} if none + */ + public static String stripFilenameExtension(String path) { + if (path == null) { + return null; + } + int extIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + if (extIndex == -1) { + return path; + } + int folderIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (folderIndex > extIndex) { + return path; + } + return path.substring(0, extIndex); + } + + /** + * Apply the given relative path to the given path, + * assuming standard Java folder separation (i.e. "/" separators). + * @param path the path to start from (usually a full file path) + * @param relativePath the relative path to apply + * (relative to the full file path above) + * @return the full file path that results from applying the relative path + */ + public static String applyRelativePath(String path, String relativePath) { + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (separatorIndex != -1) { + String newPath = path.substring(0, separatorIndex); + if (!relativePath.startsWith(FOLDER_SEPARATOR)) { + newPath += FOLDER_SEPARATOR; + } + return newPath + relativePath; + } + else { + return relativePath; + } + } + + /** + * Normalize the path by suppressing sequences like "path/.." and + * inner simple dots. + * <p>The result is convenient for path comparison. For other uses, + * notice that Windows separators ("\") are replaced by simple slashes. + * @param path the original path + * @return the normalized path + */ + public static String cleanPath(String path) { + if (path == null) { + return null; + } + String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR); + + // Strip prefix from path to analyze, to not treat it as part of the + // first path element. This is necessary to correctly parse paths like + // "file:core/../core/io/Resource.class", where the ".." should just + // strip the first "core" directory while keeping the "file:" prefix. + int prefixIndex = pathToUse.indexOf(":"); + String prefix = ""; + if (prefixIndex != -1) { + prefix = pathToUse.substring(0, prefixIndex + 1); + if (prefix.contains("/")) { + prefix = ""; + } + else { + pathToUse = pathToUse.substring(prefixIndex + 1); + } + } + if (pathToUse.startsWith(FOLDER_SEPARATOR)) { + prefix = prefix + FOLDER_SEPARATOR; + pathToUse = pathToUse.substring(1); + } + + String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR); + List<String> pathElements = new LinkedList<String>(); + int tops = 0; + + for (int i = pathArray.length - 1; i >= 0; i--) { + String element = pathArray[i]; + if (CURRENT_PATH.equals(element)) { + // Points to current directory - drop it. + } + else if (TOP_PATH.equals(element)) { + // Registering top path found. + tops++; + } + else { + if (tops > 0) { + // Merging path element with element corresponding to top path. + tops--; + } + else { + // Normal path element found. + pathElements.add(0, element); + } + } + } + + // Remaining top paths need to be retained. + for (int i = 0; i < tops; i++) { + pathElements.add(0, TOP_PATH); + } + + return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR); + } + + /** + * Compare two paths after normalization of them. + * @param path1 first path for comparison + * @param path2 second path for comparison + * @return whether the two paths are equivalent after normalization + */ + public static boolean pathEquals(String path1, String path2) { + return cleanPath(path1).equals(cleanPath(path2)); + } + + /** + * Parse the given {@code localeString} value into a {@link Locale}. + * <p>This is the inverse operation of {@link Locale#toString Locale's toString}. + * @param localeString the locale String, following {@code Locale's} + * {@code toString()} format ("en", "en_UK", etc); + * also accepts spaces as separators, as an alternative to underscores + * @return a corresponding {@code Locale} instance + * @throws IllegalArgumentException in case of an invalid locale specification + */ + public static Locale parseLocaleString(String localeString) { + String[] parts = tokenizeToStringArray(localeString, "_ ", false, false); + String language = (parts.length > 0 ? parts[0] : ""); + String country = (parts.length > 1 ? parts[1] : ""); + validateLocalePart(language); + validateLocalePart(country); + String variant = ""; + if (parts.length > 2) { + // There is definitely a variant, and it is everything after the country + // code sans the separator between the country code and the variant. + int endIndexOfCountryCode = localeString.indexOf(country, language.length()) + country.length(); + // Strip off any leading '_' and whitespace, what's left is the variant. + variant = trimLeadingWhitespace(localeString.substring(endIndexOfCountryCode)); + if (variant.startsWith("_")) { + variant = trimLeadingCharacter(variant, '_'); + } + } + return (language.length() > 0 ? new Locale(language, country, variant) : null); + } + + private static void validateLocalePart(String localePart) { + for (int i = 0; i < localePart.length(); i++) { + char ch = localePart.charAt(i); + if (ch != '_' && ch != ' ' && !Character.isLetterOrDigit(ch)) { + throw new IllegalArgumentException( + "Locale part \"" + localePart + "\" contains invalid characters"); + } + } + } + + /** + * Determine the RFC 3066 compliant language tag, + * as used for the HTTP "Accept-Language" header. + * @param locale the Locale to transform to a language tag + * @return the RFC 3066 compliant language tag as String + */ + public static String toLanguageTag(Locale locale) { + return locale.getLanguage() + (hasText(locale.getCountry()) ? "-" + locale.getCountry() : ""); + } + + + //--------------------------------------------------------------------- + // Convenience methods for working with String arrays + //--------------------------------------------------------------------- + + /** + * Append the given String to the given String array, returning a new array + * consisting of the input array contents plus the given String. + * @param array the array to append to (can be {@code null}) + * @param str the String to append + * @return the new array (never {@code null}) + */ + public static String[] addStringToArray(String[] array, String str) { + if (ObjectUtils.isEmpty(array)) { + return new String[] {str}; + } + String[] newArr = new String[array.length + 1]; + System.arraycopy(array, 0, newArr, 0, array.length); + newArr[array.length] = str; + return newArr; + } + + /** + * Concatenate the given String arrays into one, + * with overlapping array elements included twice. + * <p>The order of elements in the original arrays is preserved. + * @param array1 the first array (can be {@code null}) + * @param array2 the second array (can be {@code null}) + * @return the new array ({@code null} if both given arrays were {@code null}) + */ + public static String[] concatenateStringArrays(String[] array1, String[] array2) { + if (ObjectUtils.isEmpty(array1)) { + return array2; + } + if (ObjectUtils.isEmpty(array2)) { + return array1; + } + String[] newArr = new String[array1.length + array2.length]; + System.arraycopy(array1, 0, newArr, 0, array1.length); + System.arraycopy(array2, 0, newArr, array1.length, array2.length); + return newArr; + } + + /** + * Merge the given String arrays into one, with overlapping + * array elements only included once. + * <p>The order of elements in the original arrays is preserved + * (with the exception of overlapping elements, which are only + * included on their first occurrence). + * @param array1 the first array (can be {@code null}) + * @param array2 the second array (can be {@code null}) + * @return the new array ({@code null} if both given arrays were {@code null}) + */ + public static String[] mergeStringArrays(String[] array1, String[] array2) { + if (ObjectUtils.isEmpty(array1)) { + return array2; + } + if (ObjectUtils.isEmpty(array2)) { + return array1; + } + List<String> result = new ArrayList<String>(); + result.addAll(Arrays.asList(array1)); + for (String str : array2) { + if (!result.contains(str)) { + result.add(str); + } + } + return toStringArray(result); + } + + /** + * Turn given source String array into sorted array. + * @param array the source array + * @return the sorted array (never {@code null}) + */ + public static String[] sortStringArray(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return new String[0]; + } + Arrays.sort(array); + return array; + } + + /** + * Copy the given Collection into a String array. + * The Collection must contain String elements only. + * @param collection the Collection to copy + * @return the String array ({@code null} if the passed-in + * Collection was {@code null}) + */ + public static String[] toStringArray(Collection<String> collection) { + if (collection == null) { + return null; + } + return collection.toArray(new String[collection.size()]); + } + + /** + * Copy the given Enumeration into a String array. + * The Enumeration must contain String elements only. + * @param enumeration the Enumeration to copy + * @return the String array ({@code null} if the passed-in + * Enumeration was {@code null}) + */ + public static String[] toStringArray(Enumeration<String> enumeration) { + if (enumeration == null) { + return null; + } + List<String> list = Collections.list(enumeration); + return list.toArray(new String[list.size()]); + } + + /** + * Trim the elements of the given String array, + * calling {@code String.trim()} on each of them. + * @param array the original String array + * @return the resulting array (of the same size) with trimmed elements + */ + public static String[] trimArrayElements(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return new String[0]; + } + String[] result = new String[array.length]; + for (int i = 0; i < array.length; i++) { + String element = array[i]; + result[i] = (element != null ? element.trim() : null); + } + return result; + } + + /** + * Remove duplicate Strings from the given array. + * Also sorts the array, as it uses a TreeSet. + * @param array the String array + * @return an array without duplicates, in natural sort order + */ + public static String[] removeDuplicateStrings(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return array; + } + Set<String> set = new TreeSet<String>(); + for (String element : array) { + set.add(element); + } + return toStringArray(set); + } + + /** + * Split a String at the first occurrence of the delimiter. + * Does not include the delimiter in the result. + * @param toSplit the string to split + * @param delimiter to split the string up with + * @return a two element array with index 0 being before the delimiter, and + * index 1 being after the delimiter (neither element includes the delimiter); + * or {@code null} if the delimiter wasn't found in the given input String + */ + public static String[] split(String toSplit, String delimiter) { + if (!hasLength(toSplit) || !hasLength(delimiter)) { + return null; + } + int offset = toSplit.indexOf(delimiter); + if (offset < 0) { + return null; + } + String beforeDelimiter = toSplit.substring(0, offset); + String afterDelimiter = toSplit.substring(offset + delimiter.length()); + return new String[] {beforeDelimiter, afterDelimiter}; + } + + /** + * Take an array Strings and split each element based on the given delimiter. + * A {@code Properties} instance is then generated, with the left of the + * delimiter providing the key, and the right of the delimiter providing the value. + * <p>Will trim both the key and value before adding them to the + * {@code Properties} instance. + * @param array the array to process + * @param delimiter to split each element using (typically the equals symbol) + * @return a {@code Properties} instance representing the array contents, + * or {@code null} if the array to process was null or empty + */ + public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter) { + return splitArrayElementsIntoProperties(array, delimiter, null); + } + + /** + * Take an array Strings and split each element based on the given delimiter. + * A {@code Properties} instance is then generated, with the left of the + * delimiter providing the key, and the right of the delimiter providing the value. + * <p>Will trim both the key and value before adding them to the + * {@code Properties} instance. + * @param array the array to process + * @param delimiter to split each element using (typically the equals symbol) + * @param charsToDelete one or more characters to remove from each element + * prior to attempting the split operation (typically the quotation mark + * symbol), or {@code null} if no removal should occur + * @return a {@code Properties} instance representing the array contents, + * or {@code null} if the array to process was {@code null} or empty + */ + public static Properties splitArrayElementsIntoProperties( + String[] array, String delimiter, String charsToDelete) { + + if (ObjectUtils.isEmpty(array)) { + return null; + } + Properties result = new Properties(); + for (String element : array) { + if (charsToDelete != null) { + element = deleteAny(element, charsToDelete); + } + String[] splittedElement = split(element, delimiter); + if (splittedElement == null) { + continue; + } + result.setProperty(splittedElement[0].trim(), splittedElement[1].trim()); + } + return result; + } + + /** + * Tokenize the given String into a String array via a StringTokenizer. + * Trims tokens and omits empty tokens. + * <p>The given delimiters string is supposed to consist of any number of + * delimiter characters. Each of those characters can be used to separate + * tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using {@code delimitedListToStringArray} + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String + * (each of those characters is individually considered as delimiter). + * @return an array of the tokens + * @see java.util.StringTokenizer + * @see String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray(String str, String delimiters) { + return tokenizeToStringArray(str, delimiters, true, true); + } + + /** + * Tokenize the given String into a String array via a StringTokenizer. + * <p>The given delimiters string is supposed to consist of any number of + * delimiter characters. Each of those characters can be used to separate + * tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using {@code delimitedListToStringArray} + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String + * (each of those characters is individually considered as delimiter) + * @param trimTokens trim the tokens via String's {@code trim} + * @param ignoreEmptyTokens omit empty tokens from the result array + * (only applies to tokens that are empty after trimming; StringTokenizer + * will not consider subsequent delimiters as token in the first place). + * @return an array of the tokens ({@code null} if the input String + * was {@code null}) + * @see java.util.StringTokenizer + * @see String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray( + String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { + + if (str == null) { + return null; + } + StringTokenizer st = new StringTokenizer(str, delimiters); + List<String> tokens = new ArrayList<String>(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (!ignoreEmptyTokens || token.length() > 0) { + tokens.add(token); + } + } + return toStringArray(tokens); + } + + /** + * Take a String which is a delimited list and convert it to a String array. + * <p>A single delimiter can consists of more than one character: It will still + * be considered as single delimiter string, rather than as bunch of potential + * delimiter characters - in contrast to {@code tokenizeToStringArray}. + * @param str the input String + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) + * @return an array of the tokens in the list + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(String str, String delimiter) { + return delimitedListToStringArray(str, delimiter, null); + } + + /** + * Take a String which is a delimited list and convert it to a String array. + * <p>A single delimiter can consists of more than one character: It will still + * be considered as single delimiter string, rather than as bunch of potential + * delimiter characters - in contrast to {@code tokenizeToStringArray}. + * @param str the input String + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) + * @param charsToDelete a set of characters to delete. Useful for deleting unwanted + * line breaks: e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * @return an array of the tokens in the list + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(String str, String delimiter, String charsToDelete) { + if (str == null) { + return new String[0]; + } + if (delimiter == null) { + return new String[] {str}; + } + List<String> result = new ArrayList<String>(); + if ("".equals(delimiter)) { + for (int i = 0; i < str.length(); i++) { + result.add(deleteAny(str.substring(i, i + 1), charsToDelete)); + } + } + else { + int pos = 0; + int delPos; + while ((delPos = str.indexOf(delimiter, pos)) != -1) { + result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); + pos = delPos + delimiter.length(); + } + if (str.length() > 0 && pos <= str.length()) { + // Add rest of String, but not in case of empty input. + result.add(deleteAny(str.substring(pos), charsToDelete)); + } + } + return toStringArray(result); + } + + /** + * Convert a CSV list into an array of Strings. + * @param str the input String + * @return an array of Strings, or the empty array in case of empty input + */ + public static String[] commaDelimitedListToStringArray(String str) { + return delimitedListToStringArray(str, ","); + } + + /** + * Convenience method to convert a CSV string list to a set. + * Note that this will suppress duplicates. + * @param str the input String + * @return a Set of String entries in the list + */ + public static Set<String> commaDelimitedListToSet(String str) { + Set<String> set = new TreeSet<String>(); + String[] tokens = commaDelimitedListToStringArray(str); + for (String token : tokens) { + set.add(token); + } + return set; + } + + /** + * Convenience method to return a Collection as a delimited (e.g. CSV) + * String. E.g. useful for {@code toString()} implementations. + * @param coll the Collection to display + * @param delim the delimiter to use (probably a ",") + * @param prefix the String to start each element with + * @param suffix the String to end each element with + * @return the delimited String + */ + public static String collectionToDelimitedString(Collection<?> coll, String delim, String prefix, String suffix) { + if (CollectionUtils.isEmpty(coll)) { + return ""; + } + StringBuilder sb = new StringBuilder(); + Iterator<?> it = coll.iterator(); + while (it.hasNext()) { + sb.append(prefix).append(it.next()).append(suffix); + if (it.hasNext()) { + sb.append(delim); + } + } + return sb.toString(); + } + + /** + * Convenience method to return a Collection as a delimited (e.g. CSV) + * String. E.g. useful for {@code toString()} implementations. + * @param coll the Collection to display + * @param delim the delimiter to use (probably a ",") + * @return the delimited String + */ + public static String collectionToDelimitedString(Collection<?> coll, String delim) { + return collectionToDelimitedString(coll, delim, "", ""); + } + + /** + * Convenience method to return a Collection as a CSV String. + * E.g. useful for {@code toString()} implementations. + * @param coll the Collection to display + * @return the delimited String + */ + public static String collectionToCommaDelimitedString(Collection<?> coll) { + return collectionToDelimitedString(coll, ","); + } + + /** + * Convenience method to return a String array as a delimited (e.g. CSV) + * String. E.g. useful for {@code toString()} implementations. + * @param arr the array to display + * @param delim the delimiter to use (probably a ",") + * @return the delimited String + */ + public static String arrayToDelimitedString(Object[] arr, String delim) { + if (ObjectUtils.isEmpty(arr)) { + return ""; + } + if (arr.length == 1) { + return ObjectUtils.nullSafeToString(arr[0]); + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < arr.length; i++) { + if (i > 0) { + sb.append(delim); + } + sb.append(arr[i]); + } + return sb.toString(); + } + + /** + * Convenience method to return a String array as a CSV String. + * E.g. useful for {@code toString()} implementations. + * @param arr the array to display + * @return the delimited String + */ + public static String arrayToCommaDelimitedString(Object[] arr) { + return arrayToDelimitedString(arr, ","); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/StringValueResolver.java b/spring-core/src/main/java/org/springframework/util/StringValueResolver.java new file mode 100644 index 00000000..acc77c1e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/StringValueResolver.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2007 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.util; + +/** + * Simple strategy interface for resolving a String value. + * Used by {@link org.springframework.beans.factory.config.ConfigurableBeanFactory}. + * + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#resolveAliases + * @see org.springframework.beans.factory.config.BeanDefinitionVisitor#BeanDefinitionVisitor(StringValueResolver) + * @see org.springframework.beans.factory.config.PropertyPlaceholderConfigurer + */ +public interface StringValueResolver { + + /** + * Resolve the given String value, for example parsing placeholders. + * @param strVal the original String value + * @return the resolved String value + */ + String resolveStringValue(String strVal); + +} diff --git a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java new file mode 100644 index 00000000..6c3acd7e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2013 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.util; + +/** + * Helper class for resolving placeholders in texts. Usually applied to file paths. + * + * <p>A text may contain {@code ${...}} placeholders, to be resolved as system properties: + * e.g. {@code ${user.dir}}. Default values can be supplied using the ":" separator + * between key and value. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Dave Syer + * @since 1.2.5 + * @see #PLACEHOLDER_PREFIX + * @see #PLACEHOLDER_SUFFIX + * @see System#getProperty(String) + */ +public abstract class SystemPropertyUtils { + + /** Prefix for system property placeholders: "${" */ + public static final String PLACEHOLDER_PREFIX = "${"; + + /** Suffix for system property placeholders: "}" */ + public static final String PLACEHOLDER_SUFFIX = "}"; + + /** Value separator for system property placeholders: ":" */ + public static final String VALUE_SEPARATOR = ":"; + + + private static final PropertyPlaceholderHelper strictHelper = + new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, false); + + private static final PropertyPlaceholderHelper nonStrictHelper = + new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, true); + + + /** + * Resolve {@code ${...}} placeholders in the given text, replacing them with + * corresponding system property values. + * @param text the String to resolve + * @return the resolved String + * @see #PLACEHOLDER_PREFIX + * @see #PLACEHOLDER_SUFFIX + * @throws IllegalArgumentException if there is an unresolvable placeholder + */ + public static String resolvePlaceholders(String text) { + return resolvePlaceholders(text, false); + } + + /** + * Resolve {@code ${...}} placeholders in the given text, replacing them with + * corresponding system property values. Unresolvable placeholders with no default + * value are ignored and passed through unchanged if the flag is set to {@code true}. + * @param text the String to resolve + * @param ignoreUnresolvablePlaceholders whether unresolved placeholders are to be ignored + * @return the resolved String + * @see #PLACEHOLDER_PREFIX + * @see #PLACEHOLDER_SUFFIX + * @throws IllegalArgumentException if there is an unresolvable placeholder + * and the "ignoreUnresolvablePlaceholders" flag is {@code false} + */ + public static String resolvePlaceholders(String text, boolean ignoreUnresolvablePlaceholders) { + PropertyPlaceholderHelper helper = (ignoreUnresolvablePlaceholders ? nonStrictHelper : strictHelper); + return helper.replacePlaceholders(text, new SystemPropertyPlaceholderResolver(text)); + } + + + /** + * PlaceholderResolver implementation that resolves against system properties + * and system environment variables. + */ + private static class SystemPropertyPlaceholderResolver implements PropertyPlaceholderHelper.PlaceholderResolver { + + private final String text; + + public SystemPropertyPlaceholderResolver(String text) { + this.text = text; + } + + public String resolvePlaceholder(String placeholderName) { + try { + String propVal = System.getProperty(placeholderName); + if (propVal == null) { + // Fall back to searching the system environment. + propVal = System.getenv(placeholderName); + } + return propVal; + } + catch (Throwable ex) { + System.err.println("Could not resolve placeholder '" + placeholderName + "' in [" + + this.text + "] as system property: " + ex); + return null; + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/TypeUtils.java b/spring-core/src/main/java/org/springframework/util/TypeUtils.java new file mode 100644 index 00000000..3733f7cf --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/TypeUtils.java @@ -0,0 +1,227 @@ +/* + * Copyright 2002-2012 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.util; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; + +import org.springframework.util.ClassUtils; + +/** + * Utility to work with Java 5 generic type parameters. + * Mainly for internal use within the framework. + * + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @author Chris Beams + * @since 2.0.7 + */ +public abstract class TypeUtils { + + /** + * Check if the right-hand side type may be assigned to the left-hand side + * type following the Java generics rules. + * @param lhsType the target type + * @param rhsType the value type that should be assigned to the target type + * @return true if rhs is assignable to lhs + */ + public static boolean isAssignable(Type lhsType, Type rhsType) { + Assert.notNull(lhsType, "Left-hand side type must not be null"); + Assert.notNull(rhsType, "Right-hand side type must not be null"); + + // all types are assignable to themselves and to class Object + if (lhsType.equals(rhsType) || lhsType.equals(Object.class)) { + return true; + } + + if (lhsType instanceof Class<?>) { + Class<?> lhsClass = (Class<?>) lhsType; + + // just comparing two classes + if (rhsType instanceof Class<?>) { + return ClassUtils.isAssignable(lhsClass, (Class<?>) rhsType); + } + + if (rhsType instanceof ParameterizedType) { + Type rhsRaw = ((ParameterizedType) rhsType).getRawType(); + + // a parameterized type is always assignable to its raw class type + if (rhsRaw instanceof Class<?>) { + return ClassUtils.isAssignable(lhsClass, (Class<?>) rhsRaw); + } + } + else if (lhsClass.isArray() && rhsType instanceof GenericArrayType) { + Type rhsComponent = ((GenericArrayType) rhsType).getGenericComponentType(); + + return isAssignable(lhsClass.getComponentType(), rhsComponent); + } + } + + // parameterized types are only assignable to other parameterized types and class types + if (lhsType instanceof ParameterizedType) { + if (rhsType instanceof Class<?>) { + Type lhsRaw = ((ParameterizedType) lhsType).getRawType(); + + if (lhsRaw instanceof Class<?>) { + return ClassUtils.isAssignable((Class<?>) lhsRaw, (Class<?>) rhsType); + } + } + else if (rhsType instanceof ParameterizedType) { + return isAssignable((ParameterizedType) lhsType, (ParameterizedType) rhsType); + } + } + + if (lhsType instanceof GenericArrayType) { + Type lhsComponent = ((GenericArrayType) lhsType).getGenericComponentType(); + + if (rhsType instanceof Class<?>) { + Class<?> rhsClass = (Class<?>) rhsType; + + if (rhsClass.isArray()) { + return isAssignable(lhsComponent, rhsClass.getComponentType()); + } + } + else if (rhsType instanceof GenericArrayType) { + Type rhsComponent = ((GenericArrayType) rhsType).getGenericComponentType(); + + return isAssignable(lhsComponent, rhsComponent); + } + } + + if (lhsType instanceof WildcardType) { + return isAssignable((WildcardType) lhsType, rhsType); + } + + return false; + } + + private static boolean isAssignable(ParameterizedType lhsType, ParameterizedType rhsType) { + if (lhsType.equals(rhsType)) { + return true; + } + + Type[] lhsTypeArguments = lhsType.getActualTypeArguments(); + Type[] rhsTypeArguments = rhsType.getActualTypeArguments(); + + if (lhsTypeArguments.length != rhsTypeArguments.length) { + return false; + } + + for (int size = lhsTypeArguments.length, i = 0; i < size; ++i) { + Type lhsArg = lhsTypeArguments[i]; + Type rhsArg = rhsTypeArguments[i]; + + if (!lhsArg.equals(rhsArg) && + !(lhsArg instanceof WildcardType && isAssignable((WildcardType) lhsArg, rhsArg))) { + return false; + } + } + + return true; + } + + private static boolean isAssignable(WildcardType lhsType, Type rhsType) { + Type[] lUpperBounds = lhsType.getUpperBounds(); + + // supply the implicit upper bound if none are specified + if (lUpperBounds.length == 0) { + lUpperBounds = new Type[] { Object.class }; + } + + Type[] lLowerBounds = lhsType.getLowerBounds(); + + // supply the implicit lower bound if none are specified + if (lLowerBounds.length == 0) { + lLowerBounds = new Type[] { null }; + } + + if (rhsType instanceof WildcardType) { + // both the upper and lower bounds of the right-hand side must be + // completely enclosed in the upper and lower bounds of the left- + // hand side. + WildcardType rhsWcType = (WildcardType) rhsType; + Type[] rUpperBounds = rhsWcType.getUpperBounds(); + + if (rUpperBounds.length == 0) { + rUpperBounds = new Type[] { Object.class }; + } + + Type[] rLowerBounds = rhsWcType.getLowerBounds(); + + if (rLowerBounds.length == 0) { + rLowerBounds = new Type[] { null }; + } + + for (Type lBound : lUpperBounds) { + for (Type rBound : rUpperBounds) { + if (!isAssignableBound(lBound, rBound)) { + return false; + } + } + + for (Type rBound : rLowerBounds) { + if (!isAssignableBound(lBound, rBound)) { + return false; + } + } + } + + for (Type lBound : lLowerBounds) { + for (Type rBound : rUpperBounds) { + if (!isAssignableBound(rBound, lBound)) { + return false; + } + } + + for (Type rBound : rLowerBounds) { + if (!isAssignableBound(rBound, lBound)) { + return false; + } + } + } + } + else { + for (Type lBound : lUpperBounds) { + if (!isAssignableBound(lBound, rhsType)) { + return false; + } + } + + for (Type lBound : lLowerBounds) { + if (!isAssignableBound(rhsType, lBound)) { + return false; + } + } + } + + return true; + } + + public static boolean isAssignableBound(Type lhsType, Type rhsType) { + if (rhsType == null) { + return true; + } + + if (lhsType == null) { + return false; + } + return isAssignable(lhsType, rhsType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/WeakReferenceMonitor.java b/spring-core/src/main/java/org/springframework/util/WeakReferenceMonitor.java new file mode 100644 index 00000000..ad2e01c5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/WeakReferenceMonitor.java @@ -0,0 +1,178 @@ +/* + * Copyright 2002-2010 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.util; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Track references to arbitrary objects using proxy and weak references. To + * monitor a handle, one should call {@link #monitor(Object, ReleaseListener)}, + * with the given handle object usually being a holder that uses the target + * object underneath, and the release listener performing cleanup of the + * target object once the handle is not strongly referenced anymore. + * + * <p>When a given handle becomes weakly reachable, the specified listener + * will be called by a background thread. This thread will only be started + * lazily and will be stopped once no handles are registered for monitoring + * anymore, to be restarted if further handles are added. + * + * <p>Thanks to Tomasz Wysocki for the suggestion and the original + * implementation of this class! + * + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @since 1.2 + * @see #monitor + */ +public class WeakReferenceMonitor { + + private static final Log logger = LogFactory.getLog(WeakReferenceMonitor.class); + + // Queue receiving reachability events + private static final ReferenceQueue<Object> handleQueue = new ReferenceQueue<Object>(); + + // All tracked entries (WeakReference => ReleaseListener) + private static final Map<Reference, ReleaseListener> trackedEntries = new HashMap<Reference, ReleaseListener>(); + + // Thread polling handleQueue, lazy initialized + private static Thread monitoringThread = null; + + + /** + * Start to monitor given handle object for becoming weakly reachable. + * When the handle isn't used anymore, the given listener will be called. + * @param handle the object that will be monitored + * @param listener the listener that will be called upon release of the handle + */ + public static void monitor(Object handle, ReleaseListener listener) { + if (logger.isDebugEnabled()) { + logger.debug("Monitoring handle [" + handle + "] with release listener [" + listener + "]"); + } + + // Make weak reference to this handle, so we can say when + // handle is not used any more by polling on handleQueue. + WeakReference<Object> weakRef = new WeakReference<Object>(handle, handleQueue); + + // Add monitored entry to internal map of all monitored entries. + addEntry(weakRef, listener); + } + + /** + * Add entry to internal map of tracked entries. + * Internal monitoring thread is started if not already running. + * @param ref reference to tracked handle + * @param entry the associated entry + */ + private static void addEntry(Reference ref, ReleaseListener entry) { + synchronized (WeakReferenceMonitor.class) { + // Add entry, the key is given reference. + trackedEntries.put(ref, entry); + + // Start monitoring thread lazily. + if (monitoringThread == null) { + monitoringThread = new Thread(new MonitoringProcess(), WeakReferenceMonitor.class.getName()); + monitoringThread.setDaemon(true); + monitoringThread.start(); + } + } + } + + /** + * Remove entry from internal map of tracked entries. + * @param reference the reference that should be removed + * @return entry object associated with given reference + */ + private static ReleaseListener removeEntry(Reference reference) { + synchronized (WeakReferenceMonitor.class) { + return trackedEntries.remove(reference); + } + } + + /** + * Check whether to keep the monitoring thread alive, + * i.e. whether there are still entries being tracked. + */ + private static boolean keepMonitoringThreadAlive() { + synchronized (WeakReferenceMonitor.class) { + if (!trackedEntries.isEmpty()) { + return true; + } + else { + logger.debug("No entries left to track - stopping reference monitor thread"); + monitoringThread = null; + return false; + } + } + } + + + /** + * Thread implementation that performs the actual monitoring. + */ + private static class MonitoringProcess implements Runnable { + + public void run() { + logger.debug("Starting reference monitor thread"); + // Check if there are any tracked entries left. + while (keepMonitoringThreadAlive()) { + try { + Reference reference = handleQueue.remove(); + // Stop tracking this reference. + ReleaseListener entry = removeEntry(reference); + if (entry != null) { + // Invoke listener callback. + try { + entry.released(); + } + catch (Throwable ex) { + logger.warn("Reference release listener threw exception", ex); + } + } + } + catch (InterruptedException ex) { + synchronized (WeakReferenceMonitor.class) { + monitoringThread = null; + } + logger.debug("Reference monitor thread interrupted", ex); + break; + } + } + } + } + + + /** + * Listener that is notified when the handle is being released. + * To be implemented by users of this reference monitor. + */ + public static interface ReleaseListener { + + /** + * This callback method is invoked once the associated handle has been released, + * i.e. once there are no monitored strong references to the handle anymore. + */ + void released(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java new file mode 100644 index 00000000..6fd0c229 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2012 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.util.comparator; + +import java.io.Serializable; +import java.util.Comparator; + +/** + * A Comparator for Boolean objects that can sort either true or false first. + * + * @author Keith Donald + * @since 1.2.2 + */ +@SuppressWarnings("serial") +public final class BooleanComparator implements Comparator<Boolean>, Serializable { + + /** + * A shared default instance of this comparator, treating true lower + * than false. + */ + public static final BooleanComparator TRUE_LOW = new BooleanComparator(true); + + /** + * A shared default instance of this comparator, treating true higher + * than false. + */ + public static final BooleanComparator TRUE_HIGH = new BooleanComparator(false); + + + private final boolean trueLow; + + + /** + * Create a BooleanComparator that sorts boolean values based on + * the provided flag. + * <p>Alternatively, you can use the default shared instances: + * {@code BooleanComparator.TRUE_LOW} and + * {@code BooleanComparator.TRUE_HIGH}. + * @param trueLow whether to treat true as lower or higher than false + * @see #TRUE_LOW + * @see #TRUE_HIGH + */ + public BooleanComparator(boolean trueLow) { + this.trueLow = trueLow; + } + + + public int compare(Boolean v1, Boolean v2) { + return (v1 ^ v2) ? ((v1 ^ this.trueLow) ? 1 : -1) : 0; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof BooleanComparator)) { + return false; + } + return (this.trueLow == ((BooleanComparator) obj).trueLow); + } + + @Override + public int hashCode() { + return (this.trueLow ? -1 : 1) * getClass().hashCode(); + } + + @Override + public String toString() { + return "BooleanComparator: " + (this.trueLow ? "true low" : "true high"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/comparator/ComparableComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/ComparableComparator.java new file mode 100644 index 00000000..40e4e7af --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/ComparableComparator.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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.util.comparator; + +import java.util.Comparator; + +/** + * Comparator that adapts Comparables to the Comparator interface. + * Mainly for internal use in other Comparators, when supposed + * to work on Comparables. + * + * @author Keith Donald + * @since 1.2.2 + * @see Comparable + */ +public class ComparableComparator<T extends Comparable<T>> implements Comparator<T> { + + @SuppressWarnings("rawtypes") + public static final ComparableComparator INSTANCE = new ComparableComparator(); + + public int compare(T o1, T o2) { + return o1.compareTo(o2); + } + +} 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 new file mode 100644 index 00000000..ba56bd54 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/CompoundComparator.java @@ -0,0 +1,200 @@ +/* + * Copyright 2002-2012 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.util.comparator; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * A comparator that chains a sequence of one or more more Comparators. + * + * <p>A compound comparator calls each Comparator in sequence until a single + * Comparator returns a non-zero result, or the comparators are exhausted and + * zero is returned. + * + * <p>This facilitates in-memory sorting similar to multi-column sorting in SQL. + * The order of any single Comparator in the list can also be reversed. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + */ +@SuppressWarnings("serial") +public class CompoundComparator<T> implements Comparator<T>, Serializable { + + private final List<InvertibleComparator<T>> comparators; + + + /** + * Construct a CompoundComparator with initially no Comparators. Clients + * must add at least one Comparator before calling the compare method or an + * IllegalStateException is thrown. + */ + public CompoundComparator() { + this.comparators = new ArrayList<InvertibleComparator<T>>(); + } + + /** + * Construct a CompoundComparator from the Comparators in the provided array. + * <p>All Comparators will default to ascending sort order, + * unless they are InvertibleComparators. + * @param comparators the comparators to build into a compound comparator + * @see InvertibleComparator + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public CompoundComparator(Comparator... comparators) { + Assert.notNull(comparators, "Comparators must not be null"); + this.comparators = new ArrayList<InvertibleComparator<T>>(comparators.length); + for (Comparator comparator : comparators) { + this.addComparator(comparator); + } + } + + + /** + * Add a Comparator to the end of the chain. + * <p>The Comparator will default to ascending sort order, + * unless it is a InvertibleComparator. + * @param comparator the Comparator to add to the end of the chain + * @see InvertibleComparator + */ + public void addComparator(Comparator<T> comparator) { + if (comparator instanceof InvertibleComparator) { + this.comparators.add((InvertibleComparator<T>) comparator); + } + else { + this.comparators.add(new InvertibleComparator<T>(comparator)); + } + } + + /** + * Add a Comparator to the end of the chain using the provided sort order. + * @param comparator the Comparator to add to the end of the chain + * @param ascending the sort order: ascending (true) or descending (false) + */ + public void addComparator(Comparator<T> comparator, boolean ascending) { + this.comparators.add(new InvertibleComparator<T>(comparator, ascending)); + } + + /** + * Replace the Comparator at the given index. + * <p>The Comparator will default to ascending sort order, + * unless it is a InvertibleComparator. + * @param index the index of the Comparator to replace + * @param comparator the Comparator to place at the given index + * @see InvertibleComparator + */ + public void setComparator(int index, Comparator<T> comparator) { + if (comparator instanceof InvertibleComparator) { + this.comparators.set(index, (InvertibleComparator<T>) comparator); + } + else { + this.comparators.set(index, new InvertibleComparator<T>(comparator)); + } + } + + /** + * Replace the Comparator at the given index using the given sort order. + * @param index the index of the Comparator to replace + * @param comparator the Comparator to place at the given index + * @param ascending the sort order: ascending (true) or descending (false) + */ + public void setComparator(int index, Comparator<T> comparator, boolean ascending) { + this.comparators.set(index, new InvertibleComparator<T>(comparator, ascending)); + } + + /** + * Invert the sort order of each sort definition contained by this compound + * comparator. + */ + public void invertOrder() { + for (InvertibleComparator<T> comparator : this.comparators) { + comparator.invertOrder(); + } + } + + /** + * Invert the sort order of the sort definition at the specified index. + * @param index the index of the comparator to invert + */ + public void invertOrder(int index) { + this.comparators.get(index).invertOrder(); + } + + /** + * Change the sort order at the given index to ascending. + * @param index the index of the comparator to change + */ + public void setAscendingOrder(int index) { + this.comparators.get(index).setAscending(true); + } + + /** + * Change the sort order at the given index to descending sort. + * @param index the index of the comparator to change + */ + public void setDescendingOrder(int index) { + this.comparators.get(index).setAscending(false); + } + + /** + * Returns the number of aggregated comparators. + */ + public int getComparatorCount() { + return this.comparators.size(); + } + + public int compare(T o1, T o2) { + Assert.state(this.comparators.size() > 0, + "No sort definitions have been added to this CompoundComparator to compare"); + for (InvertibleComparator<T> comparator : this.comparators) { + int result = comparator.compare(o1, o2); + if (result != 0) { + return result; + } + } + return 0; + } + + @Override + @SuppressWarnings("unchecked") + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CompoundComparator)) { + return false; + } + CompoundComparator<T> other = (CompoundComparator<T>) obj; + return this.comparators.equals(other.comparators); + } + + @Override + public int hashCode() { + return this.comparators.hashCode(); + } + + @Override + public String toString() { + return "CompoundComparator: " + this.comparators; + } + +} 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 new file mode 100644 index 00000000..f29310e2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/InstanceComparator.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2014 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.util.comparator; + +import java.util.Comparator; + +import org.springframework.util.Assert; + +/** + * Compares objects based on an arbitrary class order. Allows objects to be sorted based + * on the types of class that they inherit, for example: this comparator can be used to + * sort a list {@code Number}s such that {@code Long}s occur before {@code Integer}s. + * + * <p>Only the specified {@code instanceOrder} classes are considered during comparison. + * If two objects are both instances of the ordered type this comparator will return a + * {@code 0}. Consider combining with a {@link CompoundComparator} if additional sorting + * is required. + * + * @author Phillip Webb + * @since 3.2 + * @param <T> the type of objects being compared + * @see CompoundComparator + */ +public class InstanceComparator<T> implements Comparator<T> { + + private final Class<?>[] instanceOrder; + + + /** + * 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. + */ + public InstanceComparator(Class<?>... instanceOrder) { + Assert.notNull(instanceOrder, "'instanceOrder' must not be null"); + this.instanceOrder = instanceOrder; + } + + + public int compare(T o1, T o2) { + int i1 = getOrder(o1); + int i2 = getOrder(o2); + return (i1 < i2 ? -1 : (i1 == i2 ? 0 : 1)); + } + + private int getOrder(T object) { + if (object != null) { + for (int i = 0; i < this.instanceOrder.length; i++) { + if (this.instanceOrder[i].isInstance(object)) { + return i; + } + } + } + return this.instanceOrder.length; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/comparator/InvertibleComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/InvertibleComparator.java new file mode 100644 index 00000000..f78e56ff --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/InvertibleComparator.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2012 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.util.comparator; + +import java.io.Serializable; +import java.util.Comparator; + +import org.springframework.util.Assert; + +/** + * A decorator for a comparator, with an "ascending" flag denoting + * whether comparison results should be treated in forward (standard + * ascending) order or flipped for reverse (descending) order. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + */ +@SuppressWarnings("serial") +public class InvertibleComparator<T> implements Comparator<T>, Serializable { + + private final Comparator<T> comparator; + + private boolean ascending = true; + + + /** + * Create an InvertibleComparator that sorts ascending by default. + * For the actual comparison, the specified Comparator will be used. + * @param comparator the comparator to decorate + */ + public InvertibleComparator(Comparator<T> comparator) { + Assert.notNull(comparator, "Comparator must not be null"); + this.comparator = comparator; + } + + /** + * Create an InvertibleComparator that sorts based on the provided order. + * For the actual comparison, the specified Comparator will be used. + * @param comparator the comparator to decorate + * @param ascending the sort order: ascending (true) or descending (false) + */ + public InvertibleComparator(Comparator<T> comparator, boolean ascending) { + Assert.notNull(comparator, "Comparator must not be null"); + this.comparator = comparator; + setAscending(ascending); + } + + + /** + * Specify the sort order: ascending (true) or descending (false). + */ + public void setAscending(boolean ascending) { + this.ascending = ascending; + } + + /** + * Return the sort order: ascending (true) or descending (false). + */ + public boolean isAscending() { + return this.ascending; + } + + /** + * Invert the sort order: ascending -> descending or + * descending -> ascending. + */ + public void invertOrder() { + this.ascending = !this.ascending; + } + + + public int compare(T o1, T o2) { + int result = this.comparator.compare(o1, o2); + if (result != 0) { + // Invert the order if it is a reverse sort. + if (!this.ascending) { + if (Integer.MIN_VALUE == result) { + result = Integer.MAX_VALUE; + } + else { + result *= -1; + } + } + return result; + } + return 0; + } + + @Override + @SuppressWarnings("unchecked") + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof InvertibleComparator)) { + return false; + } + InvertibleComparator<T> other = (InvertibleComparator<T>) obj; + return (this.comparator.equals(other.comparator) && this.ascending == other.ascending); + } + + @Override + public int hashCode() { + return this.comparator.hashCode(); + } + + @Override + public String toString() { + return "InvertibleComparator: [" + this.comparator + "]; ascending=" + this.ascending; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java new file mode 100644 index 00000000..1171bdc1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2012 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.util.comparator; + +import java.util.Comparator; + +import org.springframework.util.Assert; + +/** + * A Comparator that will safely compare nulls to be lower or higher than + * other objects. Can decorate a given Comparator or work on Comparables. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + * @see Comparable + */ +public class NullSafeComparator<T> implements Comparator<T> { + + /** + * A shared default instance of this comparator, treating nulls lower + * than non-null objects. + */ + @SuppressWarnings("rawtypes") + public static final NullSafeComparator NULLS_LOW = new NullSafeComparator<Object>(true); + + /** + * A shared default instance of this comparator, treating nulls higher + * than non-null objects. + */ + @SuppressWarnings("rawtypes") + public static final NullSafeComparator NULLS_HIGH = new NullSafeComparator<Object>(false); + + private final Comparator<T> nonNullComparator; + + private final boolean nullsLow; + + + /** + * Create a NullSafeComparator that sorts {@code null} based on + * the provided flag, working on Comparables. + * <p>When comparing two non-null objects, their Comparable implementation + * will be used: this means that non-null elements (that this Comparator + * will be applied to) need to implement Comparable. + * <p>As a convenience, you can use the default shared instances: + * {@code NullSafeComparator.NULLS_LOW} and + * {@code NullSafeComparator.NULLS_HIGH}. + * @param nullsLow whether to treat nulls lower or higher than non-null objects + * @see Comparable + * @see #NULLS_LOW + * @see #NULLS_HIGH + */ + @SuppressWarnings({ "unchecked"}) + private NullSafeComparator(boolean nullsLow) { + this.nonNullComparator = new ComparableComparator(); + this.nullsLow = nullsLow; + } + + /** + * Create a NullSafeComparator that sorts {@code null} based on the + * provided flag, decorating the given Comparator. + * <p>When comparing two non-null objects, the specified Comparator will be used. + * The given underlying Comparator must be able to handle the elements that this + * Comparator will be applied to. + * @param comparator the comparator to use when comparing two non-null objects + * @param nullsLow whether to treat nulls lower or higher than non-null objects + */ + public NullSafeComparator(Comparator<T> comparator, boolean nullsLow) { + Assert.notNull(comparator, "The non-null comparator is required"); + this.nonNullComparator = comparator; + this.nullsLow = nullsLow; + } + + + public int compare(T o1, T o2) { + if (o1 == o2) { + return 0; + } + if (o1 == null) { + return (this.nullsLow ? -1 : 1); + } + if (o2 == null) { + return (this.nullsLow ? 1 : -1); + } + return this.nonNullComparator.compare(o1, o2); + } + + @Override + @SuppressWarnings("unchecked") + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof NullSafeComparator)) { + return false; + } + NullSafeComparator<T> other = (NullSafeComparator<T>) obj; + return (this.nonNullComparator.equals(other.nonNullComparator) && this.nullsLow == other.nullsLow); + } + + @Override + public int hashCode() { + return (this.nullsLow ? -1 : 1) * this.nonNullComparator.hashCode(); + } + + @Override + public String toString() { + return "NullSafeComparator: non-null comparator [" + this.nonNullComparator + "]; " + + (this.nullsLow ? "nulls low" : "nulls high"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/comparator/package-info.java b/spring-core/src/main/java/org/springframework/util/comparator/package-info.java new file mode 100644 index 00000000..f2213670 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * Useful generic {@code java.util.Comparator} implementations, + * such as an invertible comparator and a compound comparator. + * + */ +package org.springframework.util.comparator; + diff --git a/spring-core/src/main/java/org/springframework/util/package-info.java b/spring-core/src/main/java/org/springframework/util/package-info.java new file mode 100644 index 00000000..59adb5f8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * Miscellaneous utility classes, such as String manipulation utilities, + * a Log4J configurer, and a state holder for paged lists of objects. + * + */ +package org.springframework.util; + diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxContentHandler.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxContentHandler.java new file mode 100644 index 00000000..46f4d98e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxContentHandler.java @@ -0,0 +1,175 @@ +/* + * Copyright 2002-2012 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.util.xml; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLStreamException; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * Abstract base class for SAX {@code ContentHandler} implementations that use StAX as a basis. All methods + * delegate to internal template methods, capable of throwing a {@code XMLStreamException}. Additionally, an + * namespace context is used to keep track of declared namespaces. + * + * @author Arjen Poutsma + * @since 3.0 + */ +abstract class AbstractStaxContentHandler implements ContentHandler { + + private SimpleNamespaceContext namespaceContext = new SimpleNamespaceContext(); + + private boolean namespaceContextChanged = false; + + public final void startDocument() throws SAXException { + namespaceContext.clear(); + namespaceContextChanged = false; + try { + startDocumentInternal(); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle startDocument: " + ex.getMessage(), ex); + } + } + + protected abstract void startDocumentInternal() throws XMLStreamException; + + public final void endDocument() throws SAXException { + namespaceContext.clear(); + namespaceContextChanged = false; + try { + endDocumentInternal(); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle startDocument: " + ex.getMessage(), ex); + } + } + + protected abstract void endDocumentInternal() throws XMLStreamException; + + /** + * Binds the given prefix to the given namespaces. + * + * @see SimpleNamespaceContext#bindNamespaceUri(String,String) + */ + public final void startPrefixMapping(String prefix, String uri) { + namespaceContext.bindNamespaceUri(prefix, uri); + namespaceContextChanged = true; + } + + /** + * Removes the binding for the given prefix. + * + * @see SimpleNamespaceContext#removeBinding(String) + */ + public final void endPrefixMapping(String prefix) { + namespaceContext.removeBinding(prefix); + namespaceContextChanged = true; + } + + public final void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { + try { + startElementInternal(toQName(uri, qName), atts, namespaceContextChanged ? namespaceContext : null); + namespaceContextChanged = false; + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle startElement: " + ex.getMessage(), ex); + } + } + + protected abstract void startElementInternal(QName name, Attributes atts, SimpleNamespaceContext namespaceContext) + throws XMLStreamException; + + public final void endElement(String uri, String localName, String qName) throws SAXException { + try { + endElementInternal(toQName(uri, qName), namespaceContextChanged ? namespaceContext : null); + namespaceContextChanged = false; + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle endElement: " + ex.getMessage(), ex); + } + } + + protected abstract void endElementInternal(QName name, SimpleNamespaceContext namespaceContext) + throws XMLStreamException; + + public final void characters(char ch[], int start, int length) throws SAXException { + try { + charactersInternal(ch, start, length); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle characters: " + ex.getMessage(), ex); + } + } + + protected abstract void charactersInternal(char[] ch, int start, int length) throws XMLStreamException; + + public final void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { + try { + ignorableWhitespaceInternal(ch, start, length); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle ignorableWhitespace:" + ex.getMessage(), ex); + } + } + + protected abstract void ignorableWhitespaceInternal(char[] ch, int start, int length) throws XMLStreamException; + + public final void processingInstruction(String target, String data) throws SAXException { + try { + processingInstructionInternal(target, data); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle processingInstruction: " + ex.getMessage(), ex); + } + } + + protected abstract void processingInstructionInternal(String target, String data) throws XMLStreamException; + + public final void skippedEntity(String name) throws SAXException { + try { + skippedEntityInternal(name); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle skippedEntity: " + ex.getMessage(), ex); + } + } + + /** + * Convert a namespace URI and DOM or SAX qualified name to a {@code QName}. The qualified name can have the form + * {@code prefix:localname} or {@code localName}. + * + * @param namespaceUri the namespace URI + * @param qualifiedName the qualified name + * @return a QName + */ + protected QName toQName(String namespaceUri, String qualifiedName) { + int idx = qualifiedName.indexOf(':'); + if (idx == -1) { + return new QName(namespaceUri, qualifiedName); + } + else { + String prefix = qualifiedName.substring(0, idx); + String localPart = qualifiedName.substring(idx + 1); + return new QName(namespaceUri, localPart, prefix); + } + } + + protected abstract void skippedEntityInternal(String name) throws XMLStreamException; +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxXMLReader.java new file mode 100644 index 00000000..feeefd6e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxXMLReader.java @@ -0,0 +1,237 @@ +/* + * Copyright 2002-2012 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.util.xml; + +import java.util.LinkedHashMap; +import java.util.Map; +import javax.xml.namespace.QName; +import javax.xml.stream.Location; +import javax.xml.stream.XMLStreamException; + +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; +import org.xml.sax.SAXParseException; + +import org.springframework.util.StringUtils; + +/** + * Abstract base class for SAX {@code XMLReader} implementations that use StAX as a basis. + * + * @author Arjen Poutsma + * @since 3.0 + * @see #setContentHandler(org.xml.sax.ContentHandler) + * @see #setDTDHandler(org.xml.sax.DTDHandler) + * @see #setEntityResolver(org.xml.sax.EntityResolver) + * @see #setErrorHandler(org.xml.sax.ErrorHandler) + */ +abstract class AbstractStaxXMLReader extends AbstractXMLReader { + + private static final String NAMESPACES_FEATURE_NAME = "http://xml.org/sax/features/namespaces"; + + private static final String NAMESPACE_PREFIXES_FEATURE_NAME = "http://xml.org/sax/features/namespace-prefixes"; + + private static final String IS_STANDALONE_FEATURE_NAME = "http://xml.org/sax/features/is-standalone"; + + + private boolean namespacesFeature = true; + + private boolean namespacePrefixesFeature = false; + + private Boolean isStandalone; + + private final Map<String, String> namespaces = new LinkedHashMap<String, String>(); + + @Override + public boolean getFeature(String name) throws SAXNotRecognizedException, SAXNotSupportedException { + if (NAMESPACES_FEATURE_NAME.equals(name)) { + return this.namespacesFeature; + } + else if (NAMESPACE_PREFIXES_FEATURE_NAME.equals(name)) { + return this.namespacePrefixesFeature; + } + else if (IS_STANDALONE_FEATURE_NAME.equals(name)) { + if (this.isStandalone != null) { + return this.isStandalone; + } + else { + throw new SAXNotSupportedException("startDocument() callback not completed yet"); + } + } + else { + return super.getFeature(name); + } + } + + @Override + public void setFeature(String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { + if (NAMESPACES_FEATURE_NAME.equals(name)) { + this.namespacesFeature = value; + } + else if (NAMESPACE_PREFIXES_FEATURE_NAME.equals(name)) { + this.namespacePrefixesFeature = value; + } + else { + super.setFeature(name, value); + } + } + + protected void setStandalone(boolean standalone) { + this.isStandalone = standalone; + } + + /** + * Indicates whether the SAX feature {@code http://xml.org/sax/features/namespaces} is turned on. + */ + protected boolean hasNamespacesFeature() { + return this.namespacesFeature; + } + + /** + * Indicates whether the SAX feature {@code http://xml.org/sax/features/namespaces-prefixes} is turned on. + */ + protected boolean hasNamespacePrefixesFeature() { + return this.namespacePrefixesFeature; + } + + /** + * Convert a {@code QName} to a qualified name, as used by DOM and SAX. + * The returned string has a format of {@code prefix:localName} if the + * prefix is set, or just {@code localName} if not. + * @param qName the {@code QName} + * @return the qualified name + */ + protected String toQualifiedName(QName qName) { + String prefix = qName.getPrefix(); + if (!StringUtils.hasLength(prefix)) { + return qName.getLocalPart(); + } + else { + return prefix + ":" + qName.getLocalPart(); + } + } + + + /** + * Parse the StAX XML reader passed at construction-time. + * <p><b>NOTE:</b>: The given {@code InputSource} is not read, but ignored. + * @param ignored is ignored + * @throws SAXException a SAX exception, possibly wrapping a {@code XMLStreamException} + */ + public final void parse(InputSource ignored) throws SAXException { + parse(); + } + + /** + * Parse the StAX XML reader passed at construction-time. + * <p><b>NOTE:</b>: The given system identifier is not read, but ignored. + * @param ignored is ignored + * @throws SAXException A SAX exception, possibly wrapping a {@code XMLStreamException} + */ + public final void parse(String ignored) throws SAXException { + parse(); + } + + private void parse() throws SAXException { + try { + parseInternal(); + } + catch (XMLStreamException ex) { + Locator locator = null; + if (ex.getLocation() != null) { + locator = new StaxLocator(ex.getLocation()); + } + SAXParseException saxException = new SAXParseException(ex.getMessage(), locator, ex); + if (getErrorHandler() != null) { + getErrorHandler().fatalError(saxException); + } + else { + throw saxException; + } + } + } + + /** + * Template-method that parses the StAX reader passed at construction-time. + */ + protected abstract void parseInternal() throws SAXException, XMLStreamException; + + /** + * Starts the prefix mapping for the given prefix. + * @see org.xml.sax.ContentHandler#startPrefixMapping(String, String) + */ + protected void startPrefixMapping(String prefix, String namespace) throws SAXException { + if (getContentHandler() != null) { + if (prefix == null) { + prefix = ""; + } + if (!StringUtils.hasLength(namespace)) { + return; + } + if (!namespace.equals(namespaces.get(prefix))) { + getContentHandler().startPrefixMapping(prefix, namespace); + namespaces.put(prefix, namespace); + } + } + } + + /** + * Ends the prefix mapping for the given prefix. + * @see org.xml.sax.ContentHandler#endPrefixMapping(String) + */ + protected void endPrefixMapping(String prefix) throws SAXException { + if (getContentHandler() != null) { + if (namespaces.containsKey(prefix)) { + getContentHandler().endPrefixMapping(prefix); + namespaces.remove(prefix); + } + } + } + + /** + * Implementation of the {@code Locator} interface that is based on a StAX {@code Location}. + * @see Locator + * @see Location + */ + private static class StaxLocator implements Locator { + + private Location location; + + protected StaxLocator(Location location) { + this.location = location; + } + + public String getPublicId() { + return location.getPublicId(); + } + + public String getSystemId() { + return location.getSystemId(); + } + + public int getLineNumber() { + return location.getLineNumber(); + } + + public int getColumnNumber() { + return location.getColumnNumber(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLReader.java new file mode 100644 index 00000000..300f9ff2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLReader.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2012 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.util.xml; + +import org.xml.sax.ContentHandler; +import org.xml.sax.DTDHandler; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; +import org.xml.sax.XMLReader; +import org.xml.sax.ext.LexicalHandler; + +/** + * Abstract base class for SAX {@code XMLReader} implementations. Contains properties as defined in {@link + * XMLReader}, and does not recognize any features. + * + * @author Arjen Poutsma + * @see #setContentHandler(org.xml.sax.ContentHandler) + * @see #setDTDHandler(org.xml.sax.DTDHandler) + * @see #setEntityResolver(org.xml.sax.EntityResolver) + * @see #setErrorHandler(org.xml.sax.ErrorHandler) + * @since 3.0 + */ +abstract class AbstractXMLReader implements XMLReader { + + private DTDHandler dtdHandler; + + private ContentHandler contentHandler; + + private EntityResolver entityResolver; + + private ErrorHandler errorHandler; + + private LexicalHandler lexicalHandler; + + public ContentHandler getContentHandler() { + return contentHandler; + } + + public void setContentHandler(ContentHandler contentHandler) { + this.contentHandler = contentHandler; + } + + public void setDTDHandler(DTDHandler dtdHandler) { + this.dtdHandler = dtdHandler; + } + + public DTDHandler getDTDHandler() { + return dtdHandler; + } + + public EntityResolver getEntityResolver() { + return entityResolver; + } + + public void setEntityResolver(EntityResolver entityResolver) { + this.entityResolver = entityResolver; + } + + public ErrorHandler getErrorHandler() { + return errorHandler; + } + + public void setErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + protected LexicalHandler getLexicalHandler() { + return lexicalHandler; + } + + /** + * Throws a {@code SAXNotRecognizedException} exception. + * + * @throws org.xml.sax.SAXNotRecognizedException + * always + */ + public boolean getFeature(String name) throws SAXNotRecognizedException, SAXNotSupportedException { + throw new SAXNotRecognizedException(name); + } + + /** + * Throws a {@code SAXNotRecognizedException} exception. + * + * @throws SAXNotRecognizedException always + */ + public void setFeature(String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { + throw new SAXNotRecognizedException(name); + } + + /** + * Throws a {@code SAXNotRecognizedException} exception when the given property does not signify a lexical + * handler. The property name for a lexical handler is {@code http://xml.org/sax/properties/lexical-handler}. + */ + public Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException { + if ("http://xml.org/sax/properties/lexical-handler".equals(name)) { + return lexicalHandler; + } + else { + throw new SAXNotRecognizedException(name); + } + } + + /** + * Throws a {@code SAXNotRecognizedException} exception when the given property does not signify a lexical + * handler. The property name for a lexical handler is {@code http://xml.org/sax/properties/lexical-handler}. + */ + public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException { + if ("http://xml.org/sax/properties/lexical-handler".equals(name)) { + lexicalHandler = (LexicalHandler) value; + } + else { + throw new SAXNotRecognizedException(name); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLStreamReader.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLStreamReader.java new file mode 100644 index 00000000..e8c8a643 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLStreamReader.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2012 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.util.xml; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.springframework.util.Assert; + +/** + * Abstract base class for {@code XMLStreamReader}s. + * + * @author Arjen Poutsma + * @since 3.0 + */ +abstract class AbstractXMLStreamReader implements XMLStreamReader { + + public String getElementText() throws XMLStreamException { + if (getEventType() != XMLStreamConstants.START_ELEMENT) { + throw new XMLStreamException("parser must be on START_ELEMENT to read next text", getLocation()); + } + int eventType = next(); + StringBuilder builder = new StringBuilder(); + while (eventType != XMLStreamConstants.END_ELEMENT) { + if (eventType == XMLStreamConstants.CHARACTERS || eventType == XMLStreamConstants.CDATA || + eventType == XMLStreamConstants.SPACE || eventType == XMLStreamConstants.ENTITY_REFERENCE) { + builder.append(getText()); + } + else if (eventType == XMLStreamConstants.PROCESSING_INSTRUCTION || + eventType == XMLStreamConstants.COMMENT) { + // skipping + } + else if (eventType == XMLStreamConstants.END_DOCUMENT) { + throw new XMLStreamException("unexpected end of document when reading element text content", + getLocation()); + } + else if (eventType == XMLStreamConstants.START_ELEMENT) { + throw new XMLStreamException("element text content may not contain START_ELEMENT", getLocation()); + } + else { + throw new XMLStreamException("Unexpected event type " + eventType, getLocation()); + } + eventType = next(); + } + return builder.toString(); + } + + public String getAttributeLocalName(int index) { + return getAttributeName(index).getLocalPart(); + } + + public String getAttributeNamespace(int index) { + return getAttributeName(index).getNamespaceURI(); + } + + public String getAttributePrefix(int index) { + return getAttributeName(index).getPrefix(); + } + + public String getNamespaceURI() { + int eventType = getEventType(); + if (eventType == XMLStreamConstants.START_ELEMENT || eventType == XMLStreamConstants.END_ELEMENT) { + return getName().getNamespaceURI(); + } + else { + throw new IllegalStateException("parser must be on START_ELEMENT or END_ELEMENT state"); + } + } + + public String getNamespaceURI(String prefix) { + Assert.notNull(prefix, "No prefix given"); + return getNamespaceContext().getNamespaceURI(prefix); + } + + public boolean hasText() { + int eventType = getEventType(); + return eventType == XMLStreamConstants.SPACE || eventType == XMLStreamConstants.CHARACTERS || + eventType == XMLStreamConstants.COMMENT || eventType == XMLStreamConstants.CDATA || + eventType == XMLStreamConstants.ENTITY_REFERENCE; + } + + public String getPrefix() { + int eventType = getEventType(); + if (eventType == XMLStreamConstants.START_ELEMENT || eventType == XMLStreamConstants.END_ELEMENT) { + return getName().getPrefix(); + } + else { + throw new IllegalStateException("parser must be on START_ELEMENT or END_ELEMENT state"); + } + } + + public boolean hasName() { + int eventType = getEventType(); + return eventType == XMLStreamConstants.START_ELEMENT || eventType == XMLStreamConstants.END_ELEMENT; + } + + public boolean isWhiteSpace() { + return getEventType() == XMLStreamConstants.SPACE; + } + + public boolean isStartElement() { + return getEventType() == XMLStreamConstants.START_ELEMENT; + } + + public boolean isEndElement() { + return getEventType() == XMLStreamConstants.END_ELEMENT; + } + + public boolean isCharacters() { + return getEventType() == XMLStreamConstants.CHARACTERS; + } + + public int nextTag() throws XMLStreamException { + int eventType = next(); + while (eventType == XMLStreamConstants.CHARACTERS && isWhiteSpace() || + eventType == XMLStreamConstants.CDATA && isWhiteSpace() || eventType == XMLStreamConstants.SPACE || + eventType == XMLStreamConstants.PROCESSING_INSTRUCTION || eventType == XMLStreamConstants.COMMENT) { + eventType = next(); + } + if (eventType != XMLStreamConstants.START_ELEMENT && eventType != XMLStreamConstants.END_ELEMENT) { + throw new XMLStreamException("expected start or end tag", getLocation()); + } + return eventType; + } + + public void require(int expectedType, String namespaceURI, String localName) throws XMLStreamException { + int eventType = getEventType(); + if (eventType != expectedType) { + throw new XMLStreamException("Expected [" + expectedType + "] but read [" + eventType + "]"); + } + } + + public String getAttributeValue(String namespaceURI, String localName) { + for (int i = 0; i < getAttributeCount(); i++) { + QName name = getAttributeName(i); + if (name.getLocalPart().equals(localName) && + (namespaceURI == null || name.getNamespaceURI().equals(namespaceURI))) { + return getAttributeValue(i); + } + } + return null; + } + + public boolean hasNext() throws XMLStreamException { + return getEventType() != END_DOCUMENT; + } + + public String getLocalName() { + return getName().getLocalPart(); + } + + public char[] getTextCharacters() { + return getText().toCharArray(); + } + + public int getTextCharacters(int sourceStart, char[] target, int targetStart, int length) + throws XMLStreamException { + char[] source = getTextCharacters(); + length = Math.min(length, source.length); + System.arraycopy(source, sourceStart, target, targetStart, length); + return length; + } + + public int getTextLength() { + return getText().length(); + } +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/DomContentHandler.java b/spring-core/src/main/java/org/springframework/util/xml/DomContentHandler.java new file mode 100644 index 00000000..534cb007 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/DomContentHandler.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2012 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.util.xml; + +import java.util.ArrayList; +import java.util.List; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.ProcessingInstruction; +import org.w3c.dom.Text; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; + +import org.springframework.util.Assert; + +/** + * SAX {@code ContentHandler} that transforms callback calls to DOM {@code Node}s. + * + * @author Arjen Poutsma + * @see org.w3c.dom.Node + * @since 3.0 + */ +class DomContentHandler implements ContentHandler { + + private final Document document; + + private final List<Element> elements = new ArrayList<Element>(); + + private final Node node; + + /** + * Creates a new instance of the {@code DomContentHandler} with the given node. + * + * @param node the node to publish events to + */ + DomContentHandler(Node node) { + Assert.notNull(node, "node must not be null"); + this.node = node; + if (node instanceof Document) { + document = (Document) node; + } + else { + document = node.getOwnerDocument(); + } + Assert.notNull(document, "document must not be null"); + } + + private Node getParent() { + if (!elements.isEmpty()) { + return elements.get(elements.size() - 1); + } + else { + return node; + } + } + + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + Node parent = getParent(); + Element element = document.createElementNS(uri, qName); + for (int i = 0; i < attributes.getLength(); i++) { + String attrUri = attributes.getURI(i); + String attrQname = attributes.getQName(i); + String value = attributes.getValue(i); + if (!attrQname.startsWith("xmlns")) { + element.setAttributeNS(attrUri, attrQname, value); + } + } + element = (Element) parent.appendChild(element); + elements.add(element); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + elements.remove(elements.size() - 1); + } + + public void characters(char ch[], int start, int length) throws SAXException { + String data = new String(ch, start, length); + Node parent = getParent(); + Node lastChild = parent.getLastChild(); + if (lastChild != null && lastChild.getNodeType() == Node.TEXT_NODE) { + ((Text) lastChild).appendData(data); + } + else { + Text text = document.createTextNode(data); + parent.appendChild(text); + } + } + + public void processingInstruction(String target, String data) throws SAXException { + Node parent = getParent(); + ProcessingInstruction pi = document.createProcessingInstruction(target, data); + parent.appendChild(pi); + } + + /* + * Unsupported + */ + + public void setDocumentLocator(Locator locator) { + } + + public void startDocument() throws SAXException { + } + + public void endDocument() throws SAXException { + } + + public void startPrefixMapping(String prefix, String uri) throws SAXException { + } + + public void endPrefixMapping(String prefix) throws SAXException { + } + + public void ignorableWhitespace(char ch[], int start, int length) throws SAXException { + } + + public void skippedEntity(String name) throws SAXException { + } +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/DomUtils.java b/spring-core/src/main/java/org/springframework/util/xml/DomUtils.java new file mode 100644 index 00000000..61c6271d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/DomUtils.java @@ -0,0 +1,192 @@ +/* + * Copyright 2002-2013 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.util.xml; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.w3c.dom.CharacterData; +import org.w3c.dom.Comment; +import org.w3c.dom.Element; +import org.w3c.dom.EntityReference; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.ContentHandler; + +import org.springframework.util.Assert; + +/** + * Convenience methods for working with the DOM API, + * in particular for working with DOM Nodes and DOM Elements. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Costin Leau + * @author Arjen Poutsma + * @author Luke Taylor + * @since 1.2 + * @see org.w3c.dom.Node + * @see org.w3c.dom.Element + */ +public abstract class DomUtils { + + /** + * Retrieves all child elements of the given DOM element that match any of the given element names. + * Only looks at the direct child level of the given element; do not go into further depth + * (in contrast to the DOM API's {@code getElementsByTagName} method). + * @param ele the DOM element to analyze + * @param childEleNames the child element names to look for + * @return a List of child {@code org.w3c.dom.Element} instances + * @see org.w3c.dom.Element + * @see org.w3c.dom.Element#getElementsByTagName + */ + public static List<Element> getChildElementsByTagName(Element ele, String... childEleNames) { + Assert.notNull(ele, "Element must not be null"); + Assert.notNull(childEleNames, "Element names collection must not be null"); + List<String> childEleNameList = Arrays.asList(childEleNames); + NodeList nl = ele.getChildNodes(); + List<Element> childEles = new ArrayList<Element>(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element && nodeNameMatch(node, childEleNameList)) { + childEles.add((Element) node); + } + } + return childEles; + } + + /** + * Retrieves all child elements of the given DOM element that match the given element name. + * Only look at the direct child level of the given element; do not go into further depth + * (in contrast to the DOM API's {@code getElementsByTagName} method). + * @param ele the DOM element to analyze + * @param childEleName the child element name to look for + * @return a List of child {@code org.w3c.dom.Element} instances + * @see org.w3c.dom.Element + * @see org.w3c.dom.Element#getElementsByTagName + */ + public static List<Element> getChildElementsByTagName(Element ele, String childEleName) { + return getChildElementsByTagName(ele, new String[] {childEleName}); + } + + /** + * Utility method that returns the first child element identified by its name. + * @param ele the DOM element to analyze + * @param childEleName the child element name to look for + * @return the {@code org.w3c.dom.Element} instance, or {@code null} if none found + */ + public static Element getChildElementByTagName(Element ele, String childEleName) { + Assert.notNull(ele, "Element must not be null"); + Assert.notNull(childEleName, "Element name must not be null"); + NodeList nl = ele.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element && nodeNameMatch(node, childEleName)) { + return (Element) node; + } + } + return null; + } + + /** + * Utility method that returns the first child element value identified by its name. + * @param ele the DOM element to analyze + * @param childEleName the child element name to look for + * @return the extracted text value, or {@code null} if no child element found + */ + public static String getChildElementValueByTagName(Element ele, String childEleName) { + Element child = getChildElementByTagName(ele, childEleName); + return (child != null ? getTextValue(child) : null); + } + + /** + * Retrieves all child elements of the given DOM element + * @param ele the DOM element to analyze + * @return a List of child {@code org.w3c.dom.Element} instances + */ + public static List<Element> getChildElements(Element ele) { + Assert.notNull(ele, "Element must not be null"); + NodeList nl = ele.getChildNodes(); + List<Element> childEles = new ArrayList<Element>(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element) { + childEles.add((Element) node); + } + } + return childEles; + } + + /** + * Extracts the text value from the given DOM element, ignoring XML comments. + * <p>Appends all CharacterData nodes and EntityReference nodes into a single + * String value, excluding Comment nodes. Only exposes actual user-specified + * text, no default values of any kind. + * @see CharacterData + * @see EntityReference + * @see Comment + */ + public static String getTextValue(Element valueEle) { + Assert.notNull(valueEle, "Element must not be null"); + StringBuilder sb = new StringBuilder(); + NodeList nl = valueEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node item = nl.item(i); + if ((item instanceof CharacterData && !(item instanceof Comment)) || item instanceof EntityReference) { + sb.append(item.getNodeValue()); + } + } + return sb.toString(); + } + + /** + * Namespace-aware equals comparison. Returns {@code true} if either + * {@link Node#getLocalName} or {@link Node#getNodeName} equals + * {@code desiredName}, otherwise returns {@code false}. + */ + public static boolean nodeNameEquals(Node node, String desiredName) { + Assert.notNull(node, "Node must not be null"); + Assert.notNull(desiredName, "Desired name must not be null"); + return nodeNameMatch(node, desiredName); + } + + /** + * Returns a SAX {@code ContentHandler} that transforms callback calls to DOM {@code Node}s. + * @param node the node to publish events to + * @return the content handler + */ + public static ContentHandler createContentHandler(Node node) { + return new DomContentHandler(node); + } + + /** + * Matches the given node's name and local name against the given desired name. + */ + private static boolean nodeNameMatch(Node node, String desiredName) { + return (desiredName.equals(node.getNodeName()) || desiredName.equals(node.getLocalName())); + } + + /** + * Matches the given node's name and local name against the given desired names. + */ + private static boolean nodeNameMatch(Node node, Collection<?> desiredNames) { + return (desiredNames.contains(node.getNodeName()) || desiredNames.contains(node.getLocalName())); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java b/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java new file mode 100644 index 00000000..c0d966d6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2012 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.util.xml; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; + +import org.springframework.util.Assert; + +/** + * Simple {@code javax.xml.namespace.NamespaceContext} implementation. Follows the standard + * {@code NamespaceContext} contract, and is loadable via a {@code java.util.Map} or + * {@code java.util.Properties} object + * + * @author Arjen Poutsma + * @since 3.0 + */ +public class SimpleNamespaceContext implements NamespaceContext { + + private Map<String, String> prefixToNamespaceUri = new HashMap<String, String>(); + + private Map<String, List<String>> namespaceUriToPrefixes = new HashMap<String, List<String>>(); + + private String defaultNamespaceUri = ""; + + public String getNamespaceURI(String prefix) { + Assert.notNull(prefix, "prefix is null"); + if (XMLConstants.XML_NS_PREFIX.equals(prefix)) { + return XMLConstants.XML_NS_URI; + } + else if (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix)) { + return XMLConstants.XMLNS_ATTRIBUTE_NS_URI; + } + else if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) { + return defaultNamespaceUri; + } + else if (prefixToNamespaceUri.containsKey(prefix)) { + return prefixToNamespaceUri.get(prefix); + } + return ""; + } + + public String getPrefix(String namespaceUri) { + List prefixes = getPrefixesInternal(namespaceUri); + return prefixes.isEmpty() ? null : (String) prefixes.get(0); + } + + public Iterator getPrefixes(String namespaceUri) { + return getPrefixesInternal(namespaceUri).iterator(); + } + + /** + * Sets the bindings for this namespace context. The supplied map must consist of string key value pairs. + * + * @param bindings the bindings + */ + public void setBindings(Map<String, String> bindings) { + for (Map.Entry<String, String> entry : bindings.entrySet()) { + bindNamespaceUri(entry.getKey(), entry.getValue()); + } + } + + /** + * Binds the given namespace as default namespace. + * + * @param namespaceUri the namespace uri + */ + public void bindDefaultNamespaceUri(String namespaceUri) { + bindNamespaceUri(XMLConstants.DEFAULT_NS_PREFIX, namespaceUri); + } + + /** + * Binds the given prefix to the given namespace. + * + * @param prefix the namespace prefix + * @param namespaceUri the namespace uri + */ + public void bindNamespaceUri(String prefix, String namespaceUri) { + Assert.notNull(prefix, "No prefix given"); + Assert.notNull(namespaceUri, "No namespaceUri given"); + if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) { + defaultNamespaceUri = namespaceUri; + } + else { + prefixToNamespaceUri.put(prefix, namespaceUri); + getPrefixesInternal(namespaceUri).add(prefix); + } + } + + /** Removes all declared prefixes. */ + public void clear() { + prefixToNamespaceUri.clear(); + } + + /** + * Returns all declared prefixes. + * + * @return the declared prefixes + */ + public Iterator<String> getBoundPrefixes() { + return prefixToNamespaceUri.keySet().iterator(); + } + + private List<String> getPrefixesInternal(String namespaceUri) { + if (defaultNamespaceUri.equals(namespaceUri)) { + return Collections.singletonList(XMLConstants.DEFAULT_NS_PREFIX); + } + else if (XMLConstants.XML_NS_URI.equals(namespaceUri)) { + return Collections.singletonList(XMLConstants.XML_NS_PREFIX); + } + else if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceUri)) { + return Collections.singletonList(XMLConstants.XMLNS_ATTRIBUTE); + } + else { + List<String> list = namespaceUriToPrefixes.get(namespaceUri); + if (list == null) { + list = new ArrayList<String>(); + namespaceUriToPrefixes.put(namespaceUri, list); + } + return list; + } + } + + /** + * Removes the given prefix from this context. + * + * @param prefix the prefix to be removed + */ + public void removeBinding(String prefix) { + if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) { + defaultNamespaceUri = ""; + } + else { + String namespaceUri = prefixToNamespaceUri.remove(prefix); + List prefixes = getPrefixesInternal(namespaceUri); + prefixes.remove(prefix); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/SimpleSaxErrorHandler.java b/spring-core/src/main/java/org/springframework/util/xml/SimpleSaxErrorHandler.java new file mode 100644 index 00000000..ed06ed8c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/SimpleSaxErrorHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2012 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.util.xml; + +import org.apache.commons.logging.Log; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Simple {@code org.xml.sax.ErrorHandler} implementation: + * logs warnings using the given Commons Logging logger instance, + * and rethrows errors to discontinue the XML transformation. + * + * @author Juergen Hoeller + * @since 1.2 + */ +public class SimpleSaxErrorHandler implements ErrorHandler { + + private final Log logger; + + + /** + * Create a new SimpleSaxErrorHandler for the given + * Commons Logging logger instance. + */ + public SimpleSaxErrorHandler(Log logger) { + this.logger = logger; + } + + + public void warning(SAXParseException ex) throws SAXException { + logger.warn("Ignored XML validation warning", ex); + } + + public void error(SAXParseException ex) throws SAXException { + throw ex; + } + + public void fatalError(SAXParseException ex) throws SAXException { + throw ex; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/SimpleTransformErrorListener.java b/spring-core/src/main/java/org/springframework/util/xml/SimpleTransformErrorListener.java new file mode 100644 index 00000000..7fd5dffa --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/SimpleTransformErrorListener.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2012 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.util.xml; + +import javax.xml.transform.ErrorListener; +import javax.xml.transform.TransformerException; + +import org.apache.commons.logging.Log; + +/** + * Simple {@code javax.xml.transform.ErrorListener} implementation: + * logs warnings using the given Commons Logging logger instance, + * and rethrows errors to discontinue the XML transformation. + * + * @author Juergen Hoeller + * @since 1.2 + */ +public class SimpleTransformErrorListener implements ErrorListener { + + private final Log logger; + + + /** + * Create a new SimpleTransformErrorListener for the given + * Commons Logging logger instance. + */ + public SimpleTransformErrorListener(Log logger) { + this.logger = logger; + } + + + public void warning(TransformerException ex) throws TransformerException { + logger.warn("XSLT transformation warning", ex); + } + + public void error(TransformerException ex) throws TransformerException { + logger.error("XSLT transformation error", ex); + } + + public void fatalError(TransformerException ex) throws TransformerException { + throw ex; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxEventContentHandler.java b/spring-core/src/main/java/org/springframework/util/xml/StaxEventContentHandler.java new file mode 100644 index 00000000..686ff1a6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxEventContentHandler.java @@ -0,0 +1,192 @@ +/* + * Copyright 2002-2012 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.util.xml; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; +import javax.xml.stream.Location; +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.Namespace; +import javax.xml.stream.events.XMLEvent; +import javax.xml.stream.util.XMLEventConsumer; + +import org.xml.sax.Attributes; +import org.xml.sax.Locator; + +import org.springframework.util.StringUtils; + +/** + * SAX {@code ContentHandler} that transforms callback calls to {@code XMLEvent}s + * and writes them to a {@code XMLEventConsumer}. + * + * @author Arjen Poutsma + * @since 3.0 + * @see XMLEvent + * @see XMLEventConsumer + */ +class StaxEventContentHandler extends AbstractStaxContentHandler { + + private final XMLEventFactory eventFactory; + + private final XMLEventConsumer eventConsumer; + + + /** + * Construct a new instance of the {@code StaxEventContentHandler} that writes to the given + * {@code XMLEventConsumer}. A default {@code XMLEventFactory} will be created. + * @param consumer the consumer to write events to + */ + StaxEventContentHandler(XMLEventConsumer consumer) { + this.eventFactory = XMLEventFactory.newInstance(); + this.eventConsumer = consumer; + } + + /** + * Construct a new instance of the {@code StaxEventContentHandler} that uses the given + * event factory to create events and writes to the given {@code XMLEventConsumer}. + * @param consumer the consumer to write events to + * @param factory the factory used to create events + */ + StaxEventContentHandler(XMLEventConsumer consumer, XMLEventFactory factory) { + this.eventFactory = factory; + this.eventConsumer = consumer; + } + + public void setDocumentLocator(Locator locator) { + if (locator != null) { + this.eventFactory.setLocation(new LocatorLocationAdapter(locator)); + } + } + + @Override + protected void startDocumentInternal() throws XMLStreamException { + consumeEvent(this.eventFactory.createStartDocument()); + } + + @Override + protected void endDocumentInternal() throws XMLStreamException { + consumeEvent(this.eventFactory.createEndDocument()); + } + + @Override + protected void startElementInternal(QName name, Attributes atts, SimpleNamespaceContext namespaceContext) + throws XMLStreamException { + + List attributes = getAttributes(atts); + List namespaces = createNamespaces(namespaceContext); + consumeEvent(this.eventFactory.createStartElement(name, attributes.iterator(), + (namespaces != null ? namespaces.iterator() : null))); + } + + @Override + protected void endElementInternal(QName name, SimpleNamespaceContext namespaceContext) throws XMLStreamException { + List namespaces = createNamespaces(namespaceContext); + consumeEvent(this.eventFactory.createEndElement(name, namespaces != null ? namespaces.iterator() : null)); + } + + @Override + protected void charactersInternal(char[] ch, int start, int length) throws XMLStreamException { + consumeEvent(this.eventFactory.createCharacters(new String(ch, start, length))); + } + + @Override + protected void ignorableWhitespaceInternal(char[] ch, int start, int length) throws XMLStreamException { + consumeEvent(this.eventFactory.createIgnorableSpace(new String(ch, start, length))); + } + + @Override + protected void processingInstructionInternal(String target, String data) throws XMLStreamException { + consumeEvent(this.eventFactory.createProcessingInstruction(target, data)); + } + + private void consumeEvent(XMLEvent event) throws XMLStreamException { + this.eventConsumer.add(event); + } + + /** + * Create and return a list of {@code NameSpace} objects from the {@code NamespaceContext}. + */ + private List<Namespace> createNamespaces(SimpleNamespaceContext namespaceContext) { + if (namespaceContext == null) { + return null; + } + + List<Namespace> namespaces = new ArrayList<Namespace>(); + String defaultNamespaceUri = namespaceContext.getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX); + if (StringUtils.hasLength(defaultNamespaceUri)) { + namespaces.add(this.eventFactory.createNamespace(defaultNamespaceUri)); + } + for (Iterator iterator = namespaceContext.getBoundPrefixes(); iterator.hasNext();) { + String prefix = (String) iterator.next(); + String namespaceUri = namespaceContext.getNamespaceURI(prefix); + namespaces.add(this.eventFactory.createNamespace(prefix, namespaceUri)); + } + return namespaces; + } + + private List<Attribute> getAttributes(Attributes attributes) { + List<Attribute> list = new ArrayList<Attribute>(); + for (int i = 0; i < attributes.getLength(); i++) { + QName name = toQName(attributes.getURI(i), attributes.getQName(i)); + if (!("xmlns".equals(name.getLocalPart()) || "xmlns".equals(name.getPrefix()))) { + list.add(this.eventFactory.createAttribute(name, attributes.getValue(i))); + } + } + return list; + } + + /* No operation */ + @Override + protected void skippedEntityInternal(String name) throws XMLStreamException { + } + + + private static final class LocatorLocationAdapter implements Location { + + private final Locator locator; + + public LocatorLocationAdapter(Locator locator) { + this.locator = locator; + } + + public int getLineNumber() { + return this.locator.getLineNumber(); + } + + public int getColumnNumber() { + return this.locator.getColumnNumber(); + } + + public int getCharacterOffset() { + return -1; + } + + public String getPublicId() { + return this.locator.getPublicId(); + } + + public String getSystemId() { + return this.locator.getSystemId(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java new file mode 100644 index 00000000..fd43354a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java @@ -0,0 +1,338 @@ +/* + * Copyright 2002-2013 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.util.xml; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.xml.namespace.QName; +import javax.xml.stream.Location; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.Characters; +import javax.xml.stream.events.Comment; +import javax.xml.stream.events.DTD; +import javax.xml.stream.events.EndElement; +import javax.xml.stream.events.EntityDeclaration; +import javax.xml.stream.events.EntityReference; +import javax.xml.stream.events.Namespace; +import javax.xml.stream.events.NotationDeclaration; +import javax.xml.stream.events.ProcessingInstruction; +import javax.xml.stream.events.StartDocument; +import javax.xml.stream.events.StartElement; +import javax.xml.stream.events.XMLEvent; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.ext.Locator2; +import org.xml.sax.helpers.AttributesImpl; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * SAX {@code XMLReader} that reads from a StAX {@code XMLEventReader}. Consumes {@code XMLEvents} from + * an {@code XMLEventReader}, and calls the corresponding methods on the SAX callback interfaces. + * + * @author Arjen Poutsma + * @since 3.0 + * @see XMLEventReader + * @see #setContentHandler(org.xml.sax.ContentHandler) + * @see #setDTDHandler(org.xml.sax.DTDHandler) + * @see #setEntityResolver(org.xml.sax.EntityResolver) + * @see #setErrorHandler(org.xml.sax.ErrorHandler) + */ +class StaxEventXMLReader extends AbstractStaxXMLReader { + + private static final String DEFAULT_XML_VERSION = "1.0"; + + private final XMLEventReader reader; + + private final Map<String, String> namespaces = new LinkedHashMap<String, String>(); + + private String xmlVersion = DEFAULT_XML_VERSION; + + private String encoding; + + + /** + * Constructs a new instance of the {@code StaxEventXmlReader} that reads from the given + * {@code XMLEventReader}. The supplied event reader must be in {@code XMLStreamConstants.START_DOCUMENT} or + * {@code XMLStreamConstants.START_ELEMENT} state. + * @param reader the {@code XMLEventReader} to read from + * @throws IllegalStateException if the reader is not at the start of a document or element + */ + StaxEventXMLReader(XMLEventReader reader) { + Assert.notNull(reader, "'reader' must not be null"); + try { + XMLEvent event = reader.peek(); + if (event != null && !(event.isStartDocument() || event.isStartElement())) { + throw new IllegalStateException("XMLEventReader not at start of document or element"); + } + } + catch (XMLStreamException ex) { + throw new IllegalStateException("Could not read first element: " + ex.getMessage()); + } + this.reader = reader; + } + + + @Override + protected void parseInternal() throws SAXException, XMLStreamException { + boolean documentStarted = false; + boolean documentEnded = false; + int elementDepth = 0; + while (this.reader.hasNext() && elementDepth >= 0) { + XMLEvent event = this.reader.nextEvent(); + if (!event.isStartDocument() && !event.isEndDocument() && !documentStarted) { + handleStartDocument(event); + documentStarted = true; + } + switch (event.getEventType()) { + case XMLStreamConstants.START_DOCUMENT: + handleStartDocument(event); + documentStarted = true; + break; + case XMLStreamConstants.START_ELEMENT: + elementDepth++; + handleStartElement(event.asStartElement()); + break; + case XMLStreamConstants.END_ELEMENT: + elementDepth--; + if (elementDepth >= 0) { + handleEndElement(event.asEndElement()); + } + break; + case XMLStreamConstants.PROCESSING_INSTRUCTION: + handleProcessingInstruction((ProcessingInstruction) event); + break; + case XMLStreamConstants.CHARACTERS: + case XMLStreamConstants.SPACE: + case XMLStreamConstants.CDATA: + handleCharacters(event.asCharacters()); + break; + case XMLStreamConstants.END_DOCUMENT: + handleEndDocument(); + documentEnded = true; + break; + case XMLStreamConstants.NOTATION_DECLARATION: + handleNotationDeclaration((NotationDeclaration) event); + break; + case XMLStreamConstants.ENTITY_DECLARATION: + handleEntityDeclaration((EntityDeclaration) event); + break; + case XMLStreamConstants.COMMENT: + handleComment((Comment) event); + break; + case XMLStreamConstants.DTD: + handleDtd((DTD) event); + break; + case XMLStreamConstants.ENTITY_REFERENCE: + handleEntityReference((EntityReference) event); + break; + } + } + if (documentStarted && !documentEnded) { + handleEndDocument(); + } + + } + + private void handleStartDocument(final XMLEvent event) throws SAXException { + if (event.isStartDocument()) { + StartDocument startDocument = (StartDocument) event; + String xmlVersion = startDocument.getVersion(); + if (StringUtils.hasLength(xmlVersion)) { + this.xmlVersion = xmlVersion; + } + if (startDocument.encodingSet()) { + this.encoding = startDocument.getCharacterEncodingScheme(); + } + } + if (getContentHandler() != null) { + final Location location = event.getLocation(); + getContentHandler().setDocumentLocator(new Locator2() { + public int getColumnNumber() { + return (location != null ? location.getColumnNumber() : -1); + } + public int getLineNumber() { + return (location != null ? location.getLineNumber() : -1); + } + public String getPublicId() { + return (location != null ? location.getPublicId() : null); + } + public String getSystemId() { + return (location != null ? location.getSystemId() : null); + } + public String getXMLVersion() { + return xmlVersion; + } + public String getEncoding() { + return encoding; + } + }); + getContentHandler().startDocument(); + } + } + + private void handleStartElement(StartElement startElement) throws SAXException { + if (getContentHandler() != null) { + QName qName = startElement.getName(); + if (hasNamespacesFeature()) { + for (Iterator i = startElement.getNamespaces(); i.hasNext();) { + Namespace namespace = (Namespace) i.next(); + startPrefixMapping(namespace.getPrefix(), namespace.getNamespaceURI()); + } + for (Iterator i = startElement.getAttributes(); i.hasNext();){ + Attribute attribute = (Attribute) i.next(); + QName attributeName = attribute.getName(); + startPrefixMapping(attributeName.getPrefix(), attributeName.getNamespaceURI()); + } + + getContentHandler().startElement(qName.getNamespaceURI(), qName.getLocalPart(), toQualifiedName(qName), + getAttributes(startElement)); + } + else { + getContentHandler().startElement("", "", toQualifiedName(qName), getAttributes(startElement)); + } + } + } + + private void handleCharacters(Characters characters) throws SAXException { + char[] data = characters.getData().toCharArray(); + if (getContentHandler() != null && characters.isIgnorableWhiteSpace()) { + getContentHandler().ignorableWhitespace(data, 0, data.length); + return; + } + if (characters.isCData() && getLexicalHandler() != null) { + getLexicalHandler().startCDATA(); + } + if (getContentHandler() != null) { + getContentHandler().characters(data, 0, data.length); + } + if (characters.isCData() && getLexicalHandler() != null) { + getLexicalHandler().endCDATA(); + } + } + + private void handleEndElement(EndElement endElement) throws SAXException { + if (getContentHandler() != null) { + QName qName = endElement.getName(); + if (hasNamespacesFeature()) { + getContentHandler().endElement(qName.getNamespaceURI(), qName.getLocalPart(), toQualifiedName(qName)); + for (Iterator i = endElement.getNamespaces(); i.hasNext();) { + Namespace namespace = (Namespace) i.next(); + endPrefixMapping(namespace.getPrefix()); + } + } + else { + getContentHandler().endElement("", "", toQualifiedName(qName)); + } + + } + } + + private void handleEndDocument() throws SAXException { + if (getContentHandler() != null) { + getContentHandler().endDocument(); + } + } + + private void handleNotationDeclaration(NotationDeclaration declaration) throws SAXException { + if (getDTDHandler() != null) { + getDTDHandler().notationDecl(declaration.getName(), declaration.getPublicId(), declaration.getSystemId()); + } + } + + private void handleEntityDeclaration(EntityDeclaration entityDeclaration) throws SAXException { + if (getDTDHandler() != null) { + getDTDHandler().unparsedEntityDecl(entityDeclaration.getName(), entityDeclaration.getPublicId(), + entityDeclaration.getSystemId(), entityDeclaration.getNotationName()); + } + } + + private void handleProcessingInstruction(ProcessingInstruction pi) throws SAXException { + if (getContentHandler() != null) { + getContentHandler().processingInstruction(pi.getTarget(), pi.getData()); + } + } + + private void handleComment(Comment comment) throws SAXException { + if (getLexicalHandler() != null) { + char[] ch = comment.getText().toCharArray(); + getLexicalHandler().comment(ch, 0, ch.length); + } + } + + private void handleDtd(DTD dtd) throws SAXException { + if (getLexicalHandler() != null) { + javax.xml.stream.Location location = dtd.getLocation(); + getLexicalHandler().startDTD(null, location.getPublicId(), location.getSystemId()); + } + if (getLexicalHandler() != null) { + getLexicalHandler().endDTD(); + } + + } + + private void handleEntityReference(EntityReference reference) throws SAXException { + if (getLexicalHandler() != null) { + getLexicalHandler().startEntity(reference.getName()); + } + if (getLexicalHandler() != null) { + getLexicalHandler().endEntity(reference.getName()); + } + + } + + private Attributes getAttributes(StartElement event) { + AttributesImpl attributes = new AttributesImpl(); + for (Iterator i = event.getAttributes(); i.hasNext();) { + Attribute attribute = (Attribute) i.next(); + QName qName = attribute.getName(); + String namespace = qName.getNamespaceURI(); + if (namespace == null || !hasNamespacesFeature()) { + namespace = ""; + } + String type = attribute.getDTDType(); + if (type == null) { + type = "CDATA"; + } + attributes.addAttribute(namespace, qName.getLocalPart(), toQualifiedName(qName), type, attribute.getValue()); + } + if (hasNamespacePrefixesFeature()) { + for (Iterator i = event.getNamespaces(); i.hasNext();) { + Namespace namespace = (Namespace) i.next(); + String prefix = namespace.getPrefix(); + String namespaceUri = namespace.getNamespaceURI(); + String qName; + if (StringUtils.hasLength(prefix)) { + qName = "xmlns:" + prefix; + } + else { + qName = "xmlns"; + } + attributes.addAttribute("", "", qName, "CDATA", namespaceUri); + } + } + + return attributes; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxResult.java b/spring-core/src/main/java/org/springframework/util/xml/StaxResult.java new file mode 100644 index 00000000..801769cd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxResult.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2013 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.util.xml; + +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.sax.SAXResult; + +import org.xml.sax.ContentHandler; + +/** + * Implementation of the {@code Result} tagging interface for StAX writers. Can be constructed with + * an {@code XMLEventConsumer} or an {@code XMLStreamWriter}. + * + * <p>This class is necessary because there is no implementation of {@code Source} for StaxReaders + * in JAXP 1.3. There is a {@code StAXResult} in JAXP 1.4 (JDK 1.6), but this class is kept around + * for backwards compatibility reasons. + * + * <p>Even though {@code StaxResult} extends from {@code SAXResult}, calling the methods of + * {@code SAXResult} is <strong>not supported</strong>. In general, the only supported operation + * on this class is to use the {@code ContentHandler} obtained via {@link #getHandler()} to parse an + * input source using an {@code XMLReader}. Calling {@link #setHandler(org.xml.sax.ContentHandler)} + * will result in {@code UnsupportedOperationException}s. + * + * @author Arjen Poutsma + * @since 3.0 + * @see XMLEventWriter + * @see XMLStreamWriter + * @see javax.xml.transform.Transformer + */ +class StaxResult extends SAXResult { + + private XMLEventWriter eventWriter; + + private XMLStreamWriter streamWriter; + + + /** + * Construct a new instance of the {@code StaxResult} with the specified {@code XMLStreamWriter}. + * @param streamWriter the {@code XMLStreamWriter} to write to + */ + StaxResult(XMLStreamWriter streamWriter) { + super.setHandler(new StaxStreamContentHandler(streamWriter)); + this.streamWriter = streamWriter; + } + + /** + * Construct a new instance of the {@code StaxResult} with the specified {@code XMLEventWriter}. + * @param eventWriter the {@code XMLEventWriter} to write to + */ + StaxResult(XMLEventWriter eventWriter) { + super.setHandler(new StaxEventContentHandler(eventWriter)); + this.eventWriter = eventWriter; + } + + /** + * Construct a new instance of the {@code StaxResult} with the specified {@code XMLEventWriter} + * and {@code XMLEventFactory}. + * @param eventWriter the {@code XMLEventWriter} to write to + * @param eventFactory the {@code XMLEventFactory} to use for creating events + */ + StaxResult(XMLEventWriter eventWriter, XMLEventFactory eventFactory) { + super.setHandler(new StaxEventContentHandler(eventWriter, eventFactory)); + this.eventWriter = eventWriter; + } + + + /** + * Return the {@code XMLEventWriter} used by this {@code StaxResult}. If this {@code StaxResult} + * was created with an {@code XMLStreamWriter}, the result will be {@code null}. + * @return the StAX event writer used by this result + * @see #StaxResult(javax.xml.stream.XMLEventWriter) + */ + XMLEventWriter getXMLEventWriter() { + return this.eventWriter; + } + + /** + * Return the {@code XMLStreamWriter} used by this {@code StaxResult}. If this {@code StaxResult} + * was created with an {@code XMLEventConsumer}, the result will be {@code null}. + * @return the StAX stream writer used by this result + * @see #StaxResult(javax.xml.stream.XMLStreamWriter) + */ + XMLStreamWriter getXMLStreamWriter() { + return this.streamWriter; + } + + + /** + * Throws an {@code UnsupportedOperationException}. + * @throws UnsupportedOperationException always + */ + @Override + public void setHandler(ContentHandler handler) { + throw new UnsupportedOperationException("setHandler is not supported"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxSource.java b/spring-core/src/main/java/org/springframework/util/xml/StaxSource.java new file mode 100644 index 00000000..5706a431 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxSource.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2013 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.util.xml; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.sax.SAXSource; + +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; + +/** + * Implementation of the {@code Source} tagging interface for StAX readers. Can be constructed with + * an {@code XMLEventReader} or an {@code XMLStreamReader}. + * + * <p>This class is necessary because there is no implementation of {@code Source} for StAX Readers + * in JAXP 1.3. There is a {@code StAXSource} in JAXP 1.4 (JDK 1.6), but this class is kept around + * for backwards compatibility reasons. + * + * <p>Even though {@code StaxSource} extends from {@code SAXSource}, calling the methods of + * {@code SAXSource} is <strong>not supported</strong>. In general, the only supported operation + * on this class is to use the {@code XMLReader} obtained via {@link #getXMLReader()} to parse the + * input source obtained via {@link #getInputSource()}. Calling {@link #setXMLReader(XMLReader)} + * or {@link #setInputSource(InputSource)} will result in {@code UnsupportedOperationException}s. + * + * @author Arjen Poutsma + * @since 3.0 + * @see XMLEventReader + * @see XMLStreamReader + * @see javax.xml.transform.Transformer + */ +class StaxSource extends SAXSource { + + private XMLEventReader eventReader; + + private XMLStreamReader streamReader; + + + /** + * Construct a new instance of the {@code StaxSource} with the specified {@code XMLStreamReader}. + * The supplied stream reader must be in {@code XMLStreamConstants.START_DOCUMENT} or + * {@code XMLStreamConstants.START_ELEMENT} state. + * @param streamReader the {@code XMLStreamReader} to read from + * @throws IllegalStateException if the reader is not at the start of a document or element + */ + StaxSource(XMLStreamReader streamReader) { + super(new StaxStreamXMLReader(streamReader), new InputSource()); + this.streamReader = streamReader; + } + + /** + * Construct a new instance of the {@code StaxSource} with the specified {@code XMLEventReader}. + * The supplied event reader must be in {@code XMLStreamConstants.START_DOCUMENT} or + * {@code XMLStreamConstants.START_ELEMENT} state. + * @param eventReader the {@code XMLEventReader} to read from + * @throws IllegalStateException if the reader is not at the start of a document or element + */ + StaxSource(XMLEventReader eventReader) { + super(new StaxEventXMLReader(eventReader), new InputSource()); + this.eventReader = eventReader; + } + + + /** + * Return the {@code XMLEventReader} used by this {@code StaxSource}. If this {@code StaxSource} + * was created with an {@code XMLStreamReader}, the result will be {@code null}. + * @return the StAX event reader used by this source + * @see StaxSource#StaxSource(javax.xml.stream.XMLEventReader) + */ + XMLEventReader getXMLEventReader() { + return this.eventReader; + } + + /** + * Return the {@code XMLStreamReader} used by this {@code StaxSource}. If this {@code StaxSource} + * was created with an {@code XMLEventReader}, the result will be {@code null}. + * @return the StAX event reader used by this source + * @see StaxSource#StaxSource(javax.xml.stream.XMLEventReader) + */ + XMLStreamReader getXMLStreamReader() { + return this.streamReader; + } + + + /** + * Throws an {@code UnsupportedOperationException}. + * @throws UnsupportedOperationException always + */ + @Override + public void setInputSource(InputSource inputSource) { + throw new UnsupportedOperationException("setInputSource is not supported"); + } + + /** + * Throws an {@code UnsupportedOperationException}. + * @throws UnsupportedOperationException always + */ + @Override + public void setXMLReader(XMLReader reader) { + throw new UnsupportedOperationException("setXMLReader is not supported"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxStreamContentHandler.java b/spring-core/src/main/java/org/springframework/util/xml/StaxStreamContentHandler.java new file mode 100644 index 00000000..2241e2f2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxStreamContentHandler.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2012 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.util.xml; + +import java.util.Iterator; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import org.xml.sax.Attributes; +import org.xml.sax.Locator; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * SAX {@code ContentHandler} that writes to a {@code XMLStreamWriter}. + * + * @author Arjen Poutsma + * @see XMLStreamWriter + * @since 3.0 + */ +class StaxStreamContentHandler extends AbstractStaxContentHandler { + + private final XMLStreamWriter streamWriter; + + /** + * Constructs a new instance of the {@code StaxStreamContentHandler} that writes to the given + * {@code XMLStreamWriter}. + * + * @param streamWriter the stream writer to write to + */ + StaxStreamContentHandler(XMLStreamWriter streamWriter) { + Assert.notNull(streamWriter, "'streamWriter' must not be null"); + this.streamWriter = streamWriter; + } + + public void setDocumentLocator(Locator locator) { + } + + @Override + protected void charactersInternal(char[] ch, int start, int length) throws XMLStreamException { + streamWriter.writeCharacters(ch, start, length); + } + + @Override + protected void endDocumentInternal() throws XMLStreamException { + streamWriter.writeEndDocument(); + } + + @Override + protected void endElementInternal(QName name, SimpleNamespaceContext namespaceContext) throws XMLStreamException { + streamWriter.writeEndElement(); + } + + @Override + protected void ignorableWhitespaceInternal(char[] ch, int start, int length) throws XMLStreamException { + streamWriter.writeCharacters(ch, start, length); + } + + @Override + protected void processingInstructionInternal(String target, String data) throws XMLStreamException { + streamWriter.writeProcessingInstruction(target, data); + } + + @Override + protected void skippedEntityInternal(String name) { + } + + @Override + protected void startDocumentInternal() throws XMLStreamException { + streamWriter.writeStartDocument(); + } + + @Override + protected void startElementInternal(QName name, Attributes attributes, SimpleNamespaceContext namespaceContext) + throws XMLStreamException { + streamWriter.writeStartElement(name.getPrefix(), name.getLocalPart(), name.getNamespaceURI()); + if (namespaceContext != null) { + String defaultNamespaceUri = namespaceContext.getNamespaceURI(""); + if (StringUtils.hasLength(defaultNamespaceUri)) { + streamWriter.writeNamespace("", defaultNamespaceUri); + streamWriter.setDefaultNamespace(defaultNamespaceUri); + } + for (Iterator<String> iterator = namespaceContext.getBoundPrefixes(); iterator.hasNext();) { + String prefix = iterator.next(); + streamWriter.writeNamespace(prefix, namespaceContext.getNamespaceURI(prefix)); + streamWriter.setPrefix(prefix, namespaceContext.getNamespaceURI(prefix)); + } + } + for (int i = 0; i < attributes.getLength(); i++) { + QName attrName = toQName(attributes.getURI(i), attributes.getQName(i)); + if (!("xmlns".equals(attrName.getLocalPart()) || "xmlns".equals(attrName.getPrefix()))) { + streamWriter.writeAttribute(attrName.getPrefix(), attrName.getNamespaceURI(), attrName.getLocalPart(), + attributes.getValue(i)); + } + } + } +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxStreamXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/StaxStreamXMLReader.java new file mode 100644 index 00000000..359514b5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxStreamXMLReader.java @@ -0,0 +1,296 @@ +/* + * Copyright 2002-2014 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.util.xml; + +import javax.xml.namespace.QName; +import javax.xml.stream.Location; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.ext.Locator2; +import org.xml.sax.helpers.AttributesImpl; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * SAX {@code XMLReader} that reads from a StAX {@code XMLStreamReader}. Reads from an + * {@code XMLStreamReader}, and calls the corresponding methods on the SAX callback interfaces. + * + * @author Arjen Poutsma + * @since 3.0 + * @see XMLStreamReader + * @see #setContentHandler(org.xml.sax.ContentHandler) + * @see #setDTDHandler(org.xml.sax.DTDHandler) + * @see #setEntityResolver(org.xml.sax.EntityResolver) + * @see #setErrorHandler(org.xml.sax.ErrorHandler) + */ +class StaxStreamXMLReader extends AbstractStaxXMLReader { + + private static final String DEFAULT_XML_VERSION = "1.0"; + + private final XMLStreamReader reader; + + private String xmlVersion = DEFAULT_XML_VERSION; + + private String encoding; + + + /** + * Construct a new instance of the {@code StaxStreamXmlReader} that reads from the given + * {@code XMLStreamReader}. The supplied stream reader must be in {@code XMLStreamConstants.START_DOCUMENT} + * or {@code XMLStreamConstants.START_ELEMENT} state. + * @param reader the {@code XMLEventReader} to read from + * @throws IllegalStateException if the reader is not at the start of a document or element + */ + StaxStreamXMLReader(XMLStreamReader reader) { + Assert.notNull(reader, "'reader' must not be null"); + int event = reader.getEventType(); + if (!(event == XMLStreamConstants.START_DOCUMENT || event == XMLStreamConstants.START_ELEMENT)) { + throw new IllegalStateException("XMLEventReader not at start of document or element"); + } + this.reader = reader; + } + + + @Override + protected void parseInternal() throws SAXException, XMLStreamException { + boolean documentStarted = false; + boolean documentEnded = false; + int elementDepth = 0; + int eventType = this.reader.getEventType(); + while (true) { + if (eventType != XMLStreamConstants.START_DOCUMENT && eventType != XMLStreamConstants.END_DOCUMENT && + !documentStarted) { + handleStartDocument(); + documentStarted = true; + } + switch (eventType) { + case XMLStreamConstants.START_ELEMENT: + elementDepth++; + handleStartElement(); + break; + case XMLStreamConstants.END_ELEMENT: + elementDepth--; + if (elementDepth >= 0) { + handleEndElement(); + } + break; + case XMLStreamConstants.PROCESSING_INSTRUCTION: + handleProcessingInstruction(); + break; + case XMLStreamConstants.CHARACTERS: + case XMLStreamConstants.SPACE: + case XMLStreamConstants.CDATA: + handleCharacters(); + break; + case XMLStreamConstants.START_DOCUMENT: + handleStartDocument(); + documentStarted = true; + break; + case XMLStreamConstants.END_DOCUMENT: + handleEndDocument(); + documentEnded = true; + break; + case XMLStreamConstants.COMMENT: + handleComment(); + break; + case XMLStreamConstants.DTD: + handleDtd(); + break; + case XMLStreamConstants.ENTITY_REFERENCE: + handleEntityReference(); + break; + } + if (this.reader.hasNext() && elementDepth >= 0) { + eventType = this.reader.next(); + } + else { + break; + } + } + if (!documentEnded) { + handleEndDocument(); + } + } + + private void handleStartDocument() throws SAXException { + if (XMLStreamConstants.START_DOCUMENT == this.reader.getEventType()) { + String xmlVersion = this.reader.getVersion(); + if (StringUtils.hasLength(xmlVersion)) { + this.xmlVersion = xmlVersion; + } + this.encoding = this.reader.getCharacterEncodingScheme(); + } + if (getContentHandler() != null) { + final Location location = this.reader.getLocation(); + getContentHandler().setDocumentLocator(new Locator2() { + public int getColumnNumber() { + return (location != null ? location.getColumnNumber() : -1); + } + public int getLineNumber() { + return (location != null ? location.getLineNumber() : -1); + } + public String getPublicId() { + return (location != null ? location.getPublicId() : null); + } + public String getSystemId() { + return (location != null ? location.getSystemId() : null); + } + public String getXMLVersion() { + return xmlVersion; + } + public String getEncoding() { + return encoding; + } + }); + getContentHandler().startDocument(); + if (this.reader.standaloneSet()) { + setStandalone(this.reader.isStandalone()); + } + } + } + + private void handleStartElement() throws SAXException { + if (getContentHandler() != null) { + QName qName = this.reader.getName(); + if (hasNamespacesFeature()) { + for (int i = 0; i < this.reader.getNamespaceCount(); i++) { + startPrefixMapping(this.reader.getNamespacePrefix(i), this.reader.getNamespaceURI(i)); + } + for (int i = 0; i < this.reader.getAttributeCount(); i++) { + String prefix = this.reader.getAttributePrefix(i); + String namespace = this.reader.getAttributeNamespace(i); + if (StringUtils.hasLength(namespace)) { + startPrefixMapping(prefix, namespace); + } + } + getContentHandler().startElement(qName.getNamespaceURI(), qName.getLocalPart(), + toQualifiedName(qName), getAttributes()); + } + else { + getContentHandler().startElement("", "", toQualifiedName(qName), getAttributes()); + } + } + } + + private void handleEndElement() throws SAXException { + if (getContentHandler() != null) { + QName qName = this.reader.getName(); + if (hasNamespacesFeature()) { + getContentHandler().endElement(qName.getNamespaceURI(), qName.getLocalPart(), toQualifiedName(qName)); + for (int i = 0; i < this.reader.getNamespaceCount(); i++) { + String prefix = this.reader.getNamespacePrefix(i); + if (prefix == null) { + prefix = ""; + } + endPrefixMapping(prefix); + } + } + else { + getContentHandler().endElement("", "", toQualifiedName(qName)); + } + } + } + + private void handleCharacters() throws SAXException { + if (XMLStreamConstants.CDATA == this.reader.getEventType() && getLexicalHandler() != null) { + getLexicalHandler().startCDATA(); + } + if (getContentHandler() != null) { + getContentHandler().characters(this.reader.getTextCharacters(), + this.reader.getTextStart(), this.reader.getTextLength()); + } + if (XMLStreamConstants.CDATA == this.reader.getEventType() && getLexicalHandler() != null) { + getLexicalHandler().endCDATA(); + } + } + + private void handleComment() throws SAXException { + if (getLexicalHandler() != null) { + getLexicalHandler().comment(this.reader.getTextCharacters(), + this.reader.getTextStart(), this.reader.getTextLength()); + } + } + + private void handleDtd() throws SAXException { + if (getLexicalHandler() != null) { + javax.xml.stream.Location location = this.reader.getLocation(); + getLexicalHandler().startDTD(null, location.getPublicId(), location.getSystemId()); + } + if (getLexicalHandler() != null) { + getLexicalHandler().endDTD(); + } + } + + private void handleEntityReference() throws SAXException { + if (getLexicalHandler() != null) { + getLexicalHandler().startEntity(this.reader.getLocalName()); + } + if (getLexicalHandler() != null) { + getLexicalHandler().endEntity(this.reader.getLocalName()); + } + } + + private void handleEndDocument() throws SAXException { + if (getContentHandler() != null) { + getContentHandler().endDocument(); + } + } + + private void handleProcessingInstruction() throws SAXException { + if (getContentHandler() != null) { + getContentHandler().processingInstruction(this.reader.getPITarget(), this.reader.getPIData()); + } + } + + private Attributes getAttributes() { + AttributesImpl attributes = new AttributesImpl(); + for (int i = 0; i < this.reader.getAttributeCount(); i++) { + String namespace = this.reader.getAttributeNamespace(i); + if (namespace == null || !hasNamespacesFeature()) { + namespace = ""; + } + String type = this.reader.getAttributeType(i); + if (type == null) { + type = "CDATA"; + } + attributes.addAttribute(namespace, this.reader.getAttributeLocalName(i), + toQualifiedName(this.reader.getAttributeName(i)), type, this.reader.getAttributeValue(i)); + } + if (hasNamespacePrefixesFeature()) { + for (int i = 0; i < this.reader.getNamespaceCount(); i++) { + String prefix = this.reader.getNamespacePrefix(i); + String namespaceUri = this.reader.getNamespaceURI(i); + String qName; + if (StringUtils.hasLength(prefix)) { + qName = "xmlns:" + prefix; + } + else { + qName = "xmlns"; + } + attributes.addAttribute("", "", qName, "CDATA", namespaceUri); + } + } + + return attributes; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxUtils.java b/spring-core/src/main/java/org/springframework/util/xml/StaxUtils.java new file mode 100644 index 00000000..41fd3f63 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxUtils.java @@ -0,0 +1,389 @@ +/* + * Copyright 2002-2013 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.util.xml; + +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.stax.StAXResult; +import javax.xml.transform.stax.StAXSource; + +import org.xml.sax.ContentHandler; +import org.xml.sax.XMLReader; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Convenience methods for working with the StAX API. + * + * <p>In particular, methods for using StAX ({@code javax.xml.stream}) in combination with the TrAX API + * ({@code javax.xml.transform}), and converting StAX readers/writers into SAX readers/handlers and vice-versa. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + */ +public abstract class StaxUtils { + + // JAXP 1.4 is only available on JDK 1.6+ + private static boolean jaxp14Available = + ClassUtils.isPresent("javax.xml.transform.stax.StAXSource", StaxUtils.class.getClassLoader()); + + + // Stax Source + + /** + * Create a custom, non-JAXP 1.4 StAX {@link Source} for the given {@link XMLStreamReader}. + * @param streamReader the StAX stream reader + * @return a source wrapping the {@code streamReader} + */ + public static Source createCustomStaxSource(XMLStreamReader streamReader) { + return new StaxSource(streamReader); + } + + /** + * Create a StAX {@link Source} for the given {@link XMLStreamReader}. + * <p>If JAXP 1.4 is available, this method returns a {@link StAXSource}; + * otherwise it returns a custom StAX Source. + * @param streamReader the StAX stream reader + * @return a source wrapping the {@code streamReader} + * @see #createCustomStaxSource(XMLStreamReader) + */ + public static Source createStaxSource(XMLStreamReader streamReader) { + if (jaxp14Available) { + return Jaxp14StaxHandler.createStaxSource(streamReader); + } + else { + return createCustomStaxSource(streamReader); + } + } + + /** + * Create a custom, non-JAXP 1.4 StAX {@link Source} for the given {@link XMLEventReader}. + * @param eventReader the StAX event reader + * @return a source wrapping the {@code eventReader} + */ + public static Source createCustomStaxSource(XMLEventReader eventReader) { + return new StaxSource(eventReader); + } + + /** + * Create a StAX {@link Source} for the given {@link XMLEventReader}. + * <p>If JAXP 1.4 is available, this method returns a {@link StAXSource}; + * otherwise it returns a custom StAX Source. + * @param eventReader the StAX event reader + * @return a source wrapping the {@code eventReader} + * @throws XMLStreamException in case of StAX errors + * @see #createCustomStaxSource(XMLEventReader) + */ + public static Source createStaxSource(XMLEventReader eventReader) throws XMLStreamException { + if (jaxp14Available) { + return Jaxp14StaxHandler.createStaxSource(eventReader); + } + else { + return createCustomStaxSource(eventReader); + } + } + + /** + * Indicate whether the given {@link Source} is a StAX Source. + * @return {@code true} if {@code source} is a custom StAX source or JAXP + * 1.4 {@link StAXSource}; {@code false} otherwise. + */ + public static boolean isStaxSource(Source source) { + return ((source instanceof StaxSource) || (jaxp14Available && Jaxp14StaxHandler.isStaxSource(source))); + } + + /** + * Indicate whether the given class is a StAX Source class. + * @return {@code true} if {@code source} is a custom StAX source or JAXP + * 1.4 {@link StAXSource} class; {@code false} otherwise. + */ + public static boolean isStaxSourceClass(Class<? extends Source> clazz) { + return (StaxSource.class.equals(clazz) || (jaxp14Available && Jaxp14StaxHandler.isStaxSourceClass(clazz))); + } + + + // Stax Result + + /** + * Create a custom, non-JAXP 1.4 StAX {@link Result} for the given {@link XMLStreamWriter}. + * @param streamWriter the StAX stream writer + * @return a source wrapping the {@code streamWriter} + */ + public static Result createCustomStaxResult(XMLStreamWriter streamWriter) { + return new StaxResult(streamWriter); + } + + /** + * Create a StAX {@link Result} for the given {@link XMLStreamWriter}. + * <p>If JAXP 1.4 is available, this method returns a {@link StAXResult}; + * otherwise it returns a custom StAX Result. + * @param streamWriter the StAX stream writer + * @return a result wrapping the {@code streamWriter} + * @see #createCustomStaxResult(XMLStreamWriter) + */ + public static Result createStaxResult(XMLStreamWriter streamWriter) { + if (jaxp14Available) { + return Jaxp14StaxHandler.createStaxResult(streamWriter); + } + else { + return createCustomStaxResult(streamWriter); + } + } + + /** + * Create a custom, non-JAXP 1.4 StAX {@link Result} for the given {@link XMLEventWriter}. + * @param eventWriter the StAX event writer + * @return a source wrapping the {@code eventWriter} + */ + public static Result createCustomStaxResult(XMLEventWriter eventWriter) { + return new StaxResult(eventWriter); + } + + /** + * Create a StAX {@link Result} for the given {@link XMLEventWriter}. + * <p>If JAXP 1.4 is available, this method returns a {@link StAXResult}; otherwise it returns a + * custom StAX Result. + * @param eventWriter the StAX event writer + * @return a result wrapping {@code streamReader} + * @throws XMLStreamException in case of StAX errors + * @see #createCustomStaxResult(XMLEventWriter) + */ + public static Result createStaxResult(XMLEventWriter eventWriter) throws XMLStreamException { + if (jaxp14Available) { + return Jaxp14StaxHandler.createStaxResult(eventWriter); + } + else { + return createCustomStaxResult(eventWriter); + } + } + + /** + * Indicate whether the given {@link javax.xml.transform.Result} is a StAX Result. + * @return {@code true} if {@code result} is a custom Stax Result or JAXP 1.4 + * {@link StAXResult}; {@code false} otherwise. + */ + public static boolean isStaxResult(Result result) { + return (result instanceof StaxResult || (jaxp14Available && Jaxp14StaxHandler.isStaxResult(result))); + } + + /** + * Return the {@link XMLStreamReader} for the given StAX Source. + * @param source a {@linkplain #createCustomStaxSource(XMLStreamReader) custom StAX Source} or + * JAXP 1.4 {@link StAXSource} + * @return the {@link XMLStreamReader} + * @throws IllegalArgumentException if {@code source} is neither a custom StAX Source + * nor JAXP 1.4 {@link StAXSource} + */ + public static XMLStreamReader getXMLStreamReader(Source source) { + if (source instanceof StaxSource) { + return ((StaxSource) source).getXMLStreamReader(); + } + else if (jaxp14Available) { + return Jaxp14StaxHandler.getXMLStreamReader(source); + } + else { + throw new IllegalArgumentException("Source '" + source + "' is neither StaxSource nor StAXSource"); + } + } + + /** + * Return the {@link XMLEventReader} for the given StAX Source. + * @param source a {@linkplain #createCustomStaxSource(XMLEventReader) custom StAX Source} or + * JAXP 1.4 {@link StAXSource} + * @return the {@link XMLEventReader} + * @throws IllegalArgumentException if {@code source} is neither a custom StAX Source + * nor a JAXP 1.4 {@link StAXSource} + */ + public static XMLEventReader getXMLEventReader(Source source) { + if (source instanceof StaxSource) { + return ((StaxSource) source).getXMLEventReader(); + } + else if (jaxp14Available) { + return Jaxp14StaxHandler.getXMLEventReader(source); + } + else { + throw new IllegalArgumentException("Source '" + source + "' is neither StaxSource nor StAXSource"); + } + } + + /** + * Return the {@link XMLStreamWriter} for the given StAX Result. + * @param result a {@linkplain #createCustomStaxResult(XMLStreamWriter) custom StAX Result} or + * JAXP 1.4 {@link StAXResult} + * @return the {@link XMLStreamReader} + * @throws IllegalArgumentException if {@code source} is neither a custom StAX Result + * nor a JAXP 1.4 {@link StAXResult} + */ + public static XMLStreamWriter getXMLStreamWriter(Result result) { + if (result instanceof StaxResult) { + return ((StaxResult) result).getXMLStreamWriter(); + } + else if (jaxp14Available) { + return Jaxp14StaxHandler.getXMLStreamWriter(result); + } + else { + throw new IllegalArgumentException("Result '" + result + "' is neither StaxResult nor StAXResult"); + } + } + + /** + * Return the {@link XMLEventWriter} for the given StAX Result. + * @param result a {@linkplain #createCustomStaxResult(XMLEventWriter) custom StAX Result} or + * JAXP 1.4 {@link StAXResult} + * @return the {@link XMLStreamReader} + * @throws IllegalArgumentException if {@code source} is neither a custom StAX Result + * nor a JAXP 1.4 {@link StAXResult} + */ + public static XMLEventWriter getXMLEventWriter(Result result) { + if (result instanceof StaxResult) { + return ((StaxResult) result).getXMLEventWriter(); + } + else if (jaxp14Available) { + return Jaxp14StaxHandler.getXMLEventWriter(result); + } + else { + throw new IllegalArgumentException("Result '" + result + "' is neither StaxResult nor StAXResult"); + } + } + + /** + * Create a SAX {@link ContentHandler} that writes to the given StAX {@link XMLStreamWriter}. + * @param streamWriter the StAX stream writer + * @return a content handler writing to the {@code streamWriter} + */ + public static ContentHandler createContentHandler(XMLStreamWriter streamWriter) { + return new StaxStreamContentHandler(streamWriter); + } + + /** + * Create a SAX {@link ContentHandler} that writes events to the given StAX {@link XMLEventWriter}. + * @param eventWriter the StAX event writer + * @return a content handler writing to the {@code eventWriter} + */ + public static ContentHandler createContentHandler(XMLEventWriter eventWriter) { + return new StaxEventContentHandler(eventWriter); + } + + /** + * Create a SAX {@link XMLReader} that reads from the given StAX {@link XMLStreamReader}. + * @param streamReader the StAX stream reader + * @return a XMLReader reading from the {@code streamWriter} + */ + public static XMLReader createXMLReader(XMLStreamReader streamReader) { + return new StaxStreamXMLReader(streamReader); + } + + /** + * Create a SAX {@link XMLReader} that reads from the given StAX {@link XMLEventReader}. + * @param eventReader the StAX event reader + * @return a XMLReader reading from the {@code eventWriter} + */ + public static XMLReader createXMLReader(XMLEventReader eventReader) { + return new StaxEventXMLReader(eventReader); + } + + /** + * Return a {@link XMLStreamReader} that reads from a {@link XMLEventReader}. Useful, because the StAX + * {@code XMLInputFactory} allows one to create a event reader from a stream reader, but not vice-versa. + * @return a stream reader that reads from an event reader + */ + public static XMLStreamReader createEventStreamReader(XMLEventReader eventReader) throws XMLStreamException { + return new XMLEventStreamReader(eventReader); + } + + /** + * Return a {@link XMLStreamWriter} that writes to a {@link XMLEventWriter}. + * @return a stream writer that writes to an event writer + * @since 3.2 + */ + public static XMLStreamWriter createEventStreamWriter(XMLEventWriter eventWriter) { + return new XMLEventStreamWriter(eventWriter, XMLEventFactory.newFactory()); + } + + /** + * Return a {@link XMLStreamWriter} that writes to a {@link XMLEventWriter}. + * @return a stream writer that writes to an event writer + * @since 3.0.5 + */ + public static XMLStreamWriter createEventStreamWriter(XMLEventWriter eventWriter, XMLEventFactory eventFactory) { + return new XMLEventStreamWriter(eventWriter, eventFactory); + } + + + /** + * Inner class to avoid a static JAXP 1.4 dependency. + */ + private static class Jaxp14StaxHandler { + + private static Source createStaxSource(XMLStreamReader streamReader) { + return new StAXSource(streamReader); + } + + private static Source createStaxSource(XMLEventReader eventReader) throws XMLStreamException { + return new StAXSource(eventReader); + } + + private static Result createStaxResult(XMLStreamWriter streamWriter) { + return new StAXResult(streamWriter); + } + + private static Result createStaxResult(XMLEventWriter eventWriter) { + return new StAXResult(eventWriter); + } + + private static boolean isStaxSource(Source source) { + return (source instanceof StAXSource); + } + + private static boolean isStaxSourceClass(Class<? extends Source> clazz) { + return StAXSource.class.equals(clazz); + } + + private static boolean isStaxResult(Result result) { + return (result instanceof StAXResult); + } + + private static XMLStreamReader getXMLStreamReader(Source source) { + Assert.isInstanceOf(StAXSource.class, source); + return ((StAXSource) source).getXMLStreamReader(); + } + + private static XMLEventReader getXMLEventReader(Source source) { + Assert.isInstanceOf(StAXSource.class, source); + return ((StAXSource) source).getXMLEventReader(); + } + + private static XMLStreamWriter getXMLStreamWriter(Result result) { + Assert.isInstanceOf(StAXResult.class, result); + return ((StAXResult) result).getXMLStreamWriter(); + } + + private static XMLEventWriter getXMLEventWriter(Result result) { + Assert.isInstanceOf(StAXResult.class, result); + return ((StAXResult) result).getXMLEventWriter(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/TransformerUtils.java b/spring-core/src/main/java/org/springframework/util/xml/TransformerUtils.java new file mode 100644 index 00000000..4b48abd4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/TransformerUtils.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2012 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.util.xml; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; + +import org.springframework.util.Assert; + +/** + * Contains common behavior relating to {@link javax.xml.transform.Transformer Transformers}, and the + * {@code javax.xml.transform} package in general. + * + * @author Rick Evans + * @author Juergen Hoeller + * @since 2.5.5 + */ +public abstract class TransformerUtils { + + /** + * The indent amount of characters if {@link #enableIndenting(javax.xml.transform.Transformer) indenting is enabled}. + * <p>Defaults to "2". + */ + public static final int DEFAULT_INDENT_AMOUNT = 2; + + /** + * Enable indenting for the supplied {@link javax.xml.transform.Transformer}. <p>If the underlying XSLT engine is + * Xalan, then the special output key {@code indent-amount} will be also be set to a value of {@link + * #DEFAULT_INDENT_AMOUNT} characters. + * + * @param transformer the target transformer + * @see javax.xml.transform.Transformer#setOutputProperty(String, String) + * @see javax.xml.transform.OutputKeys#INDENT + */ + public static void enableIndenting(Transformer transformer) { + enableIndenting(transformer, DEFAULT_INDENT_AMOUNT); + } + + /** + * Enable indenting for the supplied {@link javax.xml.transform.Transformer}. <p>If the underlying XSLT engine is + * Xalan, then the special output key {@code indent-amount} will be also be set to a value of {@link + * #DEFAULT_INDENT_AMOUNT} characters. + * + * @param transformer the target transformer + * @param indentAmount the size of the indent (2 characters, 3 characters, etc.) + * @see javax.xml.transform.Transformer#setOutputProperty(String, String) + * @see javax.xml.transform.OutputKeys#INDENT + */ + public static void enableIndenting(Transformer transformer, int indentAmount) { + Assert.notNull(transformer, "Transformer must not be null"); + Assert.isTrue(indentAmount > -1, "The indent amount cannot be less than zero : got " + indentAmount); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + try { + // Xalan-specific, but this is the most common XSLT engine in any case + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(indentAmount)); + } + catch (IllegalArgumentException ignored) { + } + } + + /** + * Disable indenting for the supplied {@link javax.xml.transform.Transformer}. + * + * @param transformer the target transformer + * @see javax.xml.transform.OutputKeys#INDENT + */ + public static void disableIndenting(Transformer transformer) { + Assert.notNull(transformer, "Transformer must not be null"); + transformer.setOutputProperty(OutputKeys.INDENT, "no"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamReader.java b/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamReader.java new file mode 100644 index 00000000..b8321a05 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamReader.java @@ -0,0 +1,268 @@ +/* + * Copyright 2002-2013 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.util.xml; + +import java.util.Iterator; +import javax.xml.namespace.NamespaceContext; +import javax.xml.namespace.QName; +import javax.xml.stream.Location; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.Comment; +import javax.xml.stream.events.Namespace; +import javax.xml.stream.events.ProcessingInstruction; +import javax.xml.stream.events.StartDocument; +import javax.xml.stream.events.XMLEvent; + +/** + * Implementation of the {@link javax.xml.stream.XMLStreamReader} interface that wraps a + * {@link XMLEventReader}. Useful because the StAX {@link javax.xml.stream.XMLInputFactory} + * allows one to create a event reader from a stream reader, but not vice-versa. + * + * @author Arjen Poutsma + * @since 3.0 + * @see StaxUtils#createEventStreamReader(javax.xml.stream.XMLEventReader) + */ +class XMLEventStreamReader extends AbstractXMLStreamReader { + + private XMLEvent event; + + private final XMLEventReader eventReader; + + + public XMLEventStreamReader(XMLEventReader eventReader) throws XMLStreamException { + this.eventReader = eventReader; + this.event = eventReader.nextEvent(); + } + + + public QName getName() { + if (this.event.isStartElement()) { + return this.event.asStartElement().getName(); + } + else if (this.event.isEndElement()) { + return this.event.asEndElement().getName(); + } + else { + throw new IllegalStateException(); + } + } + + public Location getLocation() { + return this.event.getLocation(); + } + + public int getEventType() { + return this.event.getEventType(); + } + + public String getVersion() { + if (this.event.isStartDocument()) { + return ((StartDocument) this.event).getVersion(); + } + else { + return null; + } + } + + public Object getProperty(String name) throws IllegalArgumentException { + return this.eventReader.getProperty(name); + } + + public boolean isStandalone() { + if (this.event.isStartDocument()) { + return ((StartDocument) event).isStandalone(); + } + else { + throw new IllegalStateException(); + } + } + + public boolean standaloneSet() { + if (this.event.isStartDocument()) { + return ((StartDocument) this.event).standaloneSet(); + } + else { + throw new IllegalStateException(); + } + } + + public String getEncoding() { + return null; + } + + public String getCharacterEncodingScheme() { + return null; + } + + public String getPITarget() { + if (this.event.isProcessingInstruction()) { + return ((ProcessingInstruction) this.event).getTarget(); + } + else { + throw new IllegalStateException(); + } + } + + public String getPIData() { + if (this.event.isProcessingInstruction()) { + return ((ProcessingInstruction) this.event).getData(); + } + else { + throw new IllegalStateException(); + } + } + + public int getTextStart() { + return 0; + } + + public String getText() { + if (this.event.isCharacters()) { + return event.asCharacters().getData(); + } + else if (this.event.getEventType() == XMLEvent.COMMENT) { + return ((Comment) this.event).getText(); + } + else { + throw new IllegalStateException(); + } + } + + @SuppressWarnings("rawtypes") + public int getAttributeCount() { + if (!this.event.isStartElement()) { + throw new IllegalStateException(); + } + Iterator attributes = this.event.asStartElement().getAttributes(); + return countIterator(attributes); + } + + public boolean isAttributeSpecified(int index) { + return getAttribute(index).isSpecified(); + } + + public QName getAttributeName(int index) { + return getAttribute(index).getName(); + } + + public String getAttributeType(int index) { + return getAttribute(index).getDTDType(); + } + + public String getAttributeValue(int index) { + return getAttribute(index).getValue(); + } + + @SuppressWarnings("rawtypes") + private Attribute getAttribute(int index) { + if (!this.event.isStartElement()) { + throw new IllegalStateException(); + } + int count = 0; + Iterator attributes = this.event.asStartElement().getAttributes(); + while (attributes.hasNext()) { + Attribute attribute = (Attribute) attributes.next(); + if (count == index) { + return attribute; + } + else { + count++; + } + } + throw new IllegalArgumentException(); + } + + public NamespaceContext getNamespaceContext() { + if (this.event.isStartElement()) { + return this.event.asStartElement().getNamespaceContext(); + } + else { + throw new IllegalStateException(); + } + } + + @SuppressWarnings("rawtypes") + public int getNamespaceCount() { + Iterator namespaces; + if (this.event.isStartElement()) { + namespaces = this.event.asStartElement().getNamespaces(); + } + else if (this.event.isEndElement()) { + namespaces = this.event.asEndElement().getNamespaces(); + } + else { + throw new IllegalStateException(); + } + return countIterator(namespaces); + } + + public String getNamespacePrefix(int index) { + return getNamespace(index).getPrefix(); + } + + public String getNamespaceURI(int index) { + return getNamespace(index).getNamespaceURI(); + } + + @SuppressWarnings("rawtypes") + private Namespace getNamespace(int index) { + Iterator namespaces; + if (this.event.isStartElement()) { + namespaces = this.event.asStartElement().getNamespaces(); + } + else if (this.event.isEndElement()) { + namespaces = this.event.asEndElement().getNamespaces(); + } + else { + throw new IllegalStateException(); + } + int count = 0; + while (namespaces.hasNext()) { + Namespace namespace = (Namespace) namespaces.next(); + if (count == index) { + return namespace; + } + else { + count++; + } + } + throw new IllegalArgumentException(); + } + + public int next() throws XMLStreamException { + this.event = this.eventReader.nextEvent(); + return this.event.getEventType(); + } + + public void close() throws XMLStreamException { + this.eventReader.close(); + } + + + @SuppressWarnings("rawtypes") + private static int countIterator(Iterator iterator) { + int count = 0; + while (iterator.hasNext()) { + iterator.next(); + count++; + } + return count; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamWriter.java b/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamWriter.java new file mode 100644 index 00000000..d65c4a36 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamWriter.java @@ -0,0 +1,248 @@ +/* + * Copyright 2002-2013 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.util.xml; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import javax.xml.namespace.NamespaceContext; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.stream.events.EndElement; +import javax.xml.stream.events.Namespace; +import javax.xml.stream.events.StartElement; + +import org.springframework.util.Assert; + +/** + * Implementation of the {@link javax.xml.stream.XMLStreamWriter} interface + * that wraps an {@link XMLEventWriter}. + * + * @author Arjen Poutsma + * @since 3.0.5 + * @see StaxUtils#createEventStreamWriter(javax.xml.stream.XMLEventWriter, javax.xml.stream.XMLEventFactory) + */ +class XMLEventStreamWriter implements XMLStreamWriter { + + private static final String DEFAULT_ENCODING = "UTF-8"; + + private final XMLEventWriter eventWriter; + + private final XMLEventFactory eventFactory; + + private final List<EndElement> endElements = new ArrayList<EndElement>(); + + private boolean emptyElement = false; + + + public XMLEventStreamWriter(XMLEventWriter eventWriter, XMLEventFactory eventFactory) { + Assert.notNull(eventWriter, "'eventWriter' must not be null"); + Assert.notNull(eventFactory, "'eventFactory' must not be null"); + this.eventWriter = eventWriter; + this.eventFactory = eventFactory; + } + + + public void setNamespaceContext(NamespaceContext context) throws XMLStreamException { + this.eventWriter.setNamespaceContext(context); + } + + public NamespaceContext getNamespaceContext() { + return this.eventWriter.getNamespaceContext(); + } + + public void setPrefix(String prefix, String uri) throws XMLStreamException { + this.eventWriter.setPrefix(prefix, uri); + } + + public String getPrefix(String uri) throws XMLStreamException { + return this.eventWriter.getPrefix(uri); + } + + public void setDefaultNamespace(String uri) throws XMLStreamException { + this.eventWriter.setDefaultNamespace(uri); + } + + public Object getProperty(String name) throws IllegalArgumentException { + throw new IllegalArgumentException(); + } + + + public void writeStartDocument() throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createStartDocument()); + } + + public void writeStartDocument(String version) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createStartDocument(DEFAULT_ENCODING, version)); + } + + public void writeStartDocument(String encoding, String version) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createStartDocument(encoding, version)); + } + + public void writeStartElement(String localName) throws XMLStreamException { + closeEmptyElementIfNecessary(); + doWriteStartElement(this.eventFactory.createStartElement(new QName(localName), null, null)); + } + + public void writeStartElement(String namespaceURI, String localName) throws XMLStreamException { + closeEmptyElementIfNecessary(); + doWriteStartElement(this.eventFactory.createStartElement(new QName(namespaceURI, localName), null, null)); + } + + public void writeStartElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + closeEmptyElementIfNecessary(); + doWriteStartElement(this.eventFactory.createStartElement(new QName(namespaceURI, localName, prefix), null, null)); + } + + private void doWriteStartElement(StartElement startElement) throws XMLStreamException { + this.eventWriter.add(startElement); + this.endElements.add(this.eventFactory.createEndElement(startElement.getName(), startElement.getNamespaces())); + } + + public void writeEmptyElement(String localName) throws XMLStreamException { + closeEmptyElementIfNecessary(); + writeStartElement(localName); + this.emptyElement = true; + } + + public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException { + closeEmptyElementIfNecessary(); + writeStartElement(namespaceURI, localName); + this.emptyElement = true; + } + + public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + closeEmptyElementIfNecessary(); + writeStartElement(prefix, localName, namespaceURI); + this.emptyElement = true; + } + + private void closeEmptyElementIfNecessary() throws XMLStreamException { + if (this.emptyElement) { + this.emptyElement = false; + writeEndElement(); + } + } + + public void writeEndElement() throws XMLStreamException { + closeEmptyElementIfNecessary(); + int last = this.endElements.size() - 1; + EndElement lastEndElement = this.endElements.get(last); + this.eventWriter.add(lastEndElement); + this.endElements.remove(last); + } + + public void writeAttribute(String localName, String value) throws XMLStreamException { + this.eventWriter.add(this.eventFactory.createAttribute(localName, value)); + } + + public void writeAttribute(String namespaceURI, String localName, String value) throws XMLStreamException { + this.eventWriter.add(this.eventFactory.createAttribute(new QName(namespaceURI, localName), value)); + } + + public void writeAttribute(String prefix, String namespaceURI, String localName, String value) + throws XMLStreamException { + + this.eventWriter.add(this.eventFactory.createAttribute(prefix, namespaceURI, localName, value)); + } + + public void writeNamespace(String prefix, String namespaceURI) throws XMLStreamException { + doWriteNamespace(this.eventFactory.createNamespace(prefix, namespaceURI)); + } + + public void writeDefaultNamespace(String namespaceURI) throws XMLStreamException { + doWriteNamespace(this.eventFactory.createNamespace(namespaceURI)); + } + + @SuppressWarnings("rawtypes") + private void doWriteNamespace(Namespace namespace) throws XMLStreamException { + int last = this.endElements.size() - 1; + EndElement oldEndElement = this.endElements.get(last); + Iterator oldNamespaces = oldEndElement.getNamespaces(); + List<Namespace> newNamespaces = new ArrayList<Namespace>(); + while (oldNamespaces.hasNext()) { + Namespace oldNamespace = (Namespace) oldNamespaces.next(); + newNamespaces.add(oldNamespace); + } + newNamespaces.add(namespace); + EndElement newEndElement = this.eventFactory.createEndElement(oldEndElement.getName(), newNamespaces.iterator()); + this.eventWriter.add(namespace); + this.endElements.set(last, newEndElement); + } + + public void writeCharacters(String text) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createCharacters(text)); + } + + public void writeCharacters(char[] text, int start, int len) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createCharacters(new String(text, start, len))); + } + + public void writeCData(String data) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createCData(data)); + } + + public void writeComment(String data) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createComment(data)); + } + + public void writeProcessingInstruction(String target) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createProcessingInstruction(target, "")); + } + + public void writeProcessingInstruction(String target, String data) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createProcessingInstruction(target, data)); + } + + public void writeDTD(String dtd) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createDTD(dtd)); + } + + public void writeEntityRef(String name) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createEntityReference(name, null)); + } + + public void writeEndDocument() throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createEndDocument()); + } + + public void flush() throws XMLStreamException { + this.eventWriter.flush(); + } + + public void close() throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.close(); + } + +} 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 new file mode 100644 index 00000000..d924fd15 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/XmlValidationModeDetector.java @@ -0,0 +1,195 @@ +/* + * Copyright 2002-2014 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.util.xml; + +import java.io.BufferedReader; +import java.io.CharConversionException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.springframework.util.StringUtils; + +/** + * Detects whether an XML stream is using DTD- or XSD-based validation. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public class XmlValidationModeDetector { + + /** + * Indicates that the validation should be disabled. + */ + public static final int VALIDATION_NONE = 0; + + /** + * Indicates that the validation mode should be auto-guessed, since we cannot find + * a clear indication (probably choked on some special characters, or the like). + */ + public static final int VALIDATION_AUTO = 1; + + /** + * Indicates that DTD validation should be used (we found a "DOCTYPE" declaration). + */ + public static final int VALIDATION_DTD = 2; + + /** + * Indicates that XSD validation should be used (found no "DOCTYPE" declaration). + */ + public static final int VALIDATION_XSD = 3; + + + /** + * The token in a XML document that declares the DTD to use for validation + * and thus that DTD validation is being used. + */ + private static final String DOCTYPE = "DOCTYPE"; + + /** + * The token that indicates the start of an XML comment. + */ + private static final String START_COMMENT = "<!--"; + + /** + * The token that indicates the end of an XML comment. + */ + private static final String END_COMMENT = "-->"; + + + /** + * Indicates whether or not the current parse position is inside an XML comment. + */ + private boolean inComment; + + + /** + * Detect the validation mode for the XML document in the supplied {@link InputStream}. + * Note that the supplied {@link InputStream} is closed by this method before returning. + * @param inputStream the InputStream to parse + * @throws IOException in case of I/O failure + * @see #VALIDATION_DTD + * @see #VALIDATION_XSD + */ + public int detectValidationMode(InputStream inputStream) throws IOException { + // Peek into the file to look for DOCTYPE. + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + try { + boolean isDtdValidated = false; + String content; + while ((content = reader.readLine()) != null) { + content = consumeCommentTokens(content); + if (this.inComment || !StringUtils.hasText(content)) { + continue; + } + if (hasDoctype(content)) { + isDtdValidated = true; + break; + } + if (hasOpeningTag(content)) { + // End of meaningful data... + break; + } + } + return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD); + } + catch (CharConversionException ex) { + // Choked on some character encoding... + // Leave the decision up to the caller. + return VALIDATION_AUTO; + } + finally { + reader.close(); + } + } + + + /** + * Does the content contain the the DTD DOCTYPE declaration? + */ + private boolean hasDoctype(String content) { + return content.contains(DOCTYPE); + } + + /** + * Does the supplied content contain an XML opening tag. If the parse state is currently + * in an XML comment then this method always returns false. It is expected that all comment + * tokens will have consumed for the supplied content before passing the remainder to this method. + */ + private boolean hasOpeningTag(String content) { + if (this.inComment) { + return false; + } + int openTagIndex = content.indexOf('<'); + return (openTagIndex > -1 && (content.length() > openTagIndex + 1) && + Character.isLetter(content.charAt(openTagIndex + 1))); + } + + /** + * Consumes all the leading comment data in the given String and returns the remaining content, which + * may be empty since the supplied content might be all comment data. For our purposes it is only important + * to strip leading comment content on a line since the first piece of non comment content will be either + * the DOCTYPE declaration or the root element of the document. + */ + private String consumeCommentTokens(String line) { + if (!line.contains(START_COMMENT) && !line.contains(END_COMMENT)) { + return line; + } + while ((line = consume(line)) != null) { + if (!this.inComment && !line.trim().startsWith(START_COMMENT)) { + return line; + } + } + return line; + } + + /** + * Consume the next comment token, update the "inComment" flag + * and return the remaining content. + */ + private String consume(String line) { + int index = (this.inComment ? endComment(line) : startComment(line)); + return (index == -1 ? null : line.substring(index)); + } + + /** + * Try to consume the {@link #START_COMMENT} token. + * @see #commentToken(String, String, boolean) + */ + private int startComment(String line) { + return commentToken(line, START_COMMENT, true); + } + + private int endComment(String line) { + return commentToken(line, END_COMMENT, false); + } + + /** + * Try to consume the supplied token against the supplied content and update the + * in comment parse state to the supplied value. Returns the index into the content + * which is after the token or -1 if the token is not found. + */ + private int commentToken(String line, String token, boolean inCommentIfPresent) { + int index = line.indexOf(token); + if (index > - 1) { + this.inComment = inCommentIfPresent; + } + return (index == -1 ? index : index + token.length()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/package-info.java b/spring-core/src/main/java/org/springframework/util/xml/package-info.java new file mode 100644 index 00000000..86b0691b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * Miscellaneous utility classes for XML parsing and transformation, + * such as error handlers that log warnings via Commons Logging. + * + */ +package org.springframework.util.xml; + diff --git a/spring-core/src/main/java/overview.html b/spring-core/src/main/java/overview.html new file mode 100644 index 00000000..2acb036f --- /dev/null +++ b/spring-core/src/main/java/overview.html @@ -0,0 +1,7 @@ +<html> +<body> +<p> +Spring's core utilities, used by many other Spring modules. +</p> +</body> +</html>
\ No newline at end of file |