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