/** * Copyright (C) 2015 Valkyrie RCP * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.valkyriercp.binding.support; import org.springframework.beans.BeansException; import org.springframework.beans.PropertyAccessor; import org.springframework.binding.collection.AbstractCachingMapDecorator; import org.springframework.util.Assert; import org.valkyriercp.binding.MutablePropertyAccessStrategy; import org.valkyriercp.binding.PropertyMetadataAccessStrategy; import org.valkyriercp.binding.value.ValueModel; import org.valkyriercp.binding.value.support.AbstractValueModel; import org.valkyriercp.binding.value.support.ValueHolder; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Map; /** * An abstract implementation of <code>MutablePropertyAccessStrategy</code> * that provides support for concrete implementations. * * <p> * As this class delegates to a <code>PropertyAccessor</code> for property * access, the support for type resolution and <b>nested properties</b> depends * on the implementation of the <code>PropertyAccessor</code> * * @author Oliver Hutchison * @author Arne Limburg */ public abstract class AbstractPropertyAccessStrategy implements MutablePropertyAccessStrategy { private final ValueModel domainObjectHolder; private final String basePropertyPath; private final ValueModelCache valueModelCache; private final PropertyMetadataAccessStrategy metaAspectAccessor; /** * Creates a new instance of AbstractPropertyAccessStrategy that will * provide access to the properties of the provided object. * * @param object the object to be accessed through this class. */ public AbstractPropertyAccessStrategy(Object object) { this(new ValueHolder(object)); } /** * Creates a new instance of AbstractPropertyAccessStrategy that will * provide access to the object contained by the provided value model. * * @param domainObjectHolder value model that holds the object to be * accessed through this class */ public AbstractPropertyAccessStrategy(final ValueModel domainObjectHolder) { Assert.notNull(domainObjectHolder, "domainObjectHolder must not be null."); this.domainObjectHolder = domainObjectHolder; this.domainObjectHolder.addValueChangeListener(new DomainObjectChangeListener()); this.basePropertyPath = ""; this.valueModelCache = new ValueModelCache(); this.metaAspectAccessor = new PropertyMetaAspectAccessor(); } /** * Creates a child instance of AbstractPropertyAccessStrategy that will * delegate to its parent for property access. * * @param parent AbstractPropertyAccessStrategy which will be used to * provide property access * @param basePropertyPath property path that will as a base when accessing * the parent AbstractPropertyAccessStrategy */ protected AbstractPropertyAccessStrategy(AbstractPropertyAccessStrategy parent, String basePropertyPath) { this.domainObjectHolder = parent.getPropertyValueModel(basePropertyPath); this.basePropertyPath = basePropertyPath; this.valueModelCache = parent.valueModelCache; this.metaAspectAccessor = new PropertyMetaAspectAccessor(); } /** * Subclasses may override this method to supply user metadata for the * specified <code>propertyPath</code> and <code>key</code>. The * default implementation invokes {@link #getAllUserMetadataFor(String)} and * uses the returned Map with the <code>key</code> parameter to find the * correlated value. * * @param propertyPath path of property relative to this bean * @param key * @return metadata associated with the specified key for the property or * <code>null</code> if there is no custom metadata associated with the * property and key. */ protected Object getUserMetadataFor(String propertyPath, String key) { final Map allMetadata = getAllUserMetadataFor(propertyPath); return allMetadata != null ? allMetadata.get(key) : null; } /** * Subclasses may override this method to supply user metadata for the * specified <code>propertyPath</code>. The default implementation always * returns <code>null</code>. * * @param propertyPath path of property relative to this bean * @return all metadata for the specified property in the form of a Map * containing String keys and Object values. This method may return * <code>null</code> if there is no metadata for the property. */ protected Map getAllUserMetadataFor(String propertyPath) { return null; } protected abstract PropertyAccessor getPropertyAccessor(); public ValueModel getDomainObjectHolder() { return domainObjectHolder; } public ValueModel getPropertyValueModel(String propertyPath) throws BeansException { return (ValueModel) valueModelCache.get(getFullPropertyPath(propertyPath)); } /** * Returns a property path that includes the base property path of the * class. */ protected String getFullPropertyPath(String propertyPath) { if (basePropertyPath.equals("")) { return propertyPath; } else if (propertyPath.equals("")) { return basePropertyPath; } else { return basePropertyPath + '.' + propertyPath; } } /** * Extracts the property name from a propertyPath. */ protected String getPropertyName(String propertyPath) { int lastSeparator = getLastPropertySeparatorIndex(propertyPath); if (lastSeparator == -1) { return propertyPath; } if (propertyPath.charAt(lastSeparator) == PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR) return propertyPath.substring(lastSeparator + 1); return propertyPath.substring(lastSeparator); } /** * Returns the property name component of the provided property path. */ protected String getParentPropertyPath(String propertyPath) { int lastSeparator = getLastPropertySeparatorIndex(propertyPath); return lastSeparator == -1 ? "" : propertyPath.substring(0, lastSeparator); } /** * Returns the index of the last nested property separator in the given * property path, ignoring dots in keys (like "map[my.key]"). */ protected int getLastPropertySeparatorIndex(String propertyPath) { boolean inKey = false; for (int i = propertyPath.length() - 1; i >= 0; i--) { switch (propertyPath.charAt(i)) { case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR: inKey = true; break; case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR: return i; case PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR: if (!inKey) { return i; } break; } } return -1; } public abstract MutablePropertyAccessStrategy getPropertyAccessStrategyForPath(String propertyPath) throws BeansException; public abstract MutablePropertyAccessStrategy newPropertyAccessStrategy(ValueModel domainObjectHolder); public Object getDomainObject() { return domainObjectHolder.getValue(); } public PropertyMetadataAccessStrategy getMetadataAccessStrategy() { return metaAspectAccessor; } public Object getPropertyValue(String propertyPath) throws BeansException { return getPropertyValueModel(propertyPath).getValue(); } /** * Called when the domain object is changed. */ protected abstract void domainObjectChanged(); /** * Keeps beanWrapper up to date with the value held by domainObjectHolder. */ private class DomainObjectChangeListener implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent evt) { domainObjectChanged(); } } /** * A cache of value models generated for specific property paths. */ private class ValueModelCache extends AbstractCachingMapDecorator { protected Object create(Object propertyPath) { String fullPropertyPath = getFullPropertyPath((String) propertyPath); String parentPropertyPath = getParentPropertyPath(fullPropertyPath); ValueModel parentValueModel = parentPropertyPath == "" ? domainObjectHolder : (ValueModel) valueModelCache .get(parentPropertyPath); return new PropertyValueModel(parentValueModel, fullPropertyPath); } } /** * A value model that wraps a single JavaBean property. Delegates to the * beanWrapperr for getting and setting the value. If the wrapped JavaBean * supports publishing property change events this class will also register * a property change listener so that changes to the property made outside * of this value model may also be detected and notified to any value change * listeners registered with this class. */ private class PropertyValueModel extends AbstractValueModel { private final ValueModel parentValueModel; private final String propertyPath; private final String propertyName; private PropertyChangeListener beanPropertyChangeListener; private Object savedParentObject; private Object savedPropertyValue; private boolean settingBeanProperty; public PropertyValueModel(ValueModel parentValueModel, String propertyPath) { this.parentValueModel = parentValueModel; this.parentValueModel.addValueChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { parentValueChanged(); } }); this.propertyPath = propertyPath; this.propertyName = getPropertyName(propertyPath); if (getPropertyAccessor().isReadableProperty(propertyPath)) { this.savedPropertyValue = getPropertyAccessor().getPropertyValue(propertyPath); } updateBeanPropertyChangeListener(); } public Object getValue() { savedPropertyValue = getPropertyAccessor().getPropertyValue(propertyPath); return savedPropertyValue; } public void setValue(Object value) { // TODO: make this thread safe try { settingBeanProperty = true; getPropertyAccessor().setPropertyValue(propertyPath, value); } finally { settingBeanProperty = false; } fireValueChange(savedPropertyValue, value); savedPropertyValue = value; } /** * Called when the parent object changes. */ private void parentValueChanged() { updateBeanPropertyChangeListener(); fireValueChange(savedPropertyValue, getValue()); } /** * Called by the parent JavaBean if it supports PropertyChangeEvent * notifications and the property wrapped by this value model has * changed. */ private void propertyValueChanged() { if (!settingBeanProperty) { fireValueChange(savedPropertyValue, getValue()); } } /** * If the parent JavaBean supports property change notification register * this class as a property change listener. */ private synchronized void updateBeanPropertyChangeListener() { final Object currentParentObject = parentValueModel.getValue(); if (currentParentObject != savedParentObject) { // remove PropertyChangeListener from old parent if (beanPropertyChangeListener != null) { PropertyChangeSupportUtils.removePropertyChangeListener(savedParentObject, propertyName, beanPropertyChangeListener); beanPropertyChangeListener = null; } // install PropertyChangeListener on new parent if (currentParentObject != null && PropertyChangeSupportUtils.supportsBoundProperties(currentParentObject.getClass())) { beanPropertyChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { propertyValueChanged(); } }; PropertyChangeSupportUtils.addPropertyChangeListener(currentParentObject, propertyName, beanPropertyChangeListener); } savedParentObject = currentParentObject; } } } /** * Implementation of PropertyMetadataAccessStrategy that simply delegates to * the beanWrapper. */ private class PropertyMetaAspectAccessor implements PropertyMetadataAccessStrategy { public Class getPropertyType(String propertyPath) { return getPropertyAccessor().getPropertyType(getFullPropertyPath(propertyPath)); } public boolean isReadable(String propertyPath) { return getPropertyAccessor().isReadableProperty(getFullPropertyPath(propertyPath)); } public boolean isWriteable(String propertyPath) { return getPropertyAccessor().isWritableProperty(getFullPropertyPath(propertyPath)); } public Object getUserMetadata(String propertyPath, String key) { return getUserMetadataFor(propertyPath, key); } public Map getAllUserMetadata(String propertyPath) { return getAllUserMetadataFor(propertyPath); } } }