alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*
 *                 Sun Public License Notice
 *
 * The contents of this file are subject to the Sun Public License
 * Version 1.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://www.sun.com/
 *
 * The Original Code is NetBeans. The Initial Developer of the Original
 * Code is Sun Microsystems, Inc. Portions Copyright 1997-2001 Sun
 * Microsystems, Inc. All Rights Reserved.
 */
package org.netbeans.mdr.util;

import java.util.Map;
import org.netbeans.api.mdr.events.*;
import org.netbeans.mdr.handlers.BaseObjectHandler;
import org.netbeans.mdr.storagemodel.CompositeCollection;
import java.util.*;
import javax.jmi.reflect.*;

/**
 * Utility class for the management of listener notifications during and
 * after transactions.
 *
 * @author Martin Matula
 * @author Holger Krug.
 * @version
 */
public final class EventNotifier {

    /* -------------------------------------------------------------------- */
    /* -- Constants ------------------------------------------------------- */
    /* -------------------------------------------------------------------- */

    /** Bitmask representing all event types which may be received by listeners
     * listening on associations. */
    public static final int EVENTMASK_BY_ASSOCIATION = MDRChangeEvent.EVENTMASK_ON_ASSOCIATION;
    /** Bitmask representing all event types which may be received by listeners
     * listening on instances. */
    public static final int EVENTMASK_BY_INSTANCE = EVENTMASK_BY_ASSOCIATION | MDRChangeEvent.EVENTMASK_ON_INSTANCE;
    /** Bitmask representing all event types which may be received by listeners
     * listening on class proxies. */
    public static final int EVENTMASK_BY_CLASS = EVENTMASK_BY_INSTANCE | MDRChangeEvent.EVENTMASK_ON_CLASS;
    /** Bitmask representing all event types which may be received by listeners
     * listening on package proxies. */
    public static final int EVENTMASK_BY_PACKAGE = EVENTMASK_BY_CLASS | MDRChangeEvent.EVENTMASK_ON_PACKAGE;
    /** Bitmask representing all event types which may be received by listeners
     * listening on repository proxies. */
    public static final int EVENTMASK_BY_REPOSITORY = EVENTMASK_BY_CLASS | MDRChangeEvent.EVENTMASK_ON_REPOSITORY;

    /* -------------------------------------------------------------------- */
    /* -- Methods for debugging purposes (static) ------------------------- */
    /* -------------------------------------------------------------------- */

