001/*
002 * Copyright 2002-2018 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.convert;
018
019import java.lang.annotation.Annotation;
020import java.lang.reflect.AnnotatedElement;
021import java.lang.reflect.Field;
022import java.lang.reflect.Method;
023import java.util.LinkedHashMap;
024import java.util.Map;
025
026import org.springframework.core.GenericTypeResolver;
027import org.springframework.core.MethodParameter;
028import org.springframework.util.ConcurrentReferenceHashMap;
029import org.springframework.util.ObjectUtils;
030import org.springframework.util.ReflectionUtils;
031import org.springframework.util.StringUtils;
032
033/**
034 * A description of a JavaBeans Property that allows us to avoid a dependency on
035 * {@code java.beans.PropertyDescriptor}. The {@code java.beans} package
036 * is not available in a number of environments (e.g. Android, Java ME), so this is
037 * desirable for portability of Spring's core conversion facility.
038 *
039 * <p>Used to build a {@link TypeDescriptor} from a property location. The built
040 * {@code TypeDescriptor} can then be used to convert from/to the property type.
041 *
042 * @author Keith Donald
043 * @author Phillip Webb
044 * @since 3.1
045 * @see TypeDescriptor#TypeDescriptor(Property)
046 * @see TypeDescriptor#nested(Property, int)
047 */
048public final class Property {
049
050        private static Map<Property, Annotation[]> annotationCache =
051                        new ConcurrentReferenceHashMap<Property, Annotation[]>();
052
053        private final Class<?> objectType;
054
055        private final Method readMethod;
056
057        private final Method writeMethod;
058
059        private final String name;
060
061        private final MethodParameter methodParameter;
062
063        private Annotation[] annotations;
064
065
066        public Property(Class<?> objectType, Method readMethod, Method writeMethod) {
067                this(objectType, readMethod, writeMethod, null);
068        }
069
070        public Property(Class<?> objectType, Method readMethod, Method writeMethod, String name) {
071                this.objectType = objectType;
072                this.readMethod = readMethod;
073                this.writeMethod = writeMethod;
074                this.methodParameter = resolveMethodParameter();
075                this.name = (name != null ? name : resolveName());
076        }
077
078
079        /**
080         * The object declaring this property, either directly or in a superclass the object extends.
081         */
082        public Class<?> getObjectType() {
083                return this.objectType;
084        }
085
086        /**
087         * The name of the property: e.g. 'foo'
088         */
089        public String getName() {
090                return this.name;
091        }
092
093        /**
094         * The property type: e.g. {@code java.lang.String}
095         */
096        public Class<?> getType() {
097                return this.methodParameter.getParameterType();
098        }
099
100        /**
101         * The property getter method: e.g. {@code getFoo()}
102         */
103        public Method getReadMethod() {
104                return this.readMethod;
105        }
106
107        /**
108         * The property setter method: e.g. {@code setFoo(String)}
109         */
110        public Method getWriteMethod() {
111                return this.writeMethod;
112        }
113
114
115        // package private
116
117        MethodParameter getMethodParameter() {
118                return this.methodParameter;
119        }
120
121        Annotation[] getAnnotations() {
122                if (this.annotations == null) {
123                        this.annotations = resolveAnnotations();
124                }
125                return this.annotations;
126        }
127
128
129        // internal helpers
130
131        private String resolveName() {
132                if (this.readMethod != null) {
133                        int index = this.readMethod.getName().indexOf("get");
134                        if (index != -1) {
135                                index += 3;
136                        }
137                        else {
138                                index = this.readMethod.getName().indexOf("is");
139                                if (index == -1) {
140                                        throw new IllegalArgumentException("Not a getter method");
141                                }
142                                index += 2;
143                        }
144                        return StringUtils.uncapitalize(this.readMethod.getName().substring(index));
145                }
146                else {
147                        int index = this.writeMethod.getName().indexOf("set");
148                        if (index == -1) {
149                                throw new IllegalArgumentException("Not a setter method");
150                        }
151                        index += 3;
152                        return StringUtils.uncapitalize(this.writeMethod.getName().substring(index));
153                }
154        }
155
156        private MethodParameter resolveMethodParameter() {
157                MethodParameter read = resolveReadMethodParameter();
158                MethodParameter write = resolveWriteMethodParameter();
159                if (write == null) {
160                        if (read == null) {
161                                throw new IllegalStateException("Property is neither readable nor writeable");
162                        }
163                        return read;
164                }
165                if (read != null) {
166                        Class<?> readType = read.getParameterType();
167                        Class<?> writeType = write.getParameterType();
168                        if (!writeType.equals(readType) && writeType.isAssignableFrom(readType)) {
169                                return read;
170                        }
171                }
172                return write;
173        }
174
175        private MethodParameter resolveReadMethodParameter() {
176                if (getReadMethod() == null) {
177                        return null;
178                }
179                return resolveParameterType(new MethodParameter(getReadMethod(), -1));
180        }
181
182        private MethodParameter resolveWriteMethodParameter() {
183                if (getWriteMethod() == null) {
184                        return null;
185                }
186                return resolveParameterType(new MethodParameter(getWriteMethod(), 0));
187        }
188
189        private MethodParameter resolveParameterType(MethodParameter parameter) {
190                // needed to resolve generic property types that parameterized by sub-classes e.g. T getFoo();
191                GenericTypeResolver.resolveParameterType(parameter, getObjectType());
192                return parameter;
193        }
194
195        private Annotation[] resolveAnnotations() {
196                Annotation[] annotations = annotationCache.get(this);
197                if (annotations == null) {
198                        Map<Class<? extends Annotation>, Annotation> annotationMap =
199                                        new LinkedHashMap<Class<? extends Annotation>, Annotation>();
200                        addAnnotationsToMap(annotationMap, getReadMethod());
201                        addAnnotationsToMap(annotationMap, getWriteMethod());
202                        addAnnotationsToMap(annotationMap, getField());
203                        annotations = annotationMap.values().toArray(new Annotation[annotationMap.size()]);
204                        annotationCache.put(this, annotations);
205                }
206                return annotations;
207        }
208
209        private void addAnnotationsToMap(
210                        Map<Class<? extends Annotation>, Annotation> annotationMap, AnnotatedElement object) {
211
212                if (object != null) {
213                        for (Annotation annotation : object.getAnnotations()) {
214                                annotationMap.put(annotation.annotationType(), annotation);
215                        }
216                }
217        }
218
219        private Field getField() {
220                String name = getName();
221                if (!StringUtils.hasLength(name)) {
222                        return null;
223                }
224                Class<?> declaringClass = declaringClass();
225                Field field = ReflectionUtils.findField(declaringClass, name);
226                if (field == null) {
227                        // Same lenient fallback checking as in CachedIntrospectionResults...
228                        field = ReflectionUtils.findField(declaringClass, StringUtils.uncapitalize(name));
229                        if (field == null) {
230                                field = ReflectionUtils.findField(declaringClass, StringUtils.capitalize(name));
231                        }
232                }
233                return field;
234        }
235
236        private Class<?> declaringClass() {
237                if (getReadMethod() != null) {
238                        return getReadMethod().getDeclaringClass();
239                }
240                else {
241                        return getWriteMethod().getDeclaringClass();
242                }
243        }
244
245
246        @Override
247        public boolean equals(Object other) {
248                if (this == other) {
249                        return true;
250                }
251                if (!(other instanceof Property)) {
252                        return false;
253                }
254                Property otherProperty = (Property) other;
255                return (ObjectUtils.nullSafeEquals(this.objectType, otherProperty.objectType) &&
256                                ObjectUtils.nullSafeEquals(this.name, otherProperty.name) &&
257                                ObjectUtils.nullSafeEquals(this.readMethod, otherProperty.readMethod) &&
258                                ObjectUtils.nullSafeEquals(this.writeMethod, otherProperty.writeMethod));
259        }
260
261        @Override
262        public int hashCode() {
263                return (ObjectUtils.nullSafeHashCode(this.objectType) * 31 + ObjectUtils.nullSafeHashCode(this.name));
264        }
265
266}