package de.invesdwin.util.bean; import java.beans.PropertyChangeEvent; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.WeakHashMap; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import de.invesdwin.norva.beanpath.impl.object.BeanObjectContext; import de.invesdwin.norva.beanpath.impl.object.BeanObjectProcessor; import de.invesdwin.norva.beanpath.spi.PathUtil; import de.invesdwin.norva.beanpath.spi.element.IPropertyBeanPathElement; import de.invesdwin.norva.beanpath.spi.element.ITableColumnBeanPathElement; import de.invesdwin.norva.beanpath.spi.visitor.SimpleBeanPathVisitorSupport; import de.invesdwin.util.assertions.Assertions; import de.invesdwin.util.collections.concurrent.AFastIterableDelegateSet; import de.invesdwin.util.lang.Strings; /** * The DirtyTracker does its best effort to detect and heal broken links between children and parents, though to provide * best results, it is recommended to always change value objects via their setters. */ @ThreadSafe public class DirtyTracker implements Serializable { private final AValueObject root; private final Set<String> beanPaths; private final Set<IDirtyTrackerListener> listeners = Collections .synchronizedSet(new AFastIterableDelegateSet<IDirtyTrackerListener>() { @Override protected Set<IDirtyTrackerListener> newDelegate() { return new LinkedHashSet<IDirtyTrackerListener>(); } }); @GuardedBy("this") private final Set<String> changedBeanPaths = new LinkedHashSet<String>(); @GuardedBy("this") private boolean trackingChangesDirectly; @GuardedBy("this") private transient TrackingChangesPropagatingRecursivePersistentPropertyChangeListener directTracker; @GuardedBy("this") private transient Map<TrackingChangesPropagatingRecursivePersistentPropertyChangeListener, String> registeredTracker_sourceBeanPath; @GuardedBy("this") private transient WeakReference<TrackingChangesPropagatingRecursivePersistentPropertyChangeListener> leadingRegisteredTrackerRef; public DirtyTracker(final AValueObject root) { this.root = root; final Set<String> beanPaths = new HashSet<String>(); final BeanObjectContext context = new BeanObjectContext(root); new BeanObjectProcessor(context, new SimpleBeanPathVisitorSupport(context) { @Override public void visitProperty(final IPropertyBeanPathElement e) { Assertions.assertThat(beanPaths.add(e.getBeanPath())).isTrue(); } }).process(); this.beanPaths = Collections.unmodifiableSet(beanPaths); } private synchronized Map<TrackingChangesPropagatingRecursivePersistentPropertyChangeListener, String> getRegisteredTrackers() { if (registeredTracker_sourceBeanPath == null) { registeredTracker_sourceBeanPath = new WeakHashMap<TrackingChangesPropagatingRecursivePersistentPropertyChangeListener, String>(); } return registeredTracker_sourceBeanPath; } /** * Starts tracking changes directly. */ public synchronized void startTrackingChangesDirectly() { if (!trackingChangesDirectly) { trackingChangesDirectly = true; if (directTracker == null) { addDirectTracker(); } } } /** * Stops tracking changes directly. */ public synchronized void stopTrackingChangesDirectly() { if (trackingChangesDirectly) { trackingChangesDirectly = false; if (directTracker != null) { removeDirectTracker(); } } } /** * Tells if this tracker is currently being updated by PropertyChangeEvents of its properties and children * properties. The tracking might be enabled by a parent that is interested in events here. Thus tracking might stop * after that parent stops tracking if startTrackingChanges() was not called on this tracker directly. */ public synchronized boolean isTrackingChanges() { return trackingChangesDirectly || (registeredTracker_sourceBeanPath != null && !registeredTracker_sourceBeanPath.isEmpty()); } /** * Tells if this tracker is currently being updated by PropertyChangeEvents of its properties and children * properties directly. This method checks of startTrackingChanges() was actually called on this instance and the * tracking is not happening because of a parent object. Or atleast that the tracking will continue even after a * parent object stops tracking. */ public synchronized boolean isTrackingChangesDirectly() { return trackingChangesDirectly; } public Set<IDirtyTrackerListener> getListeners() { return listeners; } public synchronized boolean isDirty(final String... beanPathPrefixes) { if (beanPathPrefixes == null || beanPathPrefixes.length == 0) { return !changedBeanPaths.isEmpty(); } else { for (final String beanPath : changedBeanPaths) { if (Strings.startsWithAny(beanPath, beanPathPrefixes)) { return true; } } return false; } } public Set<String> getChangedBeanPaths() { return Collections.unmodifiableSet(changedBeanPaths); } /** * WARNING: if a parent is not tracking changes, it is not known on this level and thus won't receive changes made * here. Always make sure to call "startTrackingChangesDirectly()" on the root object to prevent any problems. */ public synchronized boolean markDirty(final String... beanPathPrefixes) { final boolean changed = directMarkDirty(false, beanPathPrefixes); //manual changes are more expensive than tracked ones, since they don't propagate via the tracker on all levels //notify parents for (final Entry<TrackingChangesPropagatingRecursivePersistentPropertyChangeListener, String> entry : getRegisteredTrackers() .entrySet()) { final TrackingChangesPropagatingRecursivePersistentPropertyChangeListener registeredTracker = entry .getKey(); final String sourceBeanPath = entry.getValue(); if (registeredTracker != directTracker) { final String[] parentBeanPathPrefixes = adjustBeanPathPrefixesForParent(sourceBeanPath, beanPathPrefixes); Assertions.assertThat(registeredTracker.onManualMarkDirty(parentBeanPathPrefixes)).isEqualTo(changed); } } childrenMarkDirty(beanPathPrefixes); return changed; } private void childrenMarkDirty(final String... beanPathPrefixes) { //notify children final BeanObjectContext context = new BeanObjectContext(root); new BeanObjectProcessor(context, new SimpleBeanPathVisitorSupport(context) { @Override public void visitProperty(final IPropertyBeanPathElement e) { final String[] childBeanPathPrefixes = adjustBeanPathPrefixesForChildren(e.getBeanPath(), beanPathPrefixes); if (childBeanPathPrefixes != null && e.getAccessor().hasPublicGetterOrField() && !(e instanceof ITableColumnBeanPathElement)) { final Object value = e.getModifier().getValue(); if (value != null && value instanceof AValueObject) { final AValueObject cValue = (AValueObject) value; final DirtyTracker childDirtyTracker = cValue.dirtyTracker(); detectAndHealBrokenChildTrackers(childDirtyTracker); childDirtyTracker.directMarkDirty(false, childBeanPathPrefixes); childDirtyTracker.childrenMarkDirty(childBeanPathPrefixes); } } } }).withShallowOnly().process(); } /** * WARNING: if a parent is not tracking changes, it is not known on this level and thus won't receive changes made * here. Always make sure to call "startTrackingChangesDirectly()" on the root object to prevent any problems. */ public synchronized boolean markClean(final String... beanPathPrefixes) { final boolean changed = directMarkClean(false, beanPathPrefixes); //manual changes are more expensive than tracked ones, since they don't propagate via the tracker on all levels //notify parents for (final Entry<TrackingChangesPropagatingRecursivePersistentPropertyChangeListener, String> entry : getRegisteredTrackers() .entrySet()) { final TrackingChangesPropagatingRecursivePersistentPropertyChangeListener registeredTracker = entry .getKey(); final String sourceBeanPath = entry.getValue(); if (registeredTracker != directTracker) { final String[] parentBeanPathPrefixes = adjustBeanPathPrefixesForParent(sourceBeanPath, beanPathPrefixes); Assertions.assertThat(registeredTracker.onManualMarkClean(parentBeanPathPrefixes)).isEqualTo(changed); } } childrenMarkClean(beanPathPrefixes); return changed; } private void childrenMarkClean(final String... beanPathPrefixes) { //notify children final BeanObjectContext context = new BeanObjectContext(root); new BeanObjectProcessor(context, new SimpleBeanPathVisitorSupport(context) { @Override public void visitProperty(final IPropertyBeanPathElement e) { final String[] childBeanPathPrefixes = adjustBeanPathPrefixesForChildren(e.getBeanPath(), beanPathPrefixes); if (childBeanPathPrefixes != null && e.getAccessor().hasPublicGetterOrField() && !(e instanceof ITableColumnBeanPathElement)) { final Object value = e.getModifier().getValue(); if (value != null && value instanceof AValueObject) { final AValueObject cValue = (AValueObject) value; final DirtyTracker childDirtyTracker = cValue.dirtyTracker(); detectAndHealBrokenChildTrackers(childDirtyTracker); childDirtyTracker.directMarkClean(false, childBeanPathPrefixes); childDirtyTracker.childrenMarkClean(childBeanPathPrefixes); } } } }).withShallowOnly().process(); } private synchronized void detectAndHealBrokenChildTrackers(final DirtyTracker childDirtyTracker) { synchronized (childDirtyTracker) { final Map<TrackingChangesPropagatingRecursivePersistentPropertyChangeListener, String> childRegisteredTrackers = childDirtyTracker .getRegisteredTrackers(); for (final TrackingChangesPropagatingRecursivePersistentPropertyChangeListener tracker : getRegisteredTrackers() .keySet()) { if (!childRegisteredTrackers.containsKey(tracker)) { //there seems to be a new child that was not added via a setter, thus reattach everything from scratch tracker.removeListenersFromSourceHierarchy(); tracker.addListenersToSourceHierarchy(); } } } } private String[] adjustBeanPathPrefixesForParent(final String sourceBeanPath, final String[] beanPathPrefixes) { if (beanPathPrefixes == null || beanPathPrefixes.length == 0) { /* * if everything here is marked, we don't want the parent to mark any more than is done here. thus we put * the path under which the parent knows this here. * * We add a separator at the end to now change the dirty flag of the parent accessor to this value. */ return new String[] { sourceBeanPath + PathUtil.BEAN_PATH_SEPARATOR }; } else { final String[] newBeanPathPrefixes = new String[beanPathPrefixes.length]; for (int i = 0; i < beanPathPrefixes.length; i++) { newBeanPathPrefixes[i] = sourceBeanPath + PathUtil.BEAN_PATH_SEPARATOR + beanPathPrefixes[i]; } return newBeanPathPrefixes; } } /** * this method might return null to indicate that this child should be skipped */ private String[] adjustBeanPathPrefixesForChildren(final String childBeanPath, final String[] beanPathPrefixes) { if (beanPathPrefixes == null) { //convert null to empty since this might also be a parameter from the outside that wants to update all return new String[0]; } else if (beanPathPrefixes.length == 0) { return beanPathPrefixes; } else { boolean childBeanPathFound = false; final String[] newBeanPathPrefixes = new String[beanPathPrefixes.length]; for (int i = 0; i < beanPathPrefixes.length; i++) { if (Strings.startsWith(beanPathPrefixes[i], childBeanPath) && Strings.contains(beanPathPrefixes[i], PathUtil.BEAN_PATH_SEPARATOR)) { newBeanPathPrefixes[i] = Strings.substringAfter(beanPathPrefixes[i], PathUtil.BEAN_PATH_SEPARATOR); childBeanPathFound = true; } else { //children will skip null bean path prefixes newBeanPathPrefixes[i] = null; } } if (childBeanPathFound) { return newBeanPathPrefixes; } else { return null; } } } private synchronized boolean directMarkDirty(final boolean absoluteBeanPath, final String... beanPathPrefixes) { boolean changed = false; for (final String beanPath : beanPaths) { if (beanPath == null) { continue; } if (beanPathPrefixes == null || beanPathPrefixes.length == 0 || PathUtil.startsWithAnyBeanPath(absoluteBeanPath, beanPath, beanPathPrefixes)) { if (changedBeanPaths.add(beanPath)) { changed = true; for (final IDirtyTrackerListener listener : getListeners()) { listener.onDirty(beanPath); } } } } return changed; } private synchronized boolean directMarkClean(final boolean absoluteBeanPath, final String... beanPathPrefixes) { boolean changed = false; final List<String> changedBeanPathsCopy = new ArrayList<String>(changedBeanPaths); for (final String beanPath : changedBeanPathsCopy) { if (beanPath == null) { continue; } if (beanPathPrefixes == null || beanPathPrefixes.length == 0 || PathUtil.startsWithAnyBeanPath(absoluteBeanPath, beanPath, beanPathPrefixes)) { if (changedBeanPaths.remove(beanPath)) { changed = true; for (final IDirtyTrackerListener listener : getListeners()) { listener.onClean(beanPath); } } } } return changed; } /** * Need to add tracker again after deserialization of a new tree. */ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); synchronized (this) { if (trackingChangesDirectly) { addDirectTracker(); } } } private synchronized void addDirectTracker() { Assertions.assertThat(trackingChangesDirectly).isTrue(); Assertions.assertThat(directTracker).isNull(); directTracker = new TrackingChangesPropagatingRecursivePersistentPropertyChangeListener(); directTracker.addListenersToSourceHierarchy(); } private synchronized void removeDirectTracker() { Assertions.assertThat(trackingChangesDirectly).isFalse(); Assertions.assertThat(directTracker).isNotNull(); directTracker.removeListenersFromSourceHierarchy(); directTracker = null; } private class TrackingChangesPropagatingRecursivePersistentPropertyChangeListener extends ARecursivePersistentPropertyChangeListener { TrackingChangesPropagatingRecursivePersistentPropertyChangeListener() { super(root); } @Override protected void onPropertyChangeOnLastLevel(final PropertyChangeEvent evt) { //ignore } @Override protected void onPropertyChangeOnAnyLevel(final PropertyChangeEvent evt) { final Object source = evt.getSource(); Assertions.assertThat(source).isInstanceOf(AValueObject.class); final AValueObject cSource = (AValueObject) source; final DirtyTracker sourceDirtyTracker = cSource.dirtyTracker(); final TrackingChangesPropagatingRecursivePersistentPropertyChangeListener sourceLeadingRegisteredTracker = sourceDirtyTracker .getOrUpdateLeadingRegisteredTracker(this); Assertions.assertThat(sourceLeadingRegisteredTracker).isSameAs(this); if (!sourceDirtyTracker.isTrackingChanges()) { throw new IllegalStateException( "Not tracking changes right now, thus events should not be able to arrive!"); } sourceDirtyTracker.directMarkDirty(true, evt.getPropertyName()); for (final IDirtyTrackerListener listener : sourceDirtyTracker.getListeners()) { listener.propertyChange(evt); } } @Override protected boolean shouldIgnoreEvent(final PropertyChangeEvent evt) { final Object source = evt.getSource(); if (source instanceof AValueObject) { final AValueObject cSource = (AValueObject) source; final DirtyTracker sourceDirtyTracker = cSource.dirtyTracker(); final TrackingChangesPropagatingRecursivePersistentPropertyChangeListener sourceLeadingRegisteredTracker = sourceDirtyTracker .getOrUpdateLeadingRegisteredTracker(this); if (sourceLeadingRegisteredTracker == this) { return false; } } return true; } @Override protected void onListenerAdded(final ARecursivePersistentPropertyChangeListener listener) { final APropertyChangeSupported source = listener.getSource(); if (source instanceof AValueObject) { final AValueObject cSource = (AValueObject) source; final DirtyTracker sourceDirtyTracker = cSource.dirtyTracker(); sourceDirtyTracker.addRegisteredTracker(this, listener.getSourceBeanPath()); } } @Override protected void onListenerRemoved(final ARecursivePersistentPropertyChangeListener listener) { final APropertyChangeSupported source = listener.getSource(); if (source instanceof AValueObject) { final AValueObject cSource = (AValueObject) source; final DirtyTracker sourceDirtyTracker = cSource.dirtyTracker(); sourceDirtyTracker.removeRegisteredTracker(this); } } public boolean onManualMarkClean(final String... beanPathPrefixes) { final boolean changed = directMarkClean(false, beanPathPrefixes); //we need to propagate upwards again since there might be a distance between this parent and the root parent. childrenMarkClean(beanPathPrefixes); return changed; } public boolean onManualMarkDirty(final String... beanPathPrefixes) { final boolean changed = directMarkDirty(false, beanPathPrefixes); //we need to propagate upwards again since there might be a distance between this parent and the root parent. childrenMarkDirty(beanPathPrefixes); return changed; } } private synchronized void removeRegisteredTracker( final TrackingChangesPropagatingRecursivePersistentPropertyChangeListener tracker) { final Map<TrackingChangesPropagatingRecursivePersistentPropertyChangeListener, String> registeredTrackers = getRegisteredTrackers(); Assertions.assertThat(registeredTrackers.remove(tracker)).isNotNull(); removeLeadingRegisteredTrackerIfMatching(tracker); if (registeredTrackers.isEmpty() && isTrackingChangesDirectly()) { addDirectTracker(); } } private synchronized void removeLeadingRegisteredTrackerIfMatching( final TrackingChangesPropagatingRecursivePersistentPropertyChangeListener tracker) { if (leadingRegisteredTrackerRef != null && leadingRegisteredTrackerRef.get() == tracker) { leadingRegisteredTrackerRef = null; } } private synchronized void addRegisteredTracker( final TrackingChangesPropagatingRecursivePersistentPropertyChangeListener tracker, final String sourceBeanPath) { Assertions.assertThat(getRegisteredTrackers().put(tracker, sourceBeanPath)).isNull(); Assertions.assertThat(getOrUpdateLeadingRegisteredTracker(tracker)).isNotNull(); } private synchronized TrackingChangesPropagatingRecursivePersistentPropertyChangeListener getOrUpdateLeadingRegisteredTracker( final TrackingChangesPropagatingRecursivePersistentPropertyChangeListener tracker) { if (leadingRegisteredTrackerRef == null || leadingRegisteredTrackerRef.get() == null) { leadingRegisteredTrackerRef = new WeakReference<TrackingChangesPropagatingRecursivePersistentPropertyChangeListener>( tracker); } return leadingRegisteredTrackerRef.get(); } }