001 /* 002 * Copyright 2007 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 005 * use this file except in compliance with the License. You may obtain a copy of 006 * the License at 007 * 008 * http://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, WITHOUT 012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 013 * License for the specific language governing permissions and limitations under 014 * the License. 015 */ 016 package org.springframework.richclient.beans; 017 018 import java.beans.PropertyEditor; 019 import java.io.ByteArrayOutputStream; 020 import java.io.PrintWriter; 021 import java.lang.reflect.Array; 022 import java.lang.reflect.Field; 023 import java.lang.reflect.Member; 024 import java.lang.reflect.Method; 025 import java.lang.reflect.Modifier; 026 import java.util.ArrayList; 027 import java.util.Collection; 028 import java.util.HashMap; 029 import java.util.HashSet; 030 import java.util.List; 031 import java.util.Map; 032 import java.util.Set; 033 034 import org.springframework.beans.AbstractPropertyAccessor; 035 import org.springframework.beans.BeansException; 036 import org.springframework.beans.InvalidPropertyException; 037 import org.springframework.beans.NotReadablePropertyException; 038 import org.springframework.core.JdkVersion; 039 import org.springframework.core.MethodParameter; 040 import org.springframework.richclient.core.GenericCollectionTypeResolver; 041 import org.springframework.util.Assert; 042 043 /** 044 * PropertyAccessor implementation that determines property types by field, if 045 * available. Otherwise methods are used. 046 * 047 * Actual access to properties is left for implementation for subclasses. 048 * 049 * This implementation does not support nested properties. Use 050 * {@link AbstractNestedMemberPropertyAccessor}, if you need nested 051 * property-support. 052 * 053 * @author Arne Limburg 054 * 055 */ 056 public abstract class AbstractMemberPropertyAccessor extends AbstractPropertyAccessor { 057 058 private Class targetClass; 059 060 private final boolean fieldAccessEnabled; 061 062 private final Map readAccessors = new HashMap(); 063 064 private final Map writeAccessors = new HashMap(); 065 066 /** 067 * Creates a new <tt>AbstractMemberPropertyAccessor</tt>. 068 * @param targetClass the target class. 069 * @param fieldAccessEnabled whether field access should be used for 070 * property type determination. 071 */ 072 protected AbstractMemberPropertyAccessor(Class targetClass, boolean fieldAccessEnabled) { 073 this.fieldAccessEnabled = fieldAccessEnabled; 074 registerDefaultEditors(); 075 setTargetClass(targetClass); 076 } 077 078 /** 079 * Clears all cached members and introspect methods again. If fieldAccess is 080 * enabled introspect fields as well. 081 * 082 * @param targetClass the target class. 083 */ 084 protected void setTargetClass(Class targetClass) { 085 this.targetClass = targetClass; 086 this.readAccessors.clear(); 087 this.writeAccessors.clear(); 088 introspectMethods(targetClass, new HashSet()); 089 if (isFieldAccessEnabled()) { 090 introspectFields(targetClass, new HashSet()); 091 } 092 } 093 094 /** 095 * Introspect fields of a class. This excludes static fields and handles 096 * final fields as readOnly. 097 * 098 * @param type the class to inspect. 099 * @param introspectedClasses a set of already inspected classes. 100 */ 101 private void introspectFields(Class type, Set introspectedClasses) { 102 if (type == null || Object.class.equals(type) || type.isInterface() || introspectedClasses.contains(type)) { 103 return; 104 } 105 introspectedClasses.add(type); 106 introspectFields(type.getSuperclass(), introspectedClasses); 107 Field[] fields = type.getDeclaredFields(); 108 for (int i = 0; i < fields.length; i++) { 109 if (!Modifier.isStatic(fields[i].getModifiers())) { 110 readAccessors.put(fields[i].getName(), fields[i]); 111 if (!Modifier.isFinal(fields[i].getModifiers())) { 112 writeAccessors.put(fields[i].getName(), fields[i]); 113 } 114 } 115 } 116 } 117 118 /** 119 * Introspect class for accessor methods. This includes methods starting 120 * with 'get', 'set' and 'is'. 121 * 122 * @param type class to introspect. 123 * @param introspectedClasses set of already inspected classes. 124 */ 125 private void introspectMethods(Class type, Set introspectedClasses) { 126 if (type == null || Object.class.equals(type) || introspectedClasses.contains(type)) { 127 return; 128 } 129 introspectedClasses.add(type); 130 Class[] interfaces = type.getInterfaces(); 131 for (int i = 0; i < interfaces.length; i++) { 132 introspectMethods(interfaces[i], introspectedClasses); 133 } 134 introspectMethods(type.getSuperclass(), introspectedClasses); 135 Method[] methods = type.getDeclaredMethods(); 136 for (int i = 0; i < methods.length; i++) { 137 String methodName = methods[i].getName(); 138 if (methodName.startsWith("get") && methods[i].getParameterTypes().length == 0) { 139 readAccessors.put(getPropertyName(methodName, 3), methods[i]); 140 } 141 else if (methodName.startsWith("is") && methods[i].getParameterTypes().length == 0) { 142 readAccessors.put(getPropertyName(methodName, 2), methods[i]); 143 } 144 else if (methodName.startsWith("set") && methods[i].getParameterTypes().length == 1) { 145 writeAccessors.put(getPropertyName(methodName, 3), methods[i]); 146 } 147 } 148 } 149 150 /** 151 * Returns whether this PropertyAccessor should inspect fields. 152 */ 153 public boolean isFieldAccessEnabled() { 154 return fieldAccessEnabled; 155 } 156 157 /** 158 * Returns the class used to introspect members. 159 */ 160 public Class getTargetClass() { 161 return targetClass; 162 } 163 164 /** 165 * Return the read accessor for the given property. 166 * 167 * @param propertyName name of the property. 168 * @return a Member to read the property or <code>null</code>. 169 */ 170 protected Member getReadPropertyAccessor(String propertyName) { 171 return (Member) readAccessors.get(propertyName); 172 } 173 174 /** 175 * Return the write accessor for the given property. 176 * 177 * @param propertyName name of the property. 178 * @return a Member to write the property or <code>null</code>. 179 */ 180 protected Member getWritePropertyAccessor(String propertyName) { 181 return (Member) writeAccessors.get(propertyName); 182 } 183 184 /** 185 * Return any accessor, be it read or write, for the given property. 186 * 187 * @param propertyName name of the property. 188 * @return an accessor for the property or <code>null</code> 189 */ 190 protected Member getPropertyAccessor(String propertyName) { 191 if (readAccessors.containsKey(propertyName)) { 192 return (Member) readAccessors.get(propertyName); 193 } 194 else { 195 return (Member) writeAccessors.get(propertyName); 196 } 197 } 198 199 /** 200 * {@inheritDoc} 201 */ 202 public boolean isReadableProperty(String propertyName) { 203 if (PropertyAccessorUtils.isIndexedProperty(propertyName)) { 204 String rootProperty = getRootPropertyName(propertyName); 205 String parentProperty = getParentPropertyName(propertyName); 206 return isReadableProperty(rootProperty) 207 && checkKeyTypes(propertyName) 208 && (!getPropertyType(parentProperty).isArray() || checkSize(propertyName) || isWritableProperty(parentProperty)) 209 && ((isReadableProperty(parentProperty) && getPropertyValue(parentProperty) != null) || isWritableProperty(parentProperty)); 210 } 211 else { 212 return readAccessors.containsKey(propertyName); 213 } 214 } 215 216 /** 217 * {@inheritDoc} 218 */ 219 public boolean isWritableProperty(String propertyName) { 220 if (PropertyAccessorUtils.isIndexedProperty(propertyName)) { 221 // if an indexed property is readable it is writable, too 222 return isReadableProperty(propertyName); 223 } 224 else { 225 return writeAccessors.containsKey(propertyName); 226 } 227 } 228 229 /** 230 * {@inheritDoc} 231 */ 232 public Class getPropertyType(String propertyName) { 233 if (PropertyAccessorUtils.isIndexedProperty(propertyName)) { 234 int nestingLevel = PropertyAccessorUtils.getNestingLevel(propertyName); 235 if (JdkVersion.isAtLeastJava15()) { 236 Member accessor = getPropertyAccessor(getRootPropertyName(propertyName)); 237 if (accessor instanceof Field) { 238 return GenericCollectionTypeResolver.getIndexedValueFieldType((Field) accessor, nestingLevel); 239 } 240 else { 241 Method accessorMethod = (Method) accessor; 242 MethodParameter parameter = new MethodParameter(accessorMethod, 243 accessorMethod.getParameterTypes().length - 1); 244 return GenericCollectionTypeResolver.getIndexedValueMethodType(parameter, nestingLevel); 245 } 246 } 247 else { 248 // we can only resolve array types in Java 1.4 249 Class type = getPropertyType(getRootPropertyName(propertyName)); 250 for (int i = 0; i < nestingLevel; i++) { 251 if (type.isArray()) { 252 type = type.getComponentType(); 253 } 254 else { 255 return Object.class; // cannot resolve type 256 } 257 } 258 return type; 259 } 260 } 261 else { 262 Member readAccessor = (Member) readAccessors.get(propertyName); 263 if (readAccessor instanceof Field) { 264 return ((Field) readAccessor).getType(); 265 } 266 else if (readAccessor instanceof Method) { 267 return ((Method) readAccessor).getReturnType(); 268 } 269 Member writeAccessor = (Member) writeAccessors.get(propertyName); 270 if (writeAccessor instanceof Field) { 271 return ((Field) writeAccessor).getType(); 272 } 273 else if (writeAccessor instanceof Method) { 274 return ((Method) writeAccessor).getParameterTypes()[0]; 275 } 276 } 277 return null; 278 } 279 280 /** 281 * Determine the type of the key used to index the collection/map. When jdk 282 * is at least 1.5, maps can be specified with generics and their key type 283 * can be resolved. 284 * 285 * @param propertyName name of the property. 286 * @return the type of the key. An integer if it's not a map, {@link String} 287 * if the jdk is less than 1.5, a specific type if the map was generified. 288 */ 289 public Class getIndexedPropertyKeyType(String propertyName) { 290 if (!PropertyAccessorUtils.isIndexedProperty(propertyName)) { 291 throw new IllegalArgumentException("'" + propertyName + "' is no indexed property"); 292 } 293 Class type = getPropertyType(getParentPropertyName(propertyName)); 294 if (!Map.class.isAssignableFrom(type)) { 295 return Integer.class; 296 } 297 if (JdkVersion.isAtLeastJava15()) { 298 int nestingLevel = PropertyAccessorUtils.getNestingLevel(propertyName) - 1; 299 Member accessor = getPropertyAccessor(getRootPropertyName(propertyName)); 300 if (accessor instanceof Field) { 301 return GenericCollectionTypeResolver.getMapKeyFieldType((Field) accessor, nestingLevel); 302 } 303 else if (accessor instanceof Method) { 304 MethodParameter parameter = new MethodParameter((Method) accessor, ((Method) accessor) 305 .getParameterTypes().length - 1, nestingLevel); 306 return GenericCollectionTypeResolver.getMapKeyParameterType(parameter); 307 } 308 else { 309 throw new InvalidPropertyException(getTargetClass(), propertyName, "property not accessable"); 310 } 311 } 312 else { 313 return String.class; // the default for Java 1.4 314 } 315 } 316 317 /** 318 * {@inheritDoc} 319 */ 320 public Object getPropertyValue(String propertyName) throws BeansException { 321 if (PropertyAccessorUtils.isIndexedProperty(propertyName)) { 322 return getIndexedPropertyValue(propertyName); 323 } 324 else { 325 return getSimplePropertyValue(propertyName); 326 } 327 } 328 329 /** 330 * {@inheritDoc} 331 */ 332 public void setPropertyValue(String propertyName, Object value) throws BeansException { 333 if (PropertyAccessorUtils.isIndexedProperty(propertyName)) { 334 setIndexedPropertyValue(propertyName, value); 335 } 336 else { 337 setSimplePropertyValue(propertyName, value); 338 } 339 } 340 341 /** 342 * Retrieve the value of an indexed property. 343 * 344 * @param propertyName name of the property. 345 * @return value of the property. 346 */ 347 protected abstract Object getIndexedPropertyValue(String propertyName); 348 349 /** 350 * Retrieve the value of a simple property (non-indexed). 351 * 352 * @param propertyName name of the property. 353 * @return value of the property. 354 */ 355 protected abstract Object getSimplePropertyValue(String propertyName); 356 357 /** 358 * Set the value of an indexed property. 359 * 360 * @param propertyName name of the property. 361 * @param value new value for the property. 362 */ 363 protected abstract void setIndexedPropertyValue(String propertyName, Object value); 364 365 /** 366 * Set the value of a simple property (non-indexed). 367 * 368 * @param propertyName name of the property. 369 * @param value new value for the property. 370 */ 371 protected abstract void setSimplePropertyValue(String propertyName, Object value); 372 373 /** 374 * Returns the propertyName based on the methodName. Cuts of the prefix and 375 * removes first capital. 376 * 377 * @param methodName name of method to convert. 378 * @param prefixLength length of prefix to cut of. 379 * @return property name. 380 */ 381 protected String getPropertyName(String methodName, int prefixLength) { 382 return Character.toLowerCase(methodName.charAt(prefixLength)) + methodName.substring(prefixLength + 1); 383 } 384 385 /** 386 * Returns the root property of an indexed property. The root property is 387 * the property that contains no indices. 388 * 389 * @param propertyName the name of the property. 390 * @return the root property. 391 */ 392 protected String getRootPropertyName(String propertyName) { 393 int location = propertyName.indexOf(PROPERTY_KEY_PREFIX); 394 return location == -1 ? propertyName : propertyName.substring(0, location); 395 } 396 397 /** 398 * Return the parent property name of an indexed property or the empty string. 399 * 400 * @param propertyName the name of the property. 401 * @return the empty string or the parent property name if it was indexed. 402 */ 403 protected String getParentPropertyName(String propertyName) { 404 if (!PropertyAccessorUtils.isIndexedProperty(propertyName)) { 405 return ""; 406 } 407 else { 408 return propertyName.substring(0, propertyName.lastIndexOf(PROPERTY_KEY_PREFIX_CHAR)); 409 } 410 } 411 412 protected boolean checkKeyTypes(String propertyName) { 413 try { 414 getIndices(propertyName); 415 return true; 416 } 417 catch (Exception e) { 418 return false; 419 } 420 } 421 422 protected Object[] getIndices(String propertyName) { 423 int location = propertyName.indexOf(PROPERTY_KEY_PREFIX); 424 if (location == -1) { 425 return new Object[0]; 426 } 427 String[] indexStrings = split(propertyName.substring(location)); 428 Object[] indices = new Object[indexStrings.length]; 429 String rootPropertyName = getRootPropertyName(propertyName); 430 String indexedPropertyName = rootPropertyName; 431 for (int i = 0; i < indices.length; i++) { 432 indexedPropertyName += '[' + indexStrings[i] + ']'; 433 Class keyType = getIndexedPropertyKeyType(indexedPropertyName); 434 indices[i] = convert(keyType, indexStrings[i]); 435 } 436 return indices; 437 } 438 439 private Object convert(Class targetClass, String value) { 440 if (Object.class.equals(targetClass) || String.class.equals(targetClass)) { 441 return value; 442 } 443 PropertyEditor editor = getDefaultEditor(targetClass); 444 editor.setAsText(value); 445 return editor.getValue(); 446 } 447 448 private String[] split(String indices) { 449 Assert.isTrue(indices.startsWith(PROPERTY_KEY_PREFIX)); 450 Assert.isTrue(indices.endsWith(PROPERTY_KEY_SUFFIX)); 451 List result = new ArrayList(); 452 int fromIndex = 1; 453 int toIndex = -1; 454 while ((toIndex = indices.indexOf("][", fromIndex)) != -1) { 455 result.add(indices.substring(fromIndex, toIndex)); 456 fromIndex = toIndex + 2; 457 } 458 result.add(indices.substring(fromIndex, indices.length() - 1)); 459 return (String[]) result.toArray(new String[result.size()]); 460 } 461 462 private boolean checkSize(String propertyName) { 463 String parentPropertyName = getParentPropertyName(propertyName); 464 if (!getPropertyType(parentPropertyName).isArray()) { 465 try { 466 // collections are considered to be expandable 467 // so if it is not null, any index matches 468 return getPropertyValue(parentPropertyName) != null; 469 } 470 catch (NotReadablePropertyException e) { 471 return false; 472 } 473 } 474 int from = propertyName.lastIndexOf(PROPERTY_KEY_PREFIX_CHAR) + 1; 475 int to = propertyName.length() - 1; 476 int index = Integer.parseInt(propertyName.substring(from, to)); 477 try { 478 Object parentProperty = getPropertyValue(parentPropertyName); 479 return parentProperty != null && Array.getLength(parentProperty) > index; 480 } 481 catch (NotReadablePropertyException e) { 482 return false; 483 } 484 } 485 486 protected NotReadablePropertyException createNotReadablePropertyException(String propertyName, Exception e) { 487 if (JdkVersion.isAtLeastJava14()) { 488 NotReadablePropertyException beanException = new NotReadablePropertyException(getTargetClass(), 489 propertyName); 490 beanException.initCause(e); 491 return beanException; 492 } 493 else { 494 ByteArrayOutputStream stackTrace = new ByteArrayOutputStream(); 495 PrintWriter stackTraceWriter = new PrintWriter(stackTrace); 496 e.printStackTrace(stackTraceWriter); 497 stackTraceWriter.close(); 498 return new NotReadablePropertyException(getTargetClass(), propertyName, 499 new String(stackTrace.toByteArray())); 500 } 501 } 502 503 /** 504 * Helper method for subclasses to set values of indexed properties, like 505 * map-values, collection-values or array-values. 506 * 507 * @param assemblageType either map or collection or array 508 * @param assemblage the assemblage to set the value on 509 * @param index the index to set the value at 510 * @param value the value to set 511 * @return the assemblage 512 */ 513 protected Object setAssemblageValue(Class assemblageType, Object assemblage, Object index, Object value) { 514 if (assemblageType.isArray()) { 515 int i = ((Integer) index).intValue(); 516 if (Array.getLength(assemblage) <= i) { 517 Object newAssemblage = Array.newInstance(assemblageType.getComponentType(), i + 1); 518 System.arraycopy(assemblage, 0, newAssemblage, 0, Array.getLength(assemblage)); 519 assemblage = newAssemblage; 520 } 521 Array.set(assemblage, i, value); 522 } 523 else if (List.class.isAssignableFrom(assemblageType)) { 524 int i = ((Integer) index).intValue(); 525 List list = (List) assemblage; 526 if (list.size() > i) { 527 list.set(i, value); 528 } 529 else { 530 while (list.size() < i) { 531 list.add(null); 532 } 533 list.add(value); 534 } 535 } 536 else if (Map.class.isAssignableFrom(assemblageType)) { 537 ((Map) assemblage).put(index, value); 538 } 539 else if (assemblage instanceof Collection) { 540 ((Collection) assemblage).add(value); 541 } 542 else { 543 throw new IllegalArgumentException("assemblage must be of type array, collection or map."); 544 } 545 return assemblage; 546 } 547 }