001package co.codewizards.cloudstore.ls.core.invoke.refjanitor;
002
003import static co.codewizards.cloudstore.core.util.AssertUtil.*;
004import static co.codewizards.cloudstore.core.util.ReflectionUtil.*;
005
006import java.beans.PropertyChangeEvent;
007import java.beans.PropertyChangeListener;
008import java.lang.ref.WeakReference;
009import java.util.HashMap;
010import java.util.Iterator;
011import java.util.LinkedList;
012import java.util.List;
013import java.util.Map;
014
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018import co.codewizards.cloudstore.core.bean.PropertyBase;
019import co.codewizards.cloudstore.core.collection.WeakIdentityHashMap;
020import co.codewizards.cloudstore.core.ref.IdentityWeakReference;
021import co.codewizards.cloudstore.ls.core.invoke.MethodInvocationRequest;
022import co.codewizards.cloudstore.ls.core.invoke.filter.ExtMethodInvocationRequest;
023
024public class PropertyChangeListenerJanitor extends AbstractReferenceJanitor {
025
026        private static final Logger logger = LoggerFactory.getLogger(PropertyChangeListenerJanitor.class);
027
028        private final WeakIdentityHashMap<Object, Map<Object, List<IdentityWeakReference<PropertyChangeListener>>>> bean2Property2ListenerRefs = new WeakIdentityHashMap<>();
029        private final WeakIdentityHashMap<PropertyChangeListener, WeakReference<FaultTolerantPropertyChangeListener>> originalListener2FaultTolerantPropertyChangeListenerRef =
030                        new WeakIdentityHashMap<>();
031
032//      // needed to pin the original listener into memory as long as the FaultTolerantPropertyChangeListener is still needed.
033//      private final WeakIdentityHashMap<FaultTolerantPropertyChangeListener, PropertyChangeListener> faultTolerantPropertyChangeListener2OriginalListener =
034//                      new WeakIdentityHashMap<>();
035
036        @Override
037        public void preInvoke(final ExtMethodInvocationRequest extMethodInvocationRequest) {
038                final MethodInvocationRequest methodInvocationRequest = extMethodInvocationRequest.getMethodInvocationRequest();
039
040                final Object bean = methodInvocationRequest.getObject(); // we don't support registering a PropertyChangeListener statically - or should we?!
041                if (bean == null)
042                        return;
043
044                final String methodName = methodInvocationRequest.getMethodName();
045                final Object[] arguments = methodInvocationRequest.getArguments();
046
047                Object property = null;
048                PropertyChangeListener listener = null;
049                if (arguments.length == 1 && arguments[0] instanceof PropertyChangeListener) {
050                        listener = (PropertyChangeListener) arguments[0];
051                        arguments[0] = getFaultTolerantPropertyChangeListenerOrCreate(listener);
052                }
053                else if (arguments.length == 2 && arguments[1] instanceof PropertyChangeListener) {
054                        listener = (PropertyChangeListener) arguments[1];
055                        if (arguments[0] instanceof PropertyBase)
056                                property = arguments[0];
057                        else if (arguments[0] instanceof String)
058                                property = arguments[0];
059                        else
060                                return;
061
062                        arguments[1] = getFaultTolerantPropertyChangeListenerOrCreate(listener);
063                }
064                else
065                        return;
066
067                assertNotNull(listener, "listener");
068
069                if ("addPropertyChangeListener".equals(methodName))
070                        trackAddPropertyChangeListener(bean, property, listener);
071                else if ("removePropertyChangeListener".equals(methodName))
072                        trackRemovePropertyChangeListener(bean, property, listener);
073        }
074
075        private synchronized FaultTolerantPropertyChangeListener getFaultTolerantPropertyChangeListenerOrCreate(final PropertyChangeListener listener) {
076                assertNotNull(listener, "listener");
077
078                final WeakReference<FaultTolerantPropertyChangeListener> ref = originalListener2FaultTolerantPropertyChangeListenerRef.get(listener);
079                FaultTolerantPropertyChangeListener faultTolerantListener = ref == null ? null : ref.get();
080                if (faultTolerantListener == null) {
081                        faultTolerantListener = new FaultTolerantPropertyChangeListener(listener);
082                        originalListener2FaultTolerantPropertyChangeListenerRef.put(listener, new WeakReference<>(faultTolerantListener));
083                }
084                return faultTolerantListener;
085        }
086
087        private synchronized FaultTolerantPropertyChangeListener getFaultTolerantPropertyChangeListener(final PropertyChangeListener listener) {
088                assertNotNull(listener, "listener");
089
090                final WeakReference<FaultTolerantPropertyChangeListener> ref = originalListener2FaultTolerantPropertyChangeListenerRef.get(listener);
091                final FaultTolerantPropertyChangeListener faultTolerantListener = ref == null ? null : ref.get();
092                return faultTolerantListener;
093        }
094
095        @Override
096        public void cleanUp() {
097                final Map<Object, Map<Object, List<IdentityWeakReference<PropertyChangeListener>>>> bean2Property2ListenerRefs;
098                synchronized (this) {
099                        bean2Property2ListenerRefs = new HashMap<>(this.bean2Property2ListenerRefs);
100                        this.bean2Property2ListenerRefs.clear();
101                }
102
103                for (final Map.Entry<Object, Map<Object, List<IdentityWeakReference<PropertyChangeListener>>>> me1 : bean2Property2ListenerRefs.entrySet()) {
104                        final Object bean = me1.getKey();
105                        if (bean == null)
106                                throw new IllegalStateException("bean2Property2ListenerRefs.entrySet() contained null-key!");
107
108                        for (final Map.Entry<Object, List<IdentityWeakReference<PropertyChangeListener>>> me2 : me1.getValue().entrySet()) {
109                                final Object property = me2.getKey();
110
111                                for (final IdentityWeakReference<PropertyChangeListener> ref : me2.getValue()) {
112                                        final PropertyChangeListener listener = ref.get();
113                                        if (listener != null)
114                                                tryRemovePropertyChangeListener(bean, property, listener);
115                                }
116                        }
117                }
118        }
119
120        private void tryRemovePropertyChangeListener(final Object bean, final Object property, final PropertyChangeListener listener) {
121                assertNotNull(bean, "bean");
122                assertNotNull(listener, "listener");
123
124                final FaultTolerantPropertyChangeListener faultTolerantPropertyChangeListener = getFaultTolerantPropertyChangeListener(listener);
125                if (faultTolerantPropertyChangeListener == null)
126                        return;
127
128                try {
129                        if (property != null)
130                                invoke(bean, "removePropertyChangeListener", property, faultTolerantPropertyChangeListener);
131                        else
132                                invoke(bean, "removePropertyChangeListener", faultTolerantPropertyChangeListener);
133                } catch (final Exception x) {
134                        logger.error("tryRemovePropertyChangeListener: " + x, x);
135                }
136        }
137
138        private synchronized void trackAddPropertyChangeListener(final Object bean, final Object property, final PropertyChangeListener listener) {
139                assertNotNull(bean, "bean");
140                assertNotNull(listener, "listener");
141
142                Map<Object, List<IdentityWeakReference<PropertyChangeListener>>> property2ListenerRefs = bean2Property2ListenerRefs.get(bean);
143                if (property2ListenerRefs == null) {
144                        property2ListenerRefs = new HashMap<>();
145                        bean2Property2ListenerRefs.put(bean, property2ListenerRefs);
146                }
147
148                List<IdentityWeakReference<PropertyChangeListener>> listenerRefs = property2ListenerRefs.get(property);
149                if (listenerRefs == null) {
150                        listenerRefs = new LinkedList<>();
151                        property2ListenerRefs.put(property, listenerRefs);
152                }
153                else
154                        expunge(listenerRefs);
155
156                // PropertyChangeSupport.addPropertyChangeListener(...) causes the same listener to be added multiple times.
157                // Hence, we do the same here: Add it once for each invocation.
158                final IdentityWeakReference<PropertyChangeListener> listenerRef = new IdentityWeakReference<PropertyChangeListener>(listener);
159                listenerRefs.add(listenerRef);
160        }
161
162        private synchronized void trackRemovePropertyChangeListener(final Object bean, final Object property, final PropertyChangeListener listener) {
163                assertNotNull(bean, "bean");
164                assertNotNull(listener, "listener");
165
166                final Map<Object, List<IdentityWeakReference<PropertyChangeListener>>> property2ListenerRefs = bean2Property2ListenerRefs.get(bean);
167                if (property2ListenerRefs == null)
168                        return;
169
170                final List<IdentityWeakReference<PropertyChangeListener>> listenerRefs = property2ListenerRefs.get(property);
171                if (listenerRefs == null)
172                        return;
173
174                final IdentityWeakReference<PropertyChangeListener> listenerRef = new IdentityWeakReference<PropertyChangeListener>(listener);
175                listenerRefs.remove(listenerRef);
176
177                expunge(listenerRefs);
178
179                if (listenerRefs.isEmpty())
180                        property2ListenerRefs.remove(property);
181
182                if (property2ListenerRefs.isEmpty())
183                        bean2Property2ListenerRefs.remove(bean);
184        }
185
186        private void expunge(final List<IdentityWeakReference<PropertyChangeListener>> listenerRefs) {
187                assertNotNull(listenerRefs, "listenerRefs");
188                for (final Iterator<IdentityWeakReference<PropertyChangeListener>> it = listenerRefs.iterator(); it.hasNext();) {
189                        final IdentityWeakReference<PropertyChangeListener> ref = it.next();
190                        if (ref.get() == null)
191                                it.remove();
192                }
193        }
194
195        private static class FaultTolerantPropertyChangeListener implements PropertyChangeListener {
196                private static final Logger logger = LoggerFactory.getLogger(PropertyChangeListenerJanitor.FaultTolerantPropertyChangeListener.class);
197
198                private final PropertyChangeListener delegate;
199
200                public FaultTolerantPropertyChangeListener(final PropertyChangeListener delegate) {
201                        this.delegate = assertNotNull(delegate, "delegate");
202                }
203
204                @Override
205                public void propertyChange(final PropertyChangeEvent event) {
206                        try {
207                                delegate.propertyChange(event);
208                        } catch (final Exception x) {
209                                logger.error("propertyChange: " + x, x);
210                        }
211                }
212
213                @Override
214                protected void finalize() throws Throwable {
215                        logger.debug("finalize: entered.");
216                        super.finalize();
217                }
218        }
219}