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}