001 /* 002 * Copyright 2002-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.binding.support; 017 018 import java.beans.PropertyChangeEvent; 019 import java.beans.PropertyChangeListener; 020 import java.util.Map; 021 022 import org.springframework.beans.BeansException; 023 import org.springframework.beans.PropertyAccessor; 024 import org.springframework.binding.MutablePropertyAccessStrategy; 025 import org.springframework.binding.PropertyMetadataAccessStrategy; 026 import org.springframework.binding.value.ValueModel; 027 import org.springframework.binding.value.support.AbstractValueModel; 028 import org.springframework.binding.value.support.ValueHolder; 029 import org.springframework.util.Assert; 030 import org.springframework.util.CachingMapDecorator; 031 032 /** 033 * An abstract implementation of <code>MutablePropertyAccessStrategy</code> 034 * that provides support for concrete implementations. 035 * 036 * <p> 037 * As this class delegates to a <code>PropertyAccessor</code> for property 038 * access, the support for type resolution and <b>nested properties</b> depends 039 * on the implementation of the <code>PropertyAccessor</code> 040 * 041 * @author Oliver Hutchison 042 * @author Arne Limburg 043 */ 044 public abstract class AbstractPropertyAccessStrategy implements MutablePropertyAccessStrategy { 045 046 private final ValueModel domainObjectHolder; 047 048 private final String basePropertyPath; 049 050 private final ValueModelCache valueModelCache; 051 052 private final PropertyMetadataAccessStrategy metaAspectAccessor; 053 054 /** 055 * Creates a new instance of AbstractPropertyAccessStrategy that will 056 * provide access to the properties of the provided object. 057 * 058 * @param object the object to be accessed through this class. 059 */ 060 public AbstractPropertyAccessStrategy(Object object) { 061 this(new ValueHolder(object)); 062 } 063 064 /** 065 * Creates a new instance of AbstractPropertyAccessStrategy that will 066 * provide access to the object contained by the provided value model. 067 * 068 * @param domainObjectHolder value model that holds the object to be 069 * accessed through this class 070 */ 071 public AbstractPropertyAccessStrategy(final ValueModel domainObjectHolder) { 072 Assert.notNull(domainObjectHolder, "domainObjectHolder must not be null."); 073 this.domainObjectHolder = domainObjectHolder; 074 this.domainObjectHolder.addValueChangeListener(new DomainObjectChangeListener()); 075 this.basePropertyPath = ""; 076 this.valueModelCache = new ValueModelCache(); 077 this.metaAspectAccessor = new PropertyMetaAspectAccessor(); 078 } 079 080 /** 081 * Creates a child instance of AbstractPropertyAccessStrategy that will 082 * delegate to its parent for property access. 083 * 084 * @param parent AbstractPropertyAccessStrategy which will be used to 085 * provide property access 086 * @param basePropertyPath property path that will as a base when accessing 087 * the parent AbstractPropertyAccessStrategy 088 */ 089 protected AbstractPropertyAccessStrategy(AbstractPropertyAccessStrategy parent, String basePropertyPath) { 090 this.domainObjectHolder = parent.getPropertyValueModel(basePropertyPath); 091 this.basePropertyPath = basePropertyPath; 092 this.valueModelCache = parent.valueModelCache; 093 this.metaAspectAccessor = new PropertyMetaAspectAccessor(); 094 } 095 096 /** 097 * Subclasses may override this method to supply user metadata for the 098 * specified <code>propertyPath</code> and <code>key</code>. The 099 * default implementation invokes {@link #getAllUserMetadataFor(String)} and 100 * uses the returned Map with the <code>key</code> parameter to find the 101 * correlated value. 102 * 103 * @param propertyPath path of property relative to this bean 104 * @param key 105 * @return metadata associated with the specified key for the property or 106 * <code>null</code> if there is no custom metadata associated with the 107 * property and key. 108 */ 109 protected Object getUserMetadataFor(String propertyPath, String key) { 110 final Map allMetadata = getAllUserMetadataFor(propertyPath); 111 return allMetadata != null ? allMetadata.get(key) : null; 112 } 113 114 /** 115 * Subclasses may override this method to supply user metadata for the 116 * specified <code>propertyPath</code>. The default implementation always 117 * returns <code>null</code>. 118 * 119 * @param propertyPath path of property relative to this bean 120 * @return all metadata for the specified property in the form of a Map 121 * containing String keys and Object values. This method may return 122 * <code>null</code> if there is no metadata for the property. 123 */ 124 protected Map getAllUserMetadataFor(String propertyPath) { 125 return null; 126 } 127 128 protected abstract PropertyAccessor getPropertyAccessor(); 129 130 public ValueModel getDomainObjectHolder() { 131 return domainObjectHolder; 132 } 133 134 public ValueModel getPropertyValueModel(String propertyPath) throws BeansException { 135 return (ValueModel) valueModelCache.get(getFullPropertyPath(propertyPath)); 136 } 137 138 /** 139 * Returns a property path that includes the base property path of the 140 * class. 141 */ 142 protected String getFullPropertyPath(String propertyPath) { 143 if (basePropertyPath.equals("")) { 144 return propertyPath; 145 } 146 else if (propertyPath.equals("")) { 147 return basePropertyPath; 148 } 149 else { 150 return basePropertyPath + '.' + propertyPath; 151 } 152 } 153 154 /** 155 * Extracts the property name from a propertyPath. 156 */ 157 protected String getPropertyName(String propertyPath) { 158 int lastSeparator = getLastPropertySeparatorIndex(propertyPath); 159 if (lastSeparator == -1) { 160 return propertyPath; 161 } 162 if (propertyPath.charAt(lastSeparator) == PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR) 163 return propertyPath.substring(lastSeparator + 1); 164 165 return propertyPath.substring(lastSeparator); 166 } 167 168 /** 169 * Returns the property name component of the provided property path. 170 */ 171 protected String getParentPropertyPath(String propertyPath) { 172 int lastSeparator = getLastPropertySeparatorIndex(propertyPath); 173 return lastSeparator == -1 ? "" : propertyPath.substring(0, lastSeparator); 174 } 175 176 /** 177 * Returns the index of the last nested property separator in the given 178 * property path, ignoring dots in keys (like "map[my.key]"). 179 */ 180 protected int getLastPropertySeparatorIndex(String propertyPath) { 181 boolean inKey = false; 182 for (int i = propertyPath.length() - 1; i >= 0; i--) { 183 switch (propertyPath.charAt(i)) { 184 case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR: 185 inKey = true; 186 break; 187 case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR: 188 return i; 189 case PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR: 190 if (!inKey) { 191 return i; 192 } 193 break; 194 } 195 } 196 return -1; 197 } 198 199 public abstract MutablePropertyAccessStrategy getPropertyAccessStrategyForPath(String propertyPath) 200 throws BeansException; 201 202 public abstract MutablePropertyAccessStrategy newPropertyAccessStrategy(ValueModel domainObjectHolder); 203 204 public Object getDomainObject() { 205 return domainObjectHolder.getValue(); 206 } 207 208 public PropertyMetadataAccessStrategy getMetadataAccessStrategy() { 209 return metaAspectAccessor; 210 } 211 212 public Object getPropertyValue(String propertyPath) throws BeansException { 213 return getPropertyValueModel(propertyPath).getValue(); 214 } 215 216 /** 217 * Called when the domain object is changed. 218 */ 219 protected abstract void domainObjectChanged(); 220 221 /** 222 * Keeps beanWrapper up to date with the value held by domainObjectHolder. 223 */ 224 private class DomainObjectChangeListener implements PropertyChangeListener { 225 226 public void propertyChange(PropertyChangeEvent evt) { 227 domainObjectChanged(); 228 } 229 } 230 231 /** 232 * A cache of value models generated for specific property paths. 233 */ 234 private class ValueModelCache extends CachingMapDecorator { 235 236 protected Object create(Object propertyPath) { 237 String fullPropertyPath = getFullPropertyPath((String) propertyPath); 238 String parentPropertyPath = getParentPropertyPath(fullPropertyPath); 239 ValueModel parentValueModel = parentPropertyPath == "" ? domainObjectHolder : (ValueModel) valueModelCache 240 .get(parentPropertyPath); 241 return new PropertyValueModel(parentValueModel, fullPropertyPath); 242 } 243 } 244 245 /** 246 * A value model that wraps a single JavaBean property. Delegates to the 247 * beanWrapperr for getting and setting the value. If the wrapped JavaBean 248 * supports publishing property change events this class will also register 249 * a property change listener so that changes to the property made outside 250 * of this value model may also be detected and notified to any value change 251 * listeners registered with this class. 252 */ 253 private class PropertyValueModel extends AbstractValueModel { 254 255 private final ValueModel parentValueModel; 256 257 private final String propertyPath; 258 259 private final String propertyName; 260 261 private PropertyChangeListener beanPropertyChangeListener; 262 263 private Object savedParentObject; 264 265 private Object savedPropertyValue; 266 267 private boolean settingBeanProperty; 268 269 public PropertyValueModel(ValueModel parentValueModel, String propertyPath) { 270 this.parentValueModel = parentValueModel; 271 this.parentValueModel.addValueChangeListener(new PropertyChangeListener() { 272 public void propertyChange(PropertyChangeEvent evt) { 273 parentValueChanged(); 274 } 275 }); 276 this.propertyPath = propertyPath; 277 this.propertyName = getPropertyName(propertyPath); 278 if (getPropertyAccessor().isReadableProperty(propertyPath)) { 279 this.savedPropertyValue = getPropertyAccessor().getPropertyValue(propertyPath); 280 } 281 updateBeanPropertyChangeListener(); 282 } 283 284 public Object getValue() { 285 savedPropertyValue = getPropertyAccessor().getPropertyValue(propertyPath); 286 return savedPropertyValue; 287 } 288 289 public void setValue(Object value) { 290 // TODO: make this thread safe 291 try { 292 settingBeanProperty = true; 293 getPropertyAccessor().setPropertyValue(propertyPath, value); 294 } 295 finally { 296 settingBeanProperty = false; 297 } 298 fireValueChange(savedPropertyValue, value); 299 savedPropertyValue = value; 300 } 301 302 /** 303 * Called when the parent object changes. 304 */ 305 private void parentValueChanged() { 306 updateBeanPropertyChangeListener(); 307 fireValueChange(savedPropertyValue, getValue()); 308 } 309 310 /** 311 * Called by the parent JavaBean if it supports PropertyChangeEvent 312 * notifications and the property wrapped by this value model has 313 * changed. 314 */ 315 private void propertyValueChanged() { 316 if (!settingBeanProperty) { 317 fireValueChange(savedPropertyValue, getValue()); 318 } 319 } 320 321 /** 322 * If the parent JavaBean supports property change notification register 323 * this class as a property change listener. 324 */ 325 private synchronized void updateBeanPropertyChangeListener() { 326 final Object currentParentObject = parentValueModel.getValue(); 327 if (currentParentObject != savedParentObject) { 328 // remove PropertyChangeListener from old parent 329 if (beanPropertyChangeListener != null) { 330 PropertyChangeSupportUtils.removePropertyChangeListener(savedParentObject, propertyName, 331 beanPropertyChangeListener); 332 beanPropertyChangeListener = null; 333 } 334 // install PropertyChangeListener on new parent 335 if (currentParentObject != null 336 && PropertyChangeSupportUtils.supportsBoundProperties(currentParentObject.getClass())) { 337 beanPropertyChangeListener = new PropertyChangeListener() { 338 public void propertyChange(PropertyChangeEvent evt) { 339 propertyValueChanged(); 340 } 341 }; 342 PropertyChangeSupportUtils.addPropertyChangeListener(currentParentObject, propertyName, 343 beanPropertyChangeListener); 344 } 345 savedParentObject = currentParentObject; 346 } 347 } 348 } 349 350 /** 351 * Implementation of PropertyMetadataAccessStrategy that simply delegates to 352 * the beanWrapper. 353 */ 354 private class PropertyMetaAspectAccessor implements PropertyMetadataAccessStrategy { 355 356 public Class getPropertyType(String propertyPath) { 357 return getPropertyAccessor().getPropertyType(getFullPropertyPath(propertyPath)); 358 } 359 360 public boolean isReadable(String propertyPath) { 361 return getPropertyAccessor().isReadableProperty(getFullPropertyPath(propertyPath)); 362 } 363 364 public boolean isWriteable(String propertyPath) { 365 return getPropertyAccessor().isWritableProperty(getFullPropertyPath(propertyPath)); 366 } 367 368 public Object getUserMetadata(String propertyPath, String key) { 369 return getUserMetadataFor(propertyPath, key); 370 } 371 372 public Map getAllUserMetadata(String propertyPath) { 373 return getAllUserMetadataFor(propertyPath); 374 } 375 } 376 377 }