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.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.MethodParameter; 027import org.springframework.lang.Nullable; 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 = new ConcurrentReferenceHashMap<>(); 051 052 private final Class<?> objectType; 053 054 @Nullable 055 private final Method readMethod; 056 057 @Nullable 058 private final Method writeMethod; 059 060 private final String name; 061 062 private final MethodParameter methodParameter; 063 064 @Nullable 065 private Annotation[] annotations; 066 067 068 public Property(Class<?> objectType, @Nullable Method readMethod, @Nullable Method writeMethod) { 069 this(objectType, readMethod, writeMethod, null); 070 } 071 072 public Property( 073 Class<?> objectType, @Nullable Method readMethod, @Nullable Method writeMethod, @Nullable String name) { 074 075 this.objectType = objectType; 076 this.readMethod = readMethod; 077 this.writeMethod = writeMethod; 078 this.methodParameter = resolveMethodParameter(); 079 this.name = (name != null ? name : resolveName()); 080 } 081 082 083 /** 084 * The object declaring this property, either directly or in a superclass the object extends. 085 */ 086 public Class<?> getObjectType() { 087 return this.objectType; 088 } 089 090 /** 091 * The name of the property: e.g. 'foo' 092 */ 093 public String getName() { 094 return this.name; 095 } 096 097 /** 098 * The property type: e.g. {@code java.lang.String} 099 */ 100 public Class<?> getType() { 101 return this.methodParameter.getParameterType(); 102 } 103 104 /** 105 * The property getter method: e.g. {@code getFoo()} 106 */ 107 @Nullable 108 public Method getReadMethod() { 109 return this.readMethod; 110 } 111 112 /** 113 * The property setter method: e.g. {@code setFoo(String)} 114 */ 115 @Nullable 116 public Method getWriteMethod() { 117 return this.writeMethod; 118 } 119 120 121 // package private 122 123 MethodParameter getMethodParameter() { 124 return this.methodParameter; 125 } 126 127 Annotation[] getAnnotations() { 128 if (this.annotations == null) { 129 this.annotations = resolveAnnotations(); 130 } 131 return this.annotations; 132 } 133 134 135 // internal helpers 136 137 private String resolveName() { 138 if (this.readMethod != null) { 139 int index = this.readMethod.getName().indexOf("get"); 140 if (index != -1) { 141 index += 3; 142 } 143 else { 144 index = this.readMethod.getName().indexOf("is"); 145 if (index == -1) { 146 throw new IllegalArgumentException("Not a getter method"); 147 } 148 index += 2; 149 } 150 return StringUtils.uncapitalize(this.readMethod.getName().substring(index)); 151 } 152 else if (this.writeMethod != null) { 153 int index = this.writeMethod.getName().indexOf("set"); 154 if (index == -1) { 155 throw new IllegalArgumentException("Not a setter method"); 156 } 157 index += 3; 158 return StringUtils.uncapitalize(this.writeMethod.getName().substring(index)); 159 } 160 else { 161 throw new IllegalStateException("Property is neither readable nor writeable"); 162 } 163 } 164 165 private MethodParameter resolveMethodParameter() { 166 MethodParameter read = resolveReadMethodParameter(); 167 MethodParameter write = resolveWriteMethodParameter(); 168 if (write == null) { 169 if (read == null) { 170 throw new IllegalStateException("Property is neither readable nor writeable"); 171 } 172 return read; 173 } 174 if (read != null) { 175 Class<?> readType = read.getParameterType(); 176 Class<?> writeType = write.getParameterType(); 177 if (!writeType.equals(readType) && writeType.isAssignableFrom(readType)) { 178 return read; 179 } 180 } 181 return write; 182 } 183 184 @Nullable 185 private MethodParameter resolveReadMethodParameter() { 186 if (getReadMethod() == null) { 187 return null; 188 } 189 return new MethodParameter(getReadMethod(), -1).withContainingClass(getObjectType()); 190 } 191 192 @Nullable 193 private MethodParameter resolveWriteMethodParameter() { 194 if (getWriteMethod() == null) { 195 return null; 196 } 197 return new MethodParameter(getWriteMethod(), 0).withContainingClass(getObjectType()); 198 } 199 200 private Annotation[] resolveAnnotations() { 201 Annotation[] annotations = annotationCache.get(this); 202 if (annotations == null) { 203 Map<Class<? extends Annotation>, Annotation> annotationMap = new LinkedHashMap<>(); 204 addAnnotationsToMap(annotationMap, getReadMethod()); 205 addAnnotationsToMap(annotationMap, getWriteMethod()); 206 addAnnotationsToMap(annotationMap, getField()); 207 annotations = annotationMap.values().toArray(new Annotation[0]); 208 annotationCache.put(this, annotations); 209 } 210 return annotations; 211 } 212 213 private void addAnnotationsToMap( 214 Map<Class<? extends Annotation>, Annotation> annotationMap, @Nullable AnnotatedElement object) { 215 216 if (object != null) { 217 for (Annotation annotation : object.getAnnotations()) { 218 annotationMap.put(annotation.annotationType(), annotation); 219 } 220 } 221 } 222 223 @Nullable 224 private Field getField() { 225 String name = getName(); 226 if (!StringUtils.hasLength(name)) { 227 return null; 228 } 229 Field field = null; 230 Class<?> declaringClass = declaringClass(); 231 if (declaringClass != null) { 232 field = ReflectionUtils.findField(declaringClass, name); 233 if (field == null) { 234 // Same lenient fallback checking as in CachedIntrospectionResults... 235 field = ReflectionUtils.findField(declaringClass, StringUtils.uncapitalize(name)); 236 if (field == null) { 237 field = ReflectionUtils.findField(declaringClass, StringUtils.capitalize(name)); 238 } 239 } 240 } 241 return field; 242 } 243 244 @Nullable 245 private Class<?> declaringClass() { 246 if (getReadMethod() != null) { 247 return getReadMethod().getDeclaringClass(); 248 } 249 else if (getWriteMethod() != null) { 250 return getWriteMethod().getDeclaringClass(); 251 } 252 else { 253 return null; 254 } 255 } 256 257 258 @Override 259 public boolean equals(@Nullable Object other) { 260 if (this == other) { 261 return true; 262 } 263 if (!(other instanceof Property)) { 264 return false; 265 } 266 Property otherProperty = (Property) other; 267 return (ObjectUtils.nullSafeEquals(this.objectType, otherProperty.objectType) && 268 ObjectUtils.nullSafeEquals(this.name, otherProperty.name) && 269 ObjectUtils.nullSafeEquals(this.readMethod, otherProperty.readMethod) && 270 ObjectUtils.nullSafeEquals(this.writeMethod, otherProperty.writeMethod)); 271 } 272 273 @Override 274 public int hashCode() { 275 return (ObjectUtils.nullSafeHashCode(this.objectType) * 31 + ObjectUtils.nullSafeHashCode(this.name)); 276 } 277 278}