001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.core;
018
019import java.io.Externalizable;
020import java.io.Serializable;
021import java.lang.reflect.Method;
022import java.lang.reflect.Proxy;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.HashSet;
027import java.util.Iterator;
028import java.util.Set;
029
030import org.springframework.util.Assert;
031import org.springframework.util.ClassUtils;
032
033/**
034 * Provides methods to support various naming and other conventions used
035 * throughout the framework. Mainly for internal use within the framework.
036 *
037 * @author Rob Harrop
038 * @author Juergen Hoeller
039 * @since 2.0
040 */
041public abstract class Conventions {
042
043        /**
044         * Suffix added to names when using arrays.
045         */
046        private static final String PLURAL_SUFFIX = "List";
047
048        /**
049         * Set of interfaces that are supposed to be ignored
050         * when searching for the 'primary' interface of a proxy.
051         */
052        private static final Set<Class<?>> IGNORED_INTERFACES;
053
054        static {
055                IGNORED_INTERFACES = Collections.unmodifiableSet(new HashSet<Class<?>>(
056                                Arrays.<Class<?>>asList(Serializable.class, Externalizable.class, Cloneable.class, Comparable.class)));
057        }
058
059
060        /**
061         * Determine the conventional variable name for the supplied
062         * {@code Object} based on its concrete type. The convention
063         * used is to return the uncapitalized short name of the {@code Class},
064         * according to JavaBeans property naming rules: So,
065         * {@code com.myapp.Product} becomes {@code product};
066         * {@code com.myapp.MyProduct} becomes {@code myProduct};
067         * {@code com.myapp.UKProduct} becomes {@code UKProduct}.
068         * <p>For arrays, we use the pluralized version of the array component type.
069         * For {@code Collection}s we attempt to 'peek ahead' in the
070         * {@code Collection} to determine the component type and
071         * return the pluralized version of that component type.
072         * @param value the value to generate a variable name for
073         * @return the generated variable name
074         */
075        public static String getVariableName(Object value) {
076                Assert.notNull(value, "Value must not be null");
077                Class<?> valueClass;
078                boolean pluralize = false;
079
080                if (value.getClass().isArray()) {
081                        valueClass = value.getClass().getComponentType();
082                        pluralize = true;
083                }
084                else if (value instanceof Collection) {
085                        Collection<?> collection = (Collection<?>) value;
086                        if (collection.isEmpty()) {
087                                throw new IllegalArgumentException("Cannot generate variable name for an empty Collection");
088                        }
089                        Object valueToCheck = peekAhead(collection);
090                        valueClass = getClassForValue(valueToCheck);
091                        pluralize = true;
092                }
093                else {
094                        valueClass = getClassForValue(value);
095                }
096
097                String name = ClassUtils.getShortNameAsProperty(valueClass);
098                return (pluralize ? pluralize(name) : name);
099        }
100
101        /**
102         * Determine the conventional variable name for the supplied parameter,
103         * taking the generic collection type (if any) into account.
104         * @param parameter the method or constructor parameter to generate a variable name for
105         * @return the generated variable name
106         */
107        public static String getVariableNameForParameter(MethodParameter parameter) {
108                Assert.notNull(parameter, "MethodParameter must not be null");
109                Class<?> valueClass;
110                boolean pluralize = false;
111
112                if (parameter.getParameterType().isArray()) {
113                        valueClass = parameter.getParameterType().getComponentType();
114                        pluralize = true;
115                }
116                else if (Collection.class.isAssignableFrom(parameter.getParameterType())) {
117                        valueClass = ResolvableType.forMethodParameter(parameter).asCollection().resolveGeneric();
118                        if (valueClass == null) {
119                                throw new IllegalArgumentException(
120                                                "Cannot generate variable name for non-typed Collection parameter type");
121                        }
122                        pluralize = true;
123                }
124                else {
125                        valueClass = parameter.getParameterType();
126                }
127
128                String name = ClassUtils.getShortNameAsProperty(valueClass);
129                return (pluralize ? pluralize(name) : name);
130        }
131
132        /**
133         * Determine the conventional variable name for the return type of the supplied method,
134         * taking the generic collection type (if any) into account.
135         * @param method the method to generate a variable name for
136         * @return the generated variable name
137         */
138        public static String getVariableNameForReturnType(Method method) {
139                return getVariableNameForReturnType(method, method.getReturnType(), null);
140        }
141
142        /**
143         * Determine the conventional variable name for the return type of the supplied method,
144         * taking the generic collection type (if any) into account, falling back to the
145         * given return value if the method declaration is not specific enough (i.e. in case of
146         * the return type being declared as {@code Object} or as untyped collection).
147         * @param method the method to generate a variable name for
148         * @param value the return value (may be {@code null} if not available)
149         * @return the generated variable name
150         */
151        public static String getVariableNameForReturnType(Method method, Object value) {
152                return getVariableNameForReturnType(method, method.getReturnType(), value);
153        }
154
155        /**
156         * Determine the conventional variable name for the return type of the supplied method,
157         * taking the generic collection type (if any) into account, falling back to the
158         * given return value if the method declaration is not specific enough (i.e. in case of
159         * the return type being declared as {@code Object} or as untyped collection).
160         * @param method the method to generate a variable name for
161         * @param resolvedType the resolved return type of the method
162         * @param value the return value (may be {@code null} if not available)
163         * @return the generated variable name
164         */
165        public static String getVariableNameForReturnType(Method method, Class<?> resolvedType, Object value) {
166                Assert.notNull(method, "Method must not be null");
167
168                if (Object.class == resolvedType) {
169                        if (value == null) {
170                                throw new IllegalArgumentException("Cannot generate variable name for an Object return type with null value");
171                        }
172                        return getVariableName(value);
173                }
174
175                Class<?> valueClass;
176                boolean pluralize = false;
177
178                if (resolvedType.isArray()) {
179                        valueClass = resolvedType.getComponentType();
180                        pluralize = true;
181                }
182                else if (Collection.class.isAssignableFrom(resolvedType)) {
183                        valueClass = ResolvableType.forMethodReturnType(method).asCollection().resolveGeneric();
184                        if (valueClass == null) {
185                                if (!(value instanceof Collection)) {
186                                        throw new IllegalArgumentException(
187                                                        "Cannot generate variable name for non-typed Collection return type and a non-Collection value");
188                                }
189                                Collection<?> collection = (Collection<?>) value;
190                                if (collection.isEmpty()) {
191                                        throw new IllegalArgumentException(
192                                                        "Cannot generate variable name for non-typed Collection return type and an empty Collection value");
193                                }
194                                Object valueToCheck = peekAhead(collection);
195                                valueClass = getClassForValue(valueToCheck);
196                        }
197                        pluralize = true;
198                }
199                else {
200                        valueClass = resolvedType;
201                }
202
203                String name = ClassUtils.getShortNameAsProperty(valueClass);
204                return (pluralize ? pluralize(name) : name);
205        }
206
207        /**
208         * Convert {@code String}s in attribute name format (lowercase, hyphens separating words)
209         * into property name format (camel-cased). For example, {@code transaction-manager} is
210         * converted into {@code transactionManager}.
211         */
212        public static String attributeNameToPropertyName(String attributeName) {
213                Assert.notNull(attributeName, "'attributeName' must not be null");
214                if (!attributeName.contains("-")) {
215                        return attributeName;
216                }
217                char[] chars = attributeName.toCharArray();
218                char[] result = new char[chars.length -1]; // not completely accurate but good guess
219                int currPos = 0;
220                boolean upperCaseNext = false;
221                for (char c : chars) {
222                        if (c == '-') {
223                                upperCaseNext = true;
224                        }
225                        else if (upperCaseNext) {
226                                result[currPos++] = Character.toUpperCase(c);
227                                upperCaseNext = false;
228                        }
229                        else {
230                                result[currPos++] = c;
231                        }
232                }
233                return new String(result, 0, currPos);
234        }
235
236        /**
237         * Return an attribute name qualified by the supplied enclosing {@link Class}. For example,
238         * the attribute name '{@code foo}' qualified by {@link Class} '{@code com.myapp.SomeClass}'
239         * would be '{@code com.myapp.SomeClass.foo}'
240         */
241        public static String getQualifiedAttributeName(Class<?> enclosingClass, String attributeName) {
242                Assert.notNull(enclosingClass, "'enclosingClass' must not be null");
243                Assert.notNull(attributeName, "'attributeName' must not be null");
244                return enclosingClass.getName() + '.' + attributeName;
245        }
246
247
248        /**
249         * Determines the class to use for naming a variable that contains
250         * the given value.
251         * <p>Will return the class of the given value, except when
252         * encountering a JDK proxy, in which case it will determine
253         * the 'primary' interface implemented by that proxy.
254         * @param value the value to check
255         * @return the class to use for naming a variable
256         */
257        private static Class<?> getClassForValue(Object value) {
258                Class<?> valueClass = value.getClass();
259                if (Proxy.isProxyClass(valueClass)) {
260                        Class<?>[] ifcs = valueClass.getInterfaces();
261                        for (Class<?> ifc : ifcs) {
262                                if (!IGNORED_INTERFACES.contains(ifc)) {
263                                        return ifc;
264                                }
265                        }
266                }
267                else if (valueClass.getName().lastIndexOf('$') != -1 && valueClass.getDeclaringClass() == null) {
268                        // '$' in the class name but no inner class -
269                        // assuming it's a special subclass (e.g. by OpenJPA)
270                        valueClass = valueClass.getSuperclass();
271                }
272                return valueClass;
273        }
274
275        /**
276         * Pluralize the given name.
277         */
278        private static String pluralize(String name) {
279                return name + PLURAL_SUFFIX;
280        }
281
282        /**
283         * Retrieves the {@code Class} of an element in the {@code Collection}.
284         * The exact element for which the {@code Class} is retrieved will depend
285         * on the concrete {@code Collection} implementation.
286         */
287        private static <E> E peekAhead(Collection<E> collection) {
288                Iterator<E> it = collection.iterator();
289                if (!it.hasNext()) {
290                        throw new IllegalStateException(
291                                        "Unable to peek ahead in non-empty collection - no element found");
292                }
293                E value = it.next();
294                if (value == null) {
295                        throw new IllegalStateException(
296                                        "Unable to peek ahead in non-empty collection - only null element found");
297                }
298                return value;
299        }
300
301}