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 }