001/*
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.  
018 */
019
020package org.apache.wiki.event;
021
022import org.apache.log4j.Logger;
023
024import java.lang.ref.WeakReference;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.Comparator;
028import java.util.ConcurrentModificationException;
029import java.util.HashMap;
030import java.util.Iterator;
031import java.util.Map;
032import java.util.Set;
033import java.util.TreeSet;
034import java.util.Vector;
035
036/**
037 *  A singleton class that manages the addition and removal of WikiEvent listeners to a event source, as well as the firing of events
038 *  to those listeners. An "event source" is the object delegating its event handling to an inner delegating class supplied by this
039 *  manager. The class being serviced is considered a "client" of the delegate. The WikiEventManager operates across any number of
040 *  simultaneously-existing Engines since it manages all delegation on a per-object basis. Anything that might fire a WikiEvent
041 *  (or any of its subclasses) can be a client.
042 *  </p>
043 *
044 *  <h3>Using a Delegate for Event Listener Management</h3>
045 *  <p>
046 *  Basically, rather than have all manner of client classes maintain their own listener lists, add and remove listener methods, any
047 *  class wanting to attach listeners can simply request a delegate object to provide that service. The delegate handles the listener
048 *  list, the add and remove listener methods. Firing events is then a matter of calling the WikiEventManager's
049 *  {@link #fireEvent(Object,WikiEvent)} method, where the Object is the client. Prior to instantiating the event object, the
050 *  client can call {@link #isListening(Object)} to see there are any listeners attached to its delegate.
051 *  </p>
052 *
053 *  <h3>Adding Listeners</h3>
054 *  <p>
055 *  Adding a WikiEventListener to an object is very simple:
056 *  </p>
057 *  <pre>
058 *      WikiEventManager.addWikiEventListener( object, listener );
059 *  </pre>
060 *
061 *  <h3>Removing Listeners</h3>
062 *  <p>
063 *  Removing a WikiEventListener from an object is very simple:
064 *  </p>
065 *  <pre>
066 *      WikiEventManager.removeWikiEventListener( object, listener );
067 *  </pre>
068 *  If you only have a reference to the listener, the following method will remove it from any clients managed by the WikiEventManager:
069 *  <pre>
070 *      WikiEventManager.removeWikiEventListener( listener );
071 *  </pre>
072 *
073 *  <h3>Backward Compatibility: Replacing Existing <tt>fireEvent()</tt> Methods</h3>
074 *  <p>
075 *  Using one manager for all events processing permits consolidation of all event listeners and their associated methods in one place
076 *  rather than having them attached to specific subcomponents of an application, and avoids a great deal of event-related cut-and-paste
077 *  code. Convenience methods that call the WikiEventManager for event delegation can be written to maintain existing APIs.
078 *  </p>
079 *  <p>
080 *  For example, an existing <tt>fireEvent()</tt> method might look something like this:
081 *  </p>
082 *  <pre>
083 *    protected final void fireEvent( WikiEvent event ) {
084 *        for( WikiEventListener listener : m_listeners ) {
085 *            listener.actionPerformed( event );
086 *        }
087 *    }
088 *  </pre>
089 *  <p>
090 *  One disadvantage is that the above method is supplied with event objects, which are created even when no listener exists for them.
091 *  In a busy wiki with many users unused/unnecessary event creation could be considerable. Another advantage is that in addition to
092 *  the iterator, there must be code to support the addition and remove of listeners. The above could be replaced with the below code
093 *  (and with no necessary local support for adding and removing listeners):
094 *  </p>
095 *  <pre>
096 *    protected final void fireEvent( int type ) {
097 *        if( WikiEventManager.isListening( this ) ) {
098 *            WikiEventManager.fireEvent( this, new WikiEngineEvent( this, type ) );
099 *        }
100 *    }
101 *  </pre>
102 *  <p>
103 *  This only needs to be customized to supply the specific parameters for whatever WikiEvent you want to create.
104 *  </p>
105 *
106 *  <h3 id="preloading">Preloading Listeners</h3>
107 *  <p>
108 *  This may be used to create listeners for objects that don't yet exist, particularly designed for embedded applications that need
109 *  to be able to listen for the instantiation of an Object, by maintaining a cache of client-less WikiEvent sources that set their
110 *  client upon being popped from the cache. Each time any of the methods expecting a client parameter is called with a null parameter
111 *  it will preload an internal cache with a client-less delegate object that will be popped and returned in preference to creating a
112 *  new object. This can have unwanted side effects if there are multiple clients populating the cache with listeners. The only check
113 *  is for a Class match, so be aware if others might be populating the client-less cache with listeners.
114 *  </p>
115 *  <h3>Listener lifecycle</h3>
116 *  <p>
117 *  Note that in most cases it is not necessary to remove a listener. As of 2.4.97, the listeners are stored as WeakReferences, and
118 *  will be automatically cleaned at the next garbage collection, if you no longer hold a reference to them.  Of course, until the
119 *  garbage is collected, your object might still be getting events, so if you wish to avoid that, please remove it explicitly as
120 *  described above.
121 *  </p>
122 * @since 2.4.20
123 */
124public final class WikiEventManager {
125
126    private static final Logger log = Logger.getLogger(WikiEventManager.class);
127
128    /* If true, permits a WikiEventMonitor to be set. */
129    private static boolean c_permitMonitor = false;
130
131    /* Optional listener to be used as all-event monitor. */
132    private static WikiEventListener c_monitor = null;
133
134    /* The Map of client object to WikiEventDelegate. */
135    private final Map< Object, WikiEventDelegate > m_delegates = new HashMap<>();
136
137    /* The Vector containing any preloaded WikiEventDelegates. */
138    private final Vector< WikiEventDelegate > m_preloadCache = new Vector<>();
139
140    /* Singleton instance of the WikiEventManager. */
141    private static WikiEventManager c_instance = null;
142
143    /** Constructor for a WikiEventManager. */
144    private WikiEventManager() {
145        c_instance = this;
146        log.debug( "instantiated WikiEventManager" );
147    }
148
149    /**
150     *  As this is a singleton class, this returns the single instance of this class provided with the property file
151     *  filename and bit-wise application settings.
152     *
153     *  @return A shared instance of the WikiEventManager
154     */
155    public static WikiEventManager getInstance() {
156        if( c_instance == null ) {
157            synchronized( WikiEventManager.class ) {
158                return new WikiEventManager();
159                // start up any post-instantiation services here
160            }
161        }
162        return c_instance;
163    }
164
165    // public/API methods ......................................................
166
167    /**
168     *  Registers a WikiEventListener with a WikiEventDelegate for the provided client object.
169     *
170     *  <h3>Monitor Listener</h3>
171     *
172     *  If <tt>client</tt> is a reference to the WikiEventManager class itself and the compile-time flag {@link #c_permitMonitor} is true,
173     *  this attaches the listener as an all-event monitor, overwriting any previous listener (hence returning true).
174     *  <p>
175     *  You can remove any existing monitor by either calling this method with <tt>client</tt> as a reference to this class and the
176     *  <tt>listener</tt> parameter as null, or {@link #removeWikiEventListener(Object,WikiEventListener)} with a <tt>client</tt>
177     *  as a reference to this class. The <tt>listener</tt> parameter in this case is ignored.
178     *
179     * @param client   the client of the event source
180     * @param listener the event listener
181     * @return true if the listener was added (i.e., it was not already in the list and was added)
182     */
183    public static boolean addWikiEventListener( final Object client, final WikiEventListener listener ) {
184        if( client == WikiEventManager.class ) {
185            if ( c_permitMonitor ) {
186                c_monitor = listener;
187            }
188            return c_permitMonitor;
189        }
190        final WikiEventDelegate delegate = getInstance().getDelegateFor(client);
191        return delegate.addWikiEventListener(listener);
192    }
193
194    /**
195     *  Un-registers a WikiEventListener with the WikiEventDelegate for the provided client object.
196     *
197     * @param client   the client of the event source
198     * @param listener the event listener
199     * @return true if the listener was found and removed.
200     */
201    public static boolean removeWikiEventListener( final Object client, final WikiEventListener listener ) {
202        if ( client == WikiEventManager.class ) {
203            c_monitor = null;
204            return true;
205        }
206        final WikiEventDelegate delegate = getInstance().getDelegateFor(client);
207        return delegate.removeWikiEventListener(listener);
208    }
209
210    /**
211     *  Return the Set containing the WikiEventListeners attached to the delegate for the supplied client. If there are no
212     *  attached listeners, returns an empty Iterator rather than null.  Note that this will create a delegate for the client
213     *  if it did not exist prior to the call.
214     *
215     *  <h3>NOTE</h3>
216     *  <p>
217     *  This method returns a Set rather than an Iterator because of the high likelihood of the Set being modified while an
218     *  Iterator might be active. This returns an unmodifiable reference to the actual Set containing the delegate's listeners.
219     *  Any attempt to modify the Set will throw an {@link java.lang.UnsupportedOperationException}. This method is not
220     *  synchronized and it should be understood that the composition of the backing Set may change at any time.
221     *  </p>
222     *
223     * @param client   the client of the event source
224     * @return an unmodifiable Set containing the WikiEventListeners attached to the client
225     * @throws java.lang.UnsupportedOperationException  if any attempt is made to modify the Set
226     */
227    public static Set<WikiEventListener> getWikiEventListeners( final Object client ) throws UnsupportedOperationException {
228        final WikiEventDelegate delegate = getInstance().getDelegateFor(client);
229        return delegate.getWikiEventListeners();
230    }
231
232    /**
233     * Un-registers a WikiEventListener from any WikiEventDelegate client managed by this WikiEventManager. A true return value indicates
234     * the WikiEventListener was found and removed.
235     *
236     * @param listener the event listener
237     * @return true if the listener was found and removed.
238     */
239    public static boolean removeWikiEventListener( final WikiEventListener listener ) {
240        boolean removed = false;
241        // get the Map.entry object for the entire Map, then check match on entry (listener)
242        final WikiEventManager mgr = getInstance();
243        final Map< Object, WikiEventDelegate > sources =  Collections.synchronizedMap( mgr.getDelegates() );
244        synchronized( sources ) {
245            // get an iterator over the Map.Enty objects in the map
246            for( final Map.Entry< Object, WikiEventDelegate > entry : sources.entrySet() ) {
247                // the entry value is the delegate
248                final WikiEventDelegate delegate = entry.getValue();
249
250                // now see if we can remove the listener from the delegate (delegate may be null because this is a weak reference)
251                if( delegate != null && delegate.removeWikiEventListener( listener ) ) {
252                    removed = true; // was removed
253                }
254            }
255        }
256        return removed;
257    }
258
259    /**
260     *  Returns true if there are one or more listeners registered with the provided client Object (undelegated event source). This locates
261     *  any delegate and checks to see if it has any listeners attached.
262     *
263     *  @param client the client Object
264     *  @return True, if there is a listener for this client object.
265     */
266    public static boolean isListening( final Object client ) {
267        return getInstance().getDelegateFor( client ).isListening();
268    }
269
270    /**
271     *  Notify all listeners of the WikiEventDelegate having a registered interest in change events of the supplied WikiEvent.
272     *
273     * @param client the client initiating the event.
274     * @param event  the WikiEvent to fire.
275     */
276    public static void fireEvent( final Object client, final WikiEvent event ) {
277        final WikiEventDelegate source = getInstance().getDelegateFor( client );
278        source.fireEvent( event );
279        if( c_monitor != null ) {
280            c_monitor.actionPerformed( event );
281        }
282    }
283
284    // private and utility methods .............................................
285
286    /**
287     *  Return the client-to-delegate Map.
288     */
289    private Map< Object, WikiEventDelegate > getDelegates() {
290        return m_delegates;
291    }
292
293    /**
294     *  Returns a WikiEventDelegate for the provided client Object. If the parameter is a class reference, will generate and return a
295     *  client-less WikiEventDelegate. If the parameter is not a Class and the delegate cache contains any objects matching the Class
296     *  of any delegates in the cache, the first Class-matching delegate will be used in preference to creating a new delegate.
297     *  If a null parameter is supplied, this will create a client-less delegate that will attach to the first incoming client (i.e.,
298     *  there will be no Class-matching requirement).
299     *
300     * @param client   the client Object, or alternately a Class reference
301     * @return the WikiEventDelegate.
302     */
303    private WikiEventDelegate getDelegateFor( final Object client ) {
304        synchronized( m_delegates ) {
305            if( client == null || client instanceof Class ) { // then preload the cache
306                final WikiEventDelegate delegate = new WikiEventDelegate( client );
307                m_preloadCache.add( delegate );
308                m_delegates.put( client, delegate );
309                return delegate;
310            } else if( !m_preloadCache.isEmpty() ) {
311                // then see if any of the cached delegates match the class of the incoming client
312                for( int i = m_preloadCache.size()-1 ; i >= 0 ; i-- ) { // start with most-recently added
313                    final WikiEventDelegate delegate = m_preloadCache.elementAt( i );
314                    if( delegate.getClientClass() == null || delegate.getClientClass().equals( client.getClass() ) ) {
315                        // we have a hit, so use it, but only on a client we haven't seen before
316                        if( !m_delegates.containsKey( client ) ) {
317                            m_preloadCache.remove( delegate );
318                            m_delegates.put( client, delegate );
319                            return delegate;
320                        }
321                    }
322                }
323            }
324            // otherwise treat normally...
325            WikiEventDelegate delegate = m_delegates.get( client );
326            if( delegate == null ) {
327                delegate = new WikiEventDelegate( client );
328                m_delegates.put( client, delegate );
329            }
330            return delegate;
331        }
332    }
333
334
335    // .........................................................................
336
337    /**
338     *  Inner delegating class that manages event listener addition and removal. Classes that generate events can obtain an instance of
339     *  this class from the WikiEventManager and delegate responsibility to it. Interaction with this delegating class is done via the
340     *  methods of the {@link WikiEventDelegate} API.
341     *
342     * @since 2.4.20
343     */
344    private static final class WikiEventDelegate {
345
346        /* A list of event listeners for this instance. */
347        private final ArrayList< WeakReference< WikiEventListener > > m_listenerList = new ArrayList<>();
348        private Class< ? >  m_class  = null;
349
350        /**
351         *  Constructor for an WikiEventDelegateImpl, provided with the client Object it will service, or the Class
352         *  of client, the latter when used to preload a future incoming delegate.
353         */
354        protected WikiEventDelegate( final Object client ) {
355            if( client instanceof Class ) {
356                m_class = ( Class< ? > )client;
357            }
358        }
359
360        /**
361         *  Returns the class of the client-less delegate, null if this delegate is attached to a client Object.
362         */
363        protected Class< ? > getClientClass() {
364            return m_class;
365        }
366
367        /**
368         *  Return an unmodifiable Set containing the WikiEventListeners of this WikiEventDelegateImpl. If there are no attached listeners,
369         *  returns an empty Set rather than null.
370         *
371         * @return an unmodifiable Set containing this delegate's WikiEventListeners
372         * @throws java.lang.UnsupportedOperationException  if any attempt is made to modify the Set
373         */
374        public Set< WikiEventListener > getWikiEventListeners() {
375            synchronized( m_listenerList ) {
376                final TreeSet< WikiEventListener > set = new TreeSet<>( new WikiEventListenerComparator() );
377                for( final WeakReference< WikiEventListener > wikiEventListenerWeakReference : m_listenerList ) {
378                    final WikiEventListener l = wikiEventListenerWeakReference.get();
379                    if( l != null ) {
380                        set.add( l );
381                    }
382                }
383
384                return Collections.unmodifiableSet( set );
385            }
386        }
387
388        /**
389         *  Adds <tt>listener</tt> as a listener for events fired by the WikiEventDelegate.
390         *
391         * @param listener the WikiEventListener to be added
392         * @return true if the listener was added (i.e., it was not already in the list and was added)
393         */
394        public boolean addWikiEventListener( final WikiEventListener listener ) {
395            synchronized( m_listenerList ) {
396                final boolean listenerAlreadyContained = m_listenerList.stream()
397                                                                       .map( WeakReference::get )
398                                                                       .anyMatch( ref -> ref == listener );
399                if( !listenerAlreadyContained ) {
400                    return m_listenerList.add( new WeakReference<>( listener ) );
401                }
402            }
403            return false;
404        }
405
406        /**
407         *  Removes <tt>listener</tt> from the WikiEventDelegate.
408         *
409         * @param listener   the WikiEventListener to be removed
410         * @return true if the listener was removed (i.e., it was actually in the list and was removed)
411         */
412        public boolean removeWikiEventListener( final WikiEventListener listener ) {
413            synchronized( m_listenerList ) {
414                for( final Iterator< WeakReference< WikiEventListener > > i = m_listenerList.iterator(); i.hasNext(); ) {
415                    final WikiEventListener l = i.next().get();
416                    if( l == listener ) {
417                        i.remove();
418                        return true;
419                    }
420                }
421            }
422
423            return false;
424        }
425
426        /**
427         *  Returns true if there are one or more listeners registered with this instance.
428         */
429        public boolean isListening() {
430            synchronized( m_listenerList ) {
431                return !m_listenerList.isEmpty();
432            }
433        }
434
435        /**
436         *  Notify all listeners having a registered interest in change events of the supplied WikiEvent.
437         */
438        public void fireEvent( final WikiEvent event ) {
439            boolean needsCleanup = false;
440            try {
441                synchronized( m_listenerList ) {
442                    for( final WeakReference< WikiEventListener > wikiEventListenerWeakReference : m_listenerList ) {
443                        final WikiEventListener listener = wikiEventListenerWeakReference.get();
444                        if( listener != null ) {
445                            listener.actionPerformed( event );
446                        } else {
447                            needsCleanup = true;
448                        }
449                    }
450
451                    //  Remove all such listeners which have expired
452                    if( needsCleanup ) {
453                        for( int i = 0; i < m_listenerList.size(); i++ ) {
454                            final WeakReference< WikiEventListener > w = m_listenerList.get( i );
455                            if( w.get() == null ) {
456                                m_listenerList.remove(i--);
457                            }
458                        }
459                    }
460
461                }
462            } catch( final ConcurrentModificationException e ) {
463                //  We don't die, we just don't do notifications in that case.
464                log.info( "Concurrent modification of event list; please report this.", e );
465            }
466        }
467    }
468
469    private static class WikiEventListenerComparator implements Comparator< WikiEventListener > {
470        // TODO: This method is a critical performance bottleneck
471        @Override
472        public int compare( final WikiEventListener w0, final WikiEventListener w1 ) {
473            if( w1 == w0 || w0.equals( w1 ) ) {
474                return 0;
475            }
476
477            return w1.hashCode() - w0.hashCode();
478        }
479    }
480
481}