001/*
002 * Copyright 2002-2019 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.lang.reflect.Method;
020import java.lang.reflect.Proxy;
021import java.util.Collection;
022import java.util.Iterator;
023
024import org.springframework.lang.Nullable;
025import org.springframework.util.Assert;
026import org.springframework.util.ClassUtils;
027
028/**
029 * Provides methods to support various naming and other conventions used
030 * throughout the framework. Mainly for internal use within the framework.
031 *
032 * @author Rob Harrop
033 * @author Juergen Hoeller
034 * @author Rossen Stoyanchev
035 * @since 2.0
036 */
037public final class Conventions {
038
039        /**
040         * Suffix added to names when using arrays.
041         */
042        private static final String PLURAL_SUFFIX = "List";
043
044
045        private Conventions() {
046        }
047
048
049        /**
050         * Determine the conventional variable name for the supplied {@code Object}
051         * based on its concrete type. The convention used is to return the
052         * un-capitalized short name of the {@code Class}, according to JavaBeans
053         * property naming rules.
054         * <p>For example:<br>
055         * {@code com.myapp.Product} becomes {@code "product"}<br>
056         * {@code com.myapp.MyProduct} becomes {@code "myProduct"}<br>
057         * {@code com.myapp.UKProduct} becomes {@code "UKProduct"}<br>
058         * <p>For arrays the pluralized version of the array component type is used.
059         * For {@code Collection}s an attempt is made to 'peek ahead' to determine
060         * the component type and return its pluralized version.
061         * @param value the value to generate a variable name for
062         * @return the generated variable name
063         */
064        public static String getVariableName(Object value) {
065                Assert.notNull(value, "Value must not be null");
066                Class<?> valueClass;
067                boolean pluralize = false;
068
069                if (value.getClass().isArray()) {
070                        valueClass = value.getClass().getComponentType();
071                        pluralize = true;
072                }
073                else if (value instanceof Collection) {
074                        Collection<?> collection = (Collection<?>) value;
075                        if (collection.isEmpty()) {
076                                throw new IllegalArgumentException(
077                                                "Cannot generate variable name for an empty Collection");
078                        }
079                        Object valueToCheck = peekAhead(collection);
080                        valueClass = getClassForValue(valueToCheck);
081                        pluralize = true;
082                }
083                else {
084                        valueClass = getClassForValue(value);
085                }
086
087                String name = ClassUtils.getShortNameAsProperty(valueClass);
088                return (pluralize ? pluralize(name) : name);
089        }
090
091        /**
092         * Determine the conventional variable name for the given parameter taking
093         * the generic collection type, if any, into account.
094         * <p>As of 5.0 this method supports reactive types:<br>
095         * {@code Mono<com.myapp.Product>} becomes {@code "productMono"}<br>
096         * {@code Flux<com.myapp.MyProduct>} becomes {@code "myProductFlux"}<br>
097         * {@code Observable<com.myapp.MyProduct>} becomes {@code "myProductObservable"}<br>
098         * @param parameter the method or constructor parameter
099         * @return the generated variable name
100         */
101        public static String getVariableNameForParameter(MethodParameter parameter) {
102                Assert.notNull(parameter, "MethodParameter must not be null");
103                Class<?> valueClass;
104                boolean pluralize = false;
105                String reactiveSuffix = "";
106
107                if (parameter.getParameterType().isArray()) {
108                        valueClass = parameter.getParameterType().getComponentType();
109                        pluralize = true;
110                }
111                else if (Collection.class.isAssignableFrom(parameter.getParameterType())) {
112                        valueClass = ResolvableType.forMethodParameter(parameter).asCollection().resolveGeneric();
113                        if (valueClass == null) {
114                                throw new IllegalArgumentException(
115                                                "Cannot generate variable name for non-typed Collection parameter type");
116                        }
117                        pluralize = true;
118                }
119                else {
120                        valueClass = parameter.getParameterType();
121                        ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(valueClass);
122                        if (adapter != null && !adapter.getDescriptor().isNoValue()) {
123                                reactiveSuffix = ClassUtils.getShortName(valueClass);
124                                valueClass = parameter.nested().getNestedParameterType();
125                        }
126                }
127
128                String name = ClassUtils.getShortNameAsProperty(valueClass);
129                return (pluralize ? pluralize(name) : name + reactiveSuffix);
130        }
131
132        /**
133         * Determine the conventional variable name for the return type of the
134         * given method, 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 given
144         * method, taking the generic collection type, if any, into account, falling
145         * back on the given actual return value if the method declaration is not
146         * specific enough, e.g. {@code Object} return type or 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, @Nullable Object value) {
152                return getVariableNameForReturnType(method, method.getReturnType(), value);
153        }
154
155        /**
156         * Determine the conventional variable name for the return type of the given
157         * method, taking the generic collection type, if any, into account, falling
158         * back on the given return value if the method declaration is not specific
159         * enough, e.g. {@code Object} return type or untyped collection.
160         * <p>As of 5.0 this method supports reactive types:<br>
161         * {@code Mono<com.myapp.Product>} becomes {@code "productMono"}<br>
162         * {@code Flux<com.myapp.MyProduct>} becomes {@code "myProductFlux"}<br>
163         * {@code Observable<com.myapp.MyProduct>} becomes {@code "myProductObservable"}<br>
164         * @param method the method to generate a variable name for
165         * @param resolvedType the resolved return type of the method
166         * @param value the return value (may be {@code null} if not available)
167         * @return the generated variable name
168         */
169        public static String getVariableNameForReturnType(Method method, Class<?> resolvedType, @Nullable Object value) {
170                Assert.notNull(method, "Method must not be null");
171
172                if (Object.class == resolvedType) {
173                        if (value == null) {
174                                throw new IllegalArgumentException(
175                                                "Cannot generate variable name for an Object return type with null value");
176                        }
177                        return getVariableName(value);
178                }
179
180                Class<?> valueClass;
181                boolean pluralize = false;
182                String reactiveSuffix = "";
183
184                if (resolvedType.isArray()) {
185                        valueClass = resolvedType.getComponentType();
186                        pluralize = true;
187                }
188                else if (Collection.class.isAssignableFrom(resolvedType)) {
189                        valueClass = ResolvableType.forMethodReturnType(method).asCollection().resolveGeneric();
190                        if (valueClass == null) {
191                                if (!(value instanceof Collection)) {
192                                        throw new IllegalArgumentException("Cannot generate variable name " +
193                                                        "for non-typed Collection return type and a non-Collection value");
194                                }
195                                Collection<?> collection = (Collection<?>) value;
196                                if (collection.isEmpty()) {
197                                        throw new IllegalArgumentException("Cannot generate variable name " +
198                                                        "for non-typed Collection return type and an empty Collection value");
199                                }
200                                Object valueToCheck = peekAhead(collection);
201                                valueClass = getClassForValue(valueToCheck);
202                        }
203                        pluralize = true;
204                }
205                else {
206                        valueClass = resolvedType;
207                        ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(valueClass);
208                        if (adapter != null && !adapter.getDescriptor().isNoValue()) {
209                                reactiveSuffix = ClassUtils.getShortName(valueClass);
210                                valueClass = ResolvableType.forMethodReturnType(method).getGeneric().toClass();
211                        }
212                }
213
214                String name = ClassUtils.getShortNameAsProperty(valueClass);
215                return (pluralize ? pluralize(name) : name + reactiveSuffix);
216        }
217
218        /**
219         * Convert {@code String}s in attribute name format (e.g. lowercase, hyphens
220         * separating words) into property name format (camel-case). For example
221         * {@code transaction-manager} becomes {@code "transactionManager"}.
222         */
223        public static String attributeNameToPropertyName(String attributeName) {
224                Assert.notNull(attributeName, "'attributeName' must not be null");
225                if (!attributeName.contains("-")) {
226                        return attributeName;
227                }
228                char[] chars = attributeName.toCharArray();
229                char[] result = new char[chars.length -1]; // not completely accurate but good guess
230                int currPos = 0;
231                boolean upperCaseNext = false;
232                for (char c : chars) {
233                        if (c == '-') {
234                                upperCaseNext = true;
235                        }
236                        else if (upperCaseNext) {
237                                result[currPos++] = Character.toUpperCase(c);
238                                upperCaseNext = false;
239                        }
240                        else {
241                                result[currPos++] = c;
242                        }
243                }
244                return new String(result, 0, currPos);
245        }
246
247        /**
248         * Return an attribute name qualified by the given enclosing {@link Class}.
249         * For example the attribute name '{@code foo}' qualified by {@link Class}
250         * '{@code com.myapp.SomeClass}' would be '{@code com.myapp.SomeClass.foo}'
251         */
252        public static String getQualifiedAttributeName(Class<?> enclosingClass, String attributeName) {
253                Assert.notNull(enclosingClass, "'enclosingClass' must not be null");
254                Assert.notNull(attributeName, "'attributeName' must not be null");
255                return enclosingClass.getName() + '.' + attributeName;
256        }
257
258
259        /**
260         * Determine the class to use for naming a variable containing the given value.
261         * <p>Will return the class of the given value, except when encountering a
262         * JDK proxy, in which case it will determine the 'primary' interface
263         * implemented by that proxy.
264         * @param value the value to check
265         * @return the class to use for naming a variable
266         */
267        private static Class<?> getClassForValue(Object value) {
268                Class<?> valueClass = value.getClass();
269                if (Proxy.isProxyClass(valueClass)) {
270                        Class<?>[] ifcs = valueClass.getInterfaces();
271                        for (Class<?> ifc : ifcs) {
272                                if (!ClassUtils.isJavaLanguageInterface(ifc)) {
273                                        return ifc;
274                                }
275                        }
276                }
277                else if (valueClass.getName().lastIndexOf('$') != -1 && valueClass.getDeclaringClass() == null) {
278                        // '$' in the class name but no inner class -
279                        // assuming it's a special subclass (e.g. by OpenJPA)
280                        valueClass = valueClass.getSuperclass();
281                }
282                return valueClass;
283        }
284
285        /**
286         * Pluralize the given name.
287         */
288        private static String pluralize(String name) {
289                return name + PLURAL_SUFFIX;
290        }
291
292        /**
293         * Retrieve the {@code Class} of an element in the {@code Collection}.
294         * The exact element for which the {@code Class} is retrieved will depend
295         * on the concrete {@code Collection} implementation.
296         */
297        private static <E> E peekAhead(Collection<E> collection) {
298                Iterator<E> it = collection.iterator();
299                if (!it.hasNext()) {
300                        throw new IllegalStateException(
301                                        "Unable to peek ahead in non-empty collection - no element found");
302                }
303                E value = it.next();
304                if (value == null) {
305                        throw new IllegalStateException(
306                                        "Unable to peek ahead in non-empty collection - only null element found");
307                }
308                return value;
309        }
310
311}