    /**
     * Pretty prints an event type.
     *
     * 

[XXX]: Probably this method should be used to a test utility * class ?!

*/ public static String prettyPrintType(MDRChangeEvent e) { if (e.isOfType(AttributeEvent.EVENT_ATTRIBUTE_ADD)) return "EVENT_ATTRIBUTE_ADD"; else if (e.isOfType(AttributeEvent.EVENT_ATTRIBUTE_REMOVE)) return "EVENT_ATTRIBUTE_REMOVE"; else if (e.isOfType(AttributeEvent.EVENT_ATTRIBUTE_SET)) return "EVENT_ATTRIBUTE_SET"; else if (e.isOfType(AttributeEvent.EVENT_CLASSATTR_ADD)) return "EVENT_CLASSATTR_ADD"; else if (e.isOfType(AttributeEvent.EVENT_CLASSATTR_REMOVE)) return "EVENT_CLASSATTR_REMOVE"; else if (e.isOfType(AttributeEvent.EVENT_CLASSATTR_SET)) return "EVENT_CLASSATTR_SET"; else if (e.isOfType(InstanceEvent.EVENT_INSTANCE_CREATE)) return "EVENT_INSTANCE_CREATE"; else if (e.isOfType(InstanceEvent.EVENT_INSTANCE_DELETE)) return "EVENT_INSTANCE_DELETE"; else if (e.isOfType(AssociationEvent.EVENT_ASSOCIATION_ADD)) return "EVENT_ASSOCIATION_ADD"; else if (e.isOfType(AssociationEvent.EVENT_ASSOCIATION_REMOVE)) return "EVENT_ASSOCIATION_REMOVE"; else if (e.isOfType(AssociationEvent.EVENT_ASSOCIATION_SET)) return "EVENT_ASSOCIATION_SET"; else if (e.isOfType(ExtentEvent.EVENT_EXTENT_CREATE)) return "EVENT_EXTENT_CREATE"; else if (e.isOfType(ExtentEvent.EVENT_EXTENT_DELETE)) return "EVENT_EXTENT_DELETE"; else if (e.isOfType(TransactionEvent.EVENT_TRANSACTION_END)) return "EVENT_TRANSACTION_END"; else if (e.isOfType(TransactionEvent.EVENT_TRANSACTION_START)) return "EVENT_TRANSACTION_START"; else return ""; } /** * Pretty prints a event type mask. * *

[XXX]: Probably this method should be used to a test utility * class ?!

*/ public static String prettyPrintMask(int mask) { StringBuffer buf = new StringBuffer(); if ( (mask & AttributeEvent.EVENT_ATTRIBUTE_ADD) == AttributeEvent.EVENT_ATTRIBUTE_ADD ) buf.append("IAA "); if ( (mask & AttributeEvent.EVENT_ATTRIBUTE_REMOVE) == AttributeEvent.EVENT_ATTRIBUTE_REMOVE ) buf.append("IAR "); if ( (mask & AttributeEvent.EVENT_ATTRIBUTE_SET) == AttributeEvent.EVENT_ATTRIBUTE_SET ) buf.append("IAS "); if ( (mask & AttributeEvent.EVENT_CLASSATTR_ADD) == AttributeEvent.EVENT_CLASSATTR_ADD ) buf.append("CAA "); if ( (mask & AttributeEvent.EVENT_CLASSATTR_REMOVE) == AttributeEvent.EVENT_CLASSATTR_REMOVE ) buf.append("CAR "); if ( (mask & AttributeEvent.EVENT_CLASSATTR_SET) == AttributeEvent.EVENT_CLASSATTR_SET ) buf.append("CAS "); if ( (mask & InstanceEvent.EVENT_INSTANCE_CREATE) == InstanceEvent.EVENT_INSTANCE_CREATE ) buf.append("IC "); if ( (mask & InstanceEvent.EVENT_INSTANCE_DELETE) == InstanceEvent.EVENT_INSTANCE_DELETE ) buf.append("ID "); if ( (mask & AssociationEvent.EVENT_ASSOCIATION_ADD) == AssociationEvent.EVENT_ASSOCIATION_ADD ) buf.append("AA "); if ( (mask & AssociationEvent.EVENT_ASSOCIATION_REMOVE) == AssociationEvent.EVENT_ASSOCIATION_REMOVE ) buf.append("AR "); if ( (mask & AssociationEvent.EVENT_ASSOCIATION_SET) == AssociationEvent.EVENT_ASSOCIATION_SET ) buf.append("AS "); if ( (mask & ExtentEvent.EVENT_EXTENT_CREATE) == ExtentEvent.EVENT_EXTENT_CREATE ) buf.append("EC "); if ( (mask & ExtentEvent.EVENT_EXTENT_DELETE) == ExtentEvent.EVENT_EXTENT_DELETE ) buf.append("ED "); if ( (mask & TransactionEvent.EVENT_TRANSACTION_END) == TransactionEvent.EVENT_TRANSACTION_END ) buf.append("TE "); if ( (mask & TransactionEvent.EVENT_TRANSACTION_START) == TransactionEvent.EVENT_TRANSACTION_START ) buf.append("TS "); if ( buf.length() > 0 ) buf.deleteCharAt(buf.length()-1); return buf.toString(); } /* -------------------------------------------------------------------- */ /* -- Public attributes ----------------------------------------------- */ /* -------------------------------------------------------------------- */ public final Association ASSOCIATION = new Association(); public final Clazz CLASS = new Clazz(); public final Instance INSTANCE = new Instance(); public final Package PACKAGE = new Package(); public final Repository REPOSITORY = new Repository(); /* -------------------------------------------------------------------- */ /* -- Private attributes ---------------------------------------------- */ /* -------------------------------------------------------------------- */ /** * Thread for the dispatching of events after transaction success. */ private final Thread dispatcher = new Thread(new EventsDelivery(), "MDR event dispatcher"); /** * Maps: event => set of pre-change listeners. */ private final HashMap preChangeListeners = new HashMap(); /** * Maps: event => set of change listeners. */ private final Hashtable changeListeners = new Hashtable(); /** * Queue for the events of the currently running write transaction. * The events are stored to inform their listeners either about * rollback or success. */ private final LinkedList localQueue = new LinkedList(); /** * Queue for the events of successfully finished transactions, the * listeners of which still have to be informed. */ private final LinkedList globalQueue = new LinkedList(); /** Flag that tells the event dispatching thread to stop (set to true in * shutdown method. */ private boolean shuttingDown = false; /* -------------------------------------------------------------------- */ /* -- Constructor (public) -------------------------------------------- */ /* -------------------------------------------------------------------- */ /** * Starts the dispatcher thread as daemon. */ public EventNotifier() { dispatcher.setDaemon(true); dispatcher.start(); } /* -------------------------------------------------------------------- */ /* -- Methods to fire events at transaction commit or rollback time --- */ /* -------------------------------------------------------------------- */ /** * Calls {@link MDRPreChangeListener.changeCancelled(MDRChangeEvent)} for * all pre-change listeners on event to inform them * about cancelling the event. */ public void fireCancelled(MDRChangeEvent event) { localQueue.remove(event); Set collected = (Set) preChangeListeners.remove(event); changeListeners.remove(event); if (collected == null) { Logger.getDefault().notify(Logger.INFORMATIONAL, new DebugException("Change cancelled event not corresponding to any planned change event.")); return; } // fire changeCancelled event for (Iterator it = collected.iterator(); it.hasNext();) { try { ((MDRPreChangeListener) it.next()).changeCancelled(event); } catch (RuntimeException e) { // log the exception Logger.getDefault().notify(Logger.INFORMATIONAL, e); } } } /** * Calls {@link MDRPreChangeListener.changeCancelled(MDRChangeEvent)} * on all pre-change listeners of all events of the transaction to be * rolled back. */ public void fireCancelled() { while (!localQueue.isEmpty()) { MDRChangeEvent event = (MDRChangeEvent) localQueue.getFirst(); fireCancelled(event); } } /** * Enqueues all events of a transaction successfully finished to inform * the listeners in a separate thread. */ public void fireChanged() { preChangeListeners.clear(); synchronized (globalQueue) { globalQueue.addAll(localQueue); globalQueue.notify(); } localQueue.clear(); } /** Shuts down event dispatcher (by stopping event dispatching thread) */ public void shutdown() { synchronized (globalQueue) { shuttingDown = true; globalQueue.notify(); } } /* -------------------------------------------------------------------- */ /* -- EventNotifier.EventDelivery (inner class, private) -------------- */ /* -------------------------------------------------------------------- */ /** * Runnable to be executed by the event dispatcher thread. Waits on the * global queue if it is empty. Informs the listeners on queued events. * Catches all exceptions and logs the stack trace. * *

Returns if a queued change event was not planned before (i.e. does * not correspond to an entry in {@link #changeListeners}. */ private class EventsDelivery implements Runnable { /** * */ public void run() { Collection collected; MDRChangeEvent event; while (true) { synchronized (globalQueue) { for (;;) { if (!globalQueue.isEmpty()) break; else if (shuttingDown) return; try { globalQueue.wait(); } catch (InterruptedException e) { Logger.getDefault().notify(Logger.INFORMATIONAL, e); } } collected = (Collection) changeListeners.remove(event = (MDRChangeEvent) globalQueue.removeFirst()); } if (collected == null) { Logger.getDefault().notify(Logger.INFORMATIONAL, new DebugException("Change event not corresponding to any planned change event.")); return; } for (Iterator it = collected.iterator(); it.hasNext();) { try { ((MDRChangeListener) it.next()).change(event); } catch (RuntimeException e) { Logger.getDefault().notify(Logger.INFORMATIONAL, e); } } } } } /* -------------------------------------------------------------------- */ /* -- EventNotifier.Abstract (inner class, public abstract) ----------- */ /* -------------------------------------------------------------------- */ /** * This class and its derived classes are used to enqueue all changes into * the local queue and to fire the pre-change events. */ public abstract class Abstract { /** * Maps repository objects to instances of RegisteredListenerSet. */ private final Map registeredListenerSets = new Map() { public Set keySet() { throw new UnsupportedOperationException(); } public Set entrySet() { throw new UnsupportedOperationException(); } public void putAll(Map t) { throw new UnsupportedOperationException(); } public boolean isEmpty() { throw new UnsupportedOperationException(); } public boolean containsKey(Object key) { throw new UnsupportedOperationException(); } public boolean containsValue(Object value) { throw new UnsupportedOperationException(); } public Collection values() { throw new UnsupportedOperationException(); } public void clear() { throw new UnsupportedOperationException(); } public int size() { throw new UnsupportedOperationException(); } private final Map inner = new HashMap(); private Object extractKey(Object key) { if (key instanceof BaseObjectHandler) { try { key = ((BaseObjectHandler) key)._getDelegate().getMofId(); } catch (InvalidObjectException e) { // ignore return null; } } return key; } public Object put(Object key, Object value) { key = extractKey(key); if (key != null) { return inner.put(key, value); } else { return null; } } public Object get(Object key) { key = extractKey(key); if (key != null) { return inner.get(key); } else { return null; } } public Object remove(Object key) { key = extractKey(key); if (key != null) { return inner.remove(key); } else { return null; } } }; /** * Adds listener to source for event types * matching mask. */ public void addListener(MDRChangeListener listener, int mask, Object source) { if ( (listener == null) || (mask == 0) ) return; synchronized (registeredListenerSets) { RegisteredListenerSet value = (RegisteredListenerSet) registeredListenerSets.get(source); if ( value == null ) { value = new RegisteredListenerSet(); registeredListenerSets.put(source, value); } value.addListener(listener, mask); } } /** * Removes listener from source for all event types. */ public void removeListener(MDRChangeListener listener, Object source) { if ( listener == null ) return; synchronized (registeredListenerSets) { RegisteredListenerSet value = (RegisteredListenerSet) registeredListenerSets.get(source); if ( value != null ) { value.removeListener(listener); if ( value.isEmpty() ) { registeredListenerSets.remove(source); } } } } /** * Removes listener from source for event types * matching mask. */ public void removeListener(MDRChangeListener listener, int mask, Object source) { if ( (listener == null) || (mask == 0) ) return; synchronized (registeredListenerSets) { RegisteredListenerSet value = (RegisteredListenerSet) registeredListenerSets.get(source); if ( value != null ) { value.removeListener(listener, mask); if ( value.isEmpty() ) { registeredListenerSets.remove(source); } } } } /** Informs pre-change listeners about the given event. Internally the * event is stored together with its pre-change listeners and change * listeners to allow further event processing at transaction rollback * resp. commit time. * * @param current The source object of this event. * @param event Event object. * @exception DebugException if the event was already fired */ public void firePlannedChange(Object current, MDRChangeEvent event) { HashSet preChange = new HashSet(); HashSet postChange = new HashSet(); // collect preChange and postChange listeners collectListeners(current, event, preChange, postChange); if (!(preChange.isEmpty() && postChange.isEmpty())) { localQueue.addLast(event); for (Iterator it = preChange.iterator(); it.hasNext();) { try { ((MDRPreChangeListener) it.next()).plannedChange(event); } catch (RuntimeException e) { Logger.getDefault().notify(Logger.INFORMATIONAL, e); } } if (preChangeListeners.put(event, preChange) != null) { throw new DebugException("Same event fired twice."); } CompositeCollection all = new CompositeCollection(); all.addCollection(preChange); all.addCollection(postChange); if (changeListeners.put(event, all) != null) { throw new DebugException("Same event fired twice."); } } } /** * Collects the listeners for the given event on object * current. This method has to be overwritten by derived * classes to inform listeners on objects to which the events are * propagated. Overwriting methods shall call this method first and * then add any further listeners. * * @param current the object on which the event was fired * @param event the event * @param post if false, listeners implementing * {@link org.netbeans.api.mdr.events.MDRPreChangeListener} are * collected, otherwise listeners implementing only * {@link org.netbeans.api.mdr.events.MDRChangeListener} * @param preChange the set where the pre-change listeners are collected * @param postChange the set where the post-change only listeners are collected */ protected void collectListeners(Object current, MDRChangeEvent event, Set preChange, Set postChange) { // fire event on all listeners registered on this object synchronized (registeredListenerSets) { RegisteredListenerSet value = (RegisteredListenerSet) registeredListenerSets.get(current); if (value != null) { value.collectListeners(event, preChange, postChange); } } } public Collection getListeners(Object source) { synchronized (registeredListenerSets) { RegisteredListenerSet value = (RegisteredListenerSet) registeredListenerSets.get(source); if (value != null) { return new ArrayList(value.map.keySet()); } else { return Collections.EMPTY_LIST; } } } } /* -------------------------------------------------------------------- */ /* -- EventNotifier.Assocation (inner class, public) ------------------ */ /* -------------------------------------------------------------------- */ /** * Handles events for associations. */ public final class Association extends Abstract { private Association() { super(); } public void addListener(MDRChangeListener listener, int mask, Object source) { super.addListener(listener, mask & EventNotifier.EVENTMASK_BY_ASSOCIATION, source); } public void removeListener(MDRChangeListener listener, int mask, Object source) { super.removeListener(listener, mask & EventNotifier.EVENTMASK_BY_ASSOCIATION, source); } /** * Adds listeners on the instances participating in the association link * added resp. removed and on the owning package. */ protected void collectListeners(Object current, MDRChangeEvent event, Set preChange, Set postChange) { super.collectListeners(current, event, preChange, postChange); // fire event on all listeners registered on instances that were affected if (event instanceof AssociationEvent) { AssociationEvent assocEvent = (AssociationEvent) event; if (assocEvent.getFixedElement() != null) INSTANCE.collectListeners(assocEvent.getFixedElement(), event, preChange, postChange); if (assocEvent.getOldElement() != null) INSTANCE.collectListeners(assocEvent.getOldElement(), event, preChange, postChange); if (assocEvent.getNewElement() != null) INSTANCE.collectListeners(assocEvent.getNewElement(), event, preChange, postChange); } // fire event on the immediate package extent PACKAGE.collectListeners(((RefAssociation) current).refImmediatePackage(), event, preChange, postChange); } } /* -------------------------------------------------------------------- */ /* -- EventNotifier.Clazz (inner class, public) ----------------------- */ /* -------------------------------------------------------------------- */ /** * Handles events for class proxies. */ public final class Clazz extends Abstract { private Clazz() { super(); } public void addListener(MDRChangeListener listener, int mask, Object source) { super.addListener(listener, mask & EventNotifier.EVENTMASK_BY_CLASS, source); } public void removeListener(MDRChangeListener listener, int mask, Object source) { super.removeListener(listener, mask & EventNotifier.EVENTMASK_BY_CLASS, source); } /** * Adds listeners on the owning package. */ protected void collectListeners(Object current, MDRChangeEvent event, Set preChange, Set postChange) { super.collectListeners(current, event, preChange, postChange); PACKAGE.collectListeners(((RefClass) current).refImmediatePackage(), event, preChange, postChange); } } /* -------------------------------------------------------------------- */ /* -- EventNotifier.Instance (inner class, public) -------------------- */ /* -------------------------------------------------------------------- */ /** * Handles events for instances. */ public final class Instance extends Abstract { private Instance() { super(); } public void addListener(MDRChangeListener listener, int mask, Object source) { super.addListener(listener, mask & EventNotifier.EVENTMASK_BY_INSTANCE, source); } public void removeListener(MDRChangeListener listener, int mask, Object source) { super.removeListener(listener, mask & EventNotifier.EVENTMASK_BY_INSTANCE, source); } /** * Adds listeners on the owning class proxy. */ protected void collectListeners(Object current, MDRChangeEvent event, Set preChange, Set postChange) { super.collectListeners(current, event, preChange, postChange); CLASS.collectListeners(((RefObject) current).refClass(), event, preChange, postChange); } } /* -------------------------------------------------------------------- */ /* -- EventNotifier.Package (inner class, public) --------------------- */ /* -------------------------------------------------------------------- */ /** * Handles events for packages. */ public final class Package extends Abstract { private Package() { super(); } public void addListener(MDRChangeListener listener, int mask, Object source) { super.addListener(listener, mask & EventNotifier.EVENTMASK_BY_PACKAGE, source); } public void removeListener(MDRChangeListener listener, int mask, Object source) { super.removeListener(listener, mask & EventNotifier.EVENTMASK_BY_PACKAGE, source); } /** * Adds listeners on the owning package resp., if this package is * outermost, on the repository. */ protected void collectListeners(Object current, MDRChangeEvent event, Set preChange, Set postChange) { super.collectListeners(current, event, preChange, postChange); RefPackage immediate = ((RefPackage) current).refImmediatePackage(); if (immediate != null) { collectListeners(immediate, event, preChange, postChange); } else { REPOSITORY.collectListeners(((BaseObjectHandler) current)._getDelegate().getMdrStorage(), event, preChange, postChange); } } } /* -------------------------------------------------------------------- */ /* -- EventNotifier.Repository (inner class, public) ------------------ */ /* -------------------------------------------------------------------- */ /** * Handles events for repositories. */ public final class Repository extends Abstract { private Repository() { super(); } public void addListener(MDRChangeListener listener, int mask, Object source) { super.addListener(listener, mask & EventNotifier.EVENTMASK_BY_REPOSITORY, source); } public void removeListener(MDRChangeListener listener, int mask, Object source) { super.removeListener(listener, mask & EventNotifier.EVENTMASK_BY_REPOSITORY, source); } } /* ===================================================================== */ /* == INNER CLASS CONTAINING OPTIMIZED/OPTIMIZABLE DATASTRUCTURES ====== */ /* == FOR LISTENER MANAGEMENT ========================================== */ /* ===================================================================== */ /* -------------------------------------------------------------------- */ /* -- EventNotifier.RegisteredListenerSet (inner class) --------------- */ /* -------------------------------------------------------------------- */ /** * Instances of this class manage listeners registered for change sources. * *

[XXX]: This class should be the subject of optimization. Because the * optimization to be used can heavily depend on the application it could * make sense to make this class an interface, to add a factory interface * for RegisteredListenerSets, to provide a default factory * implementation and to llow applications to register a factory * optimized for their specific purposes. The factory interface should * look like:

* *
     *  public interface RegisteredListenerSetFactory {
     *    public RegisteredListenerSet createInstanceListenerSet(RefObject inst);
     *    public RegisteredListenerSet createClassListenerSet(RefClass cls);
     *    public RegisteredListenerSet createAssociationListenerSet(RefAssociation ass);
     *    public RegisteredListenerSet createPackageListenerSet(RefPackage pckg);
     *    public RegisteredListenerSet createRepositoryListenerSet(MDRepository rep);
     *  }
     *  
*/ static class RegisteredListenerSet { /** * Maps listeners to event masks. */ HashMap map = new HashMap(); /** * Adds listener listening on events matching mask. */ void addListener(MDRChangeListener listener, int mask) { Integer value = (Integer) map.get(listener); if ( value != null ) { // create a composite mask, if the listener was already registered int currentMask = value.intValue(); mask |= currentMask; if ( (mask ^ currentMask) == 0 ) { // the new mask does not contain any new bit return; } } map.put(listener, new Integer(mask)); } /** * Removes listener for all events. */ void removeListener(MDRChangeListener listener) { map.remove(listener); } /** * Removes listener for events matching mask. */ void removeListener(MDRChangeListener listener, int mask) { Object value = map.get(listener); if ( value != null ) { // create a reduced mask, if the listener was already registered int currentMask = ((Integer) value).intValue(); mask = currentMask & (~mask); if (mask == 0) { // the resulting mask is empty removeListener(listener); } else { // the resulting mask still contains some bits map.put(listener, new Integer(mask)); } } } /** * Adds the listeners which are registered for events of type * event to preChange resp. postChange. * * @param event the event the listeners are collected for * @param preChange the set where the pre-change listeners are collected * @param postChange the set where the post-change only listeners are collected */ void collectListeners(MDRChangeEvent event, Set preChange, Set postChange) { Iterator it = map.entrySet().iterator(); while ( it.hasNext() ) { Map.Entry entry = (Map.Entry) it.next(); if ( event.isOfType(((Integer)entry.getValue()).intValue()) ) { Object key = entry.getKey(); if ( key instanceof MDRPreChangeListener ) { preChange.add(key); } else { postChange.add(key); } } } } /** * Returns true if the listener set is empty, * false otherwise. */ boolean isEmpty() { return map.isEmpty(); } } }
... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.