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    }