/* * Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved. * * This file is part of the Jspresso framework. * * Jspresso is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Jspresso is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Jspresso. If not, see <http://www.gnu.org/licenses/>. */ package org.jspresso.framework.util.bean; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListenerProxy; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.jspresso.framework.util.accessor.IAccessor; import org.jspresso.framework.util.accessor.IAccessorFactory; import org.jspresso.framework.util.accessor.basic.BasicAccessorFactory; import org.jspresso.framework.util.accessor.bean.BeanAccessorFactory; import org.jspresso.framework.util.accessor.map.MapAccessorFactory; import org.jspresso.framework.util.exception.NestedRuntimeException; import org.jspresso.framework.util.lang.ICloneable; /** * Abstract class to build a property change capable bean. * * @author Vincent Vandenschrick * @internal */ public abstract class AbstractPropertyChangeCapable implements IPropertyChangeCapable, ICloneable { private final static IAccessorFactory ACCESSOR_FACTORY; private transient SinglePropertyChangeSupport propertyChangeSupport; private transient SingleWeakPropertyChangeSupport weakPropertyChangeSupport; private transient List<PropertyChangeEvent> delayedEvents; private transient Map<String, NestedPropertyChangeListener> nestedPropertyChangeListeners; static { ACCESSOR_FACTORY = new BasicAccessorFactory(); ((BasicAccessorFactory) ACCESSOR_FACTORY).setBeanAccessorFactory(new BeanAccessorFactory()); ((BasicAccessorFactory) ACCESSOR_FACTORY).setMapAccessorFactory(new MapAccessorFactory()); } private synchronized void initializePropertyChangeSupportIfNeeded() { if (propertyChangeSupport == null) { this.propertyChangeSupport = new SinglePropertyChangeSupport(this); } } private synchronized void initializeNestedPropertyChangeListenersIfNeeded() { if (nestedPropertyChangeListeners == null) { this.nestedPropertyChangeListeners = new HashMap<>(); } } private synchronized void initializeWeakPropertyChangeSupportIfNeeded() { if (weakPropertyChangeSupport == null) { this.weakPropertyChangeSupport = new SingleWeakPropertyChangeSupport(this); } } /** * {@inheritDoc} */ @Override public void addPropertyChangeListener(PropertyChangeListener listener) { initializePropertyChangeSupportIfNeeded(); propertyChangeSupport.addPropertyChangeListener(listener); } /** * {@inheritDoc} */ @Override public void addWeakPropertyChangeListener(PropertyChangeListener listener) { initializeWeakPropertyChangeSupportIfNeeded(); weakPropertyChangeSupport.addPropertyChangeListener(listener); } private void registerNestedPropertyChangeListenerIfNeeded(String propertyName) { int nestedDelimiterIndex = propertyName.indexOf(IAccessor.NESTED_DELIM); if (nestedDelimiterIndex >= 0) { initializeNestedPropertyChangeListenersIfNeeded(); String rootPropertyName = propertyName.substring(0, nestedDelimiterIndex); final String remainderProperty = propertyName.substring(nestedDelimiterIndex + 1); final AbstractPropertyChangeCapable.NestedPropertyChangeListener rootNestedPropertyListener; if (nestedPropertyChangeListeners.containsKey(rootPropertyName)) { rootNestedPropertyListener = nestedPropertyChangeListeners.get(rootPropertyName); } else { rootNestedPropertyListener = new AbstractPropertyChangeCapable.NestedPropertyChangeListener(rootPropertyName); nestedPropertyChangeListeners.put(rootPropertyName, rootNestedPropertyListener); } rootNestedPropertyListener.registerNestedProperty(remainderProperty); Object rootProperty; try { rootProperty = ACCESSOR_FACTORY.createPropertyAccessor(rootPropertyName, getClass()).getValue(this); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) { throw new NestedRuntimeException(ex, "Could not attach listener on property : " + rootPropertyName); } if (rootProperty instanceof IPropertyChangeCapable) { ((IPropertyChangeCapable) rootProperty).addPropertyChangeListener(remainderProperty, rootNestedPropertyListener); } addPropertyChangeListener(rootPropertyName, new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { Object oldValue = evt.getOldValue(); Object newValue = evt.getNewValue(); if (oldValue instanceof IPropertyChangeCapable) { ((IPropertyChangeCapable) oldValue).removePropertyChangeListener(remainderProperty, rootNestedPropertyListener); } if (newValue instanceof IPropertyChangeCapable) { ((IPropertyChangeCapable) newValue).addPropertyChangeListener(remainderProperty, rootNestedPropertyListener); } rootNestedPropertyListener.fireAllNestedPropertyChanges(oldValue, newValue); } }); } } /** * {@inheritDoc} */ @Override public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { initializePropertyChangeSupportIfNeeded(); registerNestedPropertyChangeListenerIfNeeded(propertyName); propertyChangeSupport.addPropertyChangeListener(propertyName, listener); } /** * {@inheritDoc} */ @Override public void addWeakPropertyChangeListener(String propertyName, PropertyChangeListener listener) { initializeWeakPropertyChangeSupportIfNeeded(); registerNestedPropertyChangeListenerIfNeeded(propertyName); weakPropertyChangeSupport.addPropertyChangeListener(propertyName, listener); } /** * {@inheritDoc} */ @Override public AbstractPropertyChangeCapable clone() { try { AbstractPropertyChangeCapable clonedBean = (AbstractPropertyChangeCapable) super.clone(); clonedBean.propertyChangeSupport = null; clonedBean.weakPropertyChangeSupport = null; clonedBean.nestedPropertyChangeListeners = null; clonedBean.delayedEvents = null; return clonedBean; } catch (CloneNotSupportedException ex) { throw new NestedRuntimeException(ex); } } /** * {@inheritDoc} */ @Override public void removePropertyChangeListener(PropertyChangeListener listener) { if (propertyChangeSupport != null) { propertyChangeSupport.removePropertyChangeListener(listener); } if (weakPropertyChangeSupport != null) { weakPropertyChangeSupport.removePropertyChangeListener(listener); } } /** * {@inheritDoc} */ @Override public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { if (propertyChangeSupport != null) { propertyChangeSupport.removePropertyChangeListener(propertyName, listener); } if (weakPropertyChangeSupport != null) { weakPropertyChangeSupport.removePropertyChangeListener(propertyName, listener); } } /** * Performs property change firing. * * @param evt * evt * @see java.beans.PropertyChangeSupport#firePropertyChange(java.beans.PropertyChangeEvent) */ protected void firePropertyChange(PropertyChangeEvent evt) { Object oldValue = evt.getOldValue(); Object newValue = evt.getNewValue(); if (oldValue == null && newValue == null || oldValue != null && newValue != null && oldValue.equals(newValue)) { return; } if (delayedEvents != null) { delayedEvents.add(evt); } else { if (propertyChangeSupport != null) { propertyChangeSupport.firePropertyChange(evt); } if (weakPropertyChangeSupport != null) { weakPropertyChangeSupport.firePropertyChange(evt); } } } /** * Performs property change firing. * * @param propertyName * propertyName * @param oldValue * oldValue * @param newValue * newValue * @see java.beans.PropertyChangeSupport#firePropertyChange(java.lang.String, * boolean, boolean) */ protected void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) { firePropertyChange(new PropertyChangeEvent(this, propertyName, oldValue, newValue)); } /** * Performs property change firing. * * @param propertyName * propertyName * @param oldValue * oldValue * @param newValue * newValue * @see java.beans.PropertyChangeSupport#firePropertyChange(java.lang.String, * int, int) */ protected void firePropertyChange(String propertyName, int oldValue, int newValue) { firePropertyChange(new PropertyChangeEvent(this, propertyName, oldValue, newValue)); } /** * Performs property change firing. * * @param propertyName * propertyName * @param oldValue * oldValue * @param newValue * newValue * @see java.beans.PropertyChangeSupport#firePropertyChange(java.lang.String, * int, int) */ protected void firePropertyChange(String propertyName, long oldValue, long newValue) { firePropertyChange(new PropertyChangeEvent(this, propertyName, oldValue, newValue)); } /** * Performs property change firing. * * @param propertyName * propertyName * @param oldValue * oldValue * @param newValue * newValue * @see java.beans.PropertyChangeSupport#firePropertyChange(java.lang.String, * java.lang.Object, java.lang.Object) */ @Override public void firePropertyChange(String propertyName, Object oldValue, Object newValue) { firePropertyChange(new PropertyChangeEvent(this, propertyName, oldValue, newValue)); } /** * Retrieves listeners. * * @return all of the {@code PropertyChangeListeners} added or an empty array * if no listeners have been added * * @see java.beans.PropertyChangeSupport#getPropertyChangeListeners() */ @Override public PropertyChangeListener[] getPropertyChangeListeners() { ArrayList<PropertyChangeListener> listeners = new ArrayList<>(); if (propertyChangeSupport != null) { for (PropertyChangeListener pcl : propertyChangeSupport.getPropertyChangeListeners()) { // do not add single property change listeners if (!(pcl instanceof PropertyChangeListenerProxy)) { listeners.add(pcl); } } } if (weakPropertyChangeSupport != null) { listeners.addAll(Arrays.asList(weakPropertyChangeSupport.getPropertyChangeListeners())); } return listeners.toArray(new PropertyChangeListener[listeners.size()]); } /** * Retrieves listeners. * * @param propertyName * propertyName * @return all of the {@code PropertyChangeListeners} associated with the * named property. If no such listeners have been added, or if * {@code propertyName} is null, an empty array is returned. * * @see java.beans.PropertyChangeSupport#getPropertyChangeListeners(String) */ @Override public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) { ArrayList<PropertyChangeListener> listeners = new ArrayList<>(); if (propertyChangeSupport != null) { listeners.addAll(Arrays.asList(propertyChangeSupport.getPropertyChangeListeners(propertyName))); } if (weakPropertyChangeSupport != null) { listeners.addAll(Arrays.asList(weakPropertyChangeSupport.getPropertyChangeListeners(propertyName))); } return listeners.toArray(new PropertyChangeListener[listeners.size()]); } /** * Tests whether there are listeners for the property. * * @param propertyName * propertyName * @return true if there are one or more listeners for the given property. * * @see java.beans.PropertyChangeSupport#hasListeners(java.lang.String) */ @Override public boolean hasListeners(String propertyName) { if (propertyChangeSupport != null && propertyChangeSupport.hasListeners(propertyName)) { return true; } return weakPropertyChangeSupport != null && weakPropertyChangeSupport.hasListeners(propertyName); } /** * Delays events propagation by buffering them. When events are unblocked, * they get fired in the order they were recorded. {@inheritDoc} */ @Override public boolean blockEvents() { if (delayedEvents == null) { delayedEvents = new ArrayList<>(); return true; } return false; } /** * Unblocks event propagation. All events that were buffered are fired. * {@inheritDoc} */ @Override public void releaseEvents() { if (delayedEvents != null) { List<PropertyChangeEvent> delayedEventsCopy = new ArrayList<>(delayedEvents); delayedEvents = null; for (PropertyChangeEvent evt : delayedEventsCopy) { firePropertyChange(evt); } } } private final class NestedPropertyChangeListener implements PropertyChangeListener { private String rootPropertyName; private Set<String> registeredNestedProperties; public NestedPropertyChangeListener(String rootPropertyName) { this.rootPropertyName = rootPropertyName; this.registeredNestedProperties = new LinkedHashSet<>(); } @Override public void propertyChange(PropertyChangeEvent evt) { AbstractPropertyChangeCapable.this.firePropertyChange( rootPropertyName + IAccessor.NESTED_DELIM + evt.getPropertyName(), evt.getOldValue(), evt.getNewValue()); } public void registerNestedProperty(String nestedProperty) { registeredNestedProperties.add(nestedProperty); } public void fireAllNestedPropertyChanges(Object oldRoot, Object newRoot) { for (String nestedPropertyName : registeredNestedProperties) { Object oldNestedProperty = null; Object newNestedProperty = null; try { if (oldRoot != null) { oldNestedProperty = ACCESSOR_FACTORY.createPropertyAccessor(nestedPropertyName, oldRoot.getClass()) .getValue(oldRoot); } if (newRoot != null) { newNestedProperty = ACCESSOR_FACTORY.createPropertyAccessor(nestedPropertyName, newRoot.getClass()) .getValue(newRoot); } } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) { throw new NestedRuntimeException(ex, "Could not attach listener on property : " + rootPropertyName); } AbstractPropertyChangeCapable.this.firePropertyChange( rootPropertyName + IAccessor.NESTED_DELIM + nestedPropertyName, oldNestedProperty, newNestedProperty); } } } }