/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package flex.messaging;

import flex.messaging.client.FlexClient;
import flex.messaging.client.FlexClientListener;
import flex.messaging.log.LogCategories;
import flex.messaging.messages.Message;
import flex.messaging.util.TimeoutAbstractObject;

import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * The base for FlexSession implementations.
 */
public abstract class FlexSession extends TimeoutAbstractObject implements FlexClientListener, MessageClientListener {
    //--------------------------------------------------------------------------
    //
    // Public Static Variables
    //
    //--------------------------------------------------------------------------

    /**
     * Log category for FlexSession related messages.
     */
    public static final String FLEX_SESSION_LOG_CATEGORY = LogCategories.ENDPOINT_FLEXSESSION;

    /**
     *
     */
    public static final int MAX_CONNECTIONS_PER_SESSION_UNLIMITED = -1;

    //--------------------------------------------------------------------------
    //
    // Private Static Variables
    //
    //--------------------------------------------------------------------------

    /**
     * The set of session created listeners to notify upon a new session creation.
     */
    private static final CopyOnWriteArrayList<FlexSessionListener> createdListeners = new CopyOnWriteArrayList<FlexSessionListener>();

    /**
     * Error string constants.
     */
    private static final int FLEX_SESSION_INVALIDATED = 10019;

    //--------------------------------------------------------------------------
    //
    // Constructor
    //
    //--------------------------------------------------------------------------

    /**
     * @deprecated Post 2.6.1 releases require use of the constructor that takes an <tt>AbstractFlexSessionProvider</tt> argument.
     */
    public FlexSession() {
        this(null);
    }

    /**
     * Constructs a new FlexSession instance.
     *
     * @param sessionProvider The provider that instantiated this instance.
     */
    public FlexSession(AbstractFlexSessionProvider sessionProvider) {
        this.sessionProvider = sessionProvider;
    }

    //--------------------------------------------------------------------------
    //
    // Static Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Adds a session created listener that will be notified when new sessions
     * are created.
     *
     * @param listener The listener to add.
     * @see flex.messaging.FlexSessionListener
     */
    public static void addSessionCreatedListener(FlexSessionListener listener) {
        if (listener != null)
            createdListeners.addIfAbsent(listener);
    }

    /**
     * Removes a session created listener.
     *
     * @param listener The listener to remove.
     * @see flex.messaging.FlexSessionListener
     */
    public static void removeSessionCreatedListener(FlexSessionListener listener) {
        if (listener != null)
            createdListeners.remove(listener);
    }

    //--------------------------------------------------------------------------
    //
    // Variables
    //
    //--------------------------------------------------------------------------

    /**
     * Flag used to break cycles during invalidation.
     */
    protected boolean invalidating;

    /**
     * Instance level lock to sync for state changes.
     */
    protected final Object lock = new Object();

    /**
     * Flag indicated whether the session has been invalidated/destroyed.
     */
    protected boolean valid = true;

    /**
     * The attributes associated with this session.
     */
    private Map<String, Object> attributes;

    /**
     * Registered attribute listeners for the session.
     */
    private volatile CopyOnWriteArrayList<FlexSessionAttributeListener> attributeListeners;

    /**
     * Flag indicating whether creation notification has been completed.
     */
    private boolean creationNotified;

    /**
     * The set of session destroy listeners to notify when the session is destroyed.
     */
    private volatile CopyOnWriteArrayList<FlexSessionListener> destroyedListeners;

    /**
     * The associated FlexClients.
     */
    private final CopyOnWriteArrayList<FlexClient> flexClients = new CopyOnWriteArrayList<FlexClient>();

    /**
     * List of associated MessageClients created while this session was active (thread local).
     */
    private volatile CopyOnWriteArrayList<MessageClient> messageClients;

    /**
     * Storage for remote credentials associated with the session; used by the HTTPProxyService
     * when requests are made to a secured remote endpoint.
     */
    private volatile Map remoteCredentials;

    //--------------------------------------------------------------------------
    //
    // Properties
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  asyncPollMap
    //----------------------------------

    /**
     * Used internally to manage async long-polls; not for public use.
     * <p>
     * A map of endpoint to async poll objects that keeps track of what
     * client is parked on a long-poll with what endpoint.
     * We only want an endpoint to have a single connection to a client.
     * Normally, the server leaves an async long-poll in place until
     * data arrives to push to the client or a timeout is reached.
     * However, if two or more browser tabs/windows are sharing the same server session and are both attempting
     * async long-polling, we alternate their parked requests to avoid locking up the browser process by holding too many
     * Http connections open at once.
     * This also aids with closing out 'orphaned' long polls following a browser page reload.
     * Generically typed as <code>Object</code>; using this reference is left up to async poll
     * implementation code.
     */
    public volatile HashMap<String, FlexClient.AsyncPollWithTimeout> asyncPollMap;

    //----------------------------------
    //  flexSessionProvider
    //----------------------------------

    private final AbstractFlexSessionProvider sessionProvider;

    /**
     * Returns the session provider that created this instance.
     *
     * @return The session provider that created this instance.
     */
    public AbstractFlexSessionProvider getFlexSessionProvider() {
        return sessionProvider;
    }

    //----------------------------------
    //  principal
    //----------------------------------

    /**
     * The principal associated with the session.
     */
    private Principal userPrincipal;

    /**
     * This method should be called on FlexContext and not on this class.  Keeping
     * this method for backwards compatibility.  This method will produce
     * correct results when perClientAuthentication is false.  However, it will
     * not return correct results when perClientAuthentication is true.
     * <p>
     * Returns the principal associated with the session. If the client has not
     * authenticated the principal will be null.
     *
     * @return The principal associated with the session.
     */
    public Principal getUserPrincipal() {
        synchronized (lock) {
            checkValid();
            return userPrincipal;
        }
    }

    /**
     * This method should be called on FlexContext and not on this class.  Keeping
     * this method for backwards compatibility.  Calling this when perClientAuthentication
     * is true will not correctly set the UserPrincipal.
     *
     * @param userPrincipal The principal to associate with the session.
     */
    public void setUserPrincipal(Principal userPrincipal) {
        synchronized (lock) {
            checkValid();
            this.userPrincipal = userPrincipal;
        }
    }

    //----------------------------------
    //  canStream
    //----------------------------------

    /**
     * Used internally by streaming endpoints to enforce session level streaming
     * connection limits; not for public use.
     * This flag is volatile to allow for consistent reads across thread without
     * needing to pay the cost for a synchronized lock for each read.
     */
    public volatile boolean canStream = true;

    //----------------------------------
    //  maxConnectionsPerSession
    //----------------------------------

    /**
     * Used internally by streaming and long polling endpoints to enforce session
     * level streaming connection limits; not for public use. Default value is -1
     * (limitless)
     */
    public int maxConnectionsPerSession = MAX_CONNECTIONS_PER_SESSION_UNLIMITED;

    //----------------------------------
    //  streamingClientsCount
    //----------------------------------

    /**
     * Used internally by streaming and long polling endpoints to enforce
     * session level streaming connection limits; not for public use.
     * <p>
     * Some browsers put limits on the number of connections per session. For
     * example, Firefox has network.http.max-connections-per-server=8 limit which
     * limits the number of streaming connections per session to 7. Similarly,
     * IE has a limit of 2 per session.
     * <p>
     * This variable is used by streaming and long polling endpoint to keep
     * track of open connections per session and disallow them when needed.
     */
    public int streamingConnectionsCount;

    //----------------------------------
    //  useSmallMessages
    //----------------------------------

    /**
     *
     */
    private boolean useSmallMessages;

    /**
     * Determines whether the server can attempt to send small messages
     * for those messages that have a small form. This setting can be overridden
     * by an endpoint's enableSmallMessages switch which controls whether
     * small messages should be sent, even if they are supported.
     * <p>
     * The default is false.
     *
     * @return true if the server can attempt to send small messages.
     */
    public boolean useSmallMessages() {
        return useSmallMessages;
    }

    /**
     * @param value true if the server can attempt to send small messages.
     */
    public void setUseSmallMessages(boolean value) {
        useSmallMessages = value;
    }

    //----------------------------------
    //  waitMonitor
    //----------------------------------

    /**
     * Used internally to manage wait()-based long-polls; not for public use.
     * <p>
     * This is the monitor that a request handling thread associated with this
     * FlexSession is waiting on. Normally, the waiting request handling thread will wait until
     * a new message arrives that can be returned in a poll response or its wait interval times out.
     * This also aids with closing out 'orphaned' long polls following a browser page reload.
     */
    public volatile HashMap<String, FlexClient.EndpointQueue> waitMonitor;

    //--------------------------------------------------------------------------
    //
    // Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Adds a session attribute listener that will be notified when an
     * attribute is added, removed or changed.
     *
     * @param listener The listener to add.
     */
    public void addSessionAttributeListener(FlexSessionAttributeListener listener) {
        if (listener != null) {
            checkValid();

            if (attributeListeners == null) {
                synchronized (lock) {
                    if (attributeListeners == null)
                        attributeListeners = new CopyOnWriteArrayList<FlexSessionAttributeListener>();
                }
            }

            attributeListeners.addIfAbsent(listener);
        }
    }

    /**
     * Adds a session destroy listener that will be notified when the session
     * is destroyed. Session destroy listeners are notified after all attributes
     * have been unbound from the session and any FlexSessionBindingListeners
     * and FlexSessionAttributeListeners have been notified.
     *
     * @param listener The listener to add.
     * @see flex.messaging.FlexSessionListener
     */
    public void addSessionDestroyedListener(FlexSessionListener listener) {
        if (listener != null) {
            checkValid();

            if (destroyedListeners == null) {
                synchronized (lock) {
                    if (destroyedListeners == null)
                        destroyedListeners = new CopyOnWriteArrayList<FlexSessionListener>();
                }
            }

            destroyedListeners.addIfAbsent(listener);
        }
    }

    /**
     * Returns the attribute bound to the specified name in the session, or null
     * if no attribute is bound under the name.
     *
     * @param name The name the target attribute is bound to.
     * @return The attribute bound to the specified name.
     */
    public Object getAttribute(String name) {
        synchronized (lock) {
            checkValid();

            return (attributes == null) ? null : attributes.get(name);
        }
    }

    /**
     * Returns a snapshot of the names of all attributes bound to the session.
     *
     * @return A snapshot of the names of all attributes bound to the session.
     */
    public Enumeration<String> getAttributeNames() {
        synchronized (lock) {
            checkValid();

            if (attributes == null)
                return Collections.enumeration(Collections.<String>emptyList());

            // Return a copy so we do not run into concurrent modification problems if
            // someone adds to the attributes while iterating through the returned enumeration.
            return Collections.enumeration(new ArrayList<String>(attributes.keySet()));
        }
    }

    /**
     * Implements MessageClientListener.
     * Handling created events is a no-op.
     *
     * @param messageClient The new MessageClient.
     */
    public void messageClientCreated(MessageClient messageClient) {
    }

    /**
     * Implements MessageClientListener.
     * Notification that an associated MessageClient was destroyed.
     *
     * @param messageClient The MessageClient that was destroyed.
     */
    public void messageClientDestroyed(MessageClient messageClient) {
        unregisterMessageClient(messageClient);
    }

    /**
     * FlexClient invokes this to determine whether the session can be used to push messages
     * to the client.
     *
     * @return true if the FlexSession supports direct push; otherwise false (polling is assumed).
     */
    public abstract boolean isPushSupported();

    /**
     * FlexClient invokes this to push a message to a remote client.
     *
     * @param message The message to push.
     */
    public void push(Message message) {
        throw new UnsupportedOperationException("Push not supported.");
    }

    /**
     * Removes the attribute bound to the specified name in the session.
     *
     * @param name The name of the attribute to remove.
     */
    public void removeAttribute(String name) {
        Object value; // Used for event dispatch after the attribute is removed.

        synchronized (lock) {
            checkValid(); // Re-enters lock but should be fast because we're already holding it.

            value = (attributes != null) ? attributes.remove(name) : null;
        }

        // If no value was bound under this name it's a no-op.
        if (value == null)
            return;

        notifyAttributeUnbound(name, value);
        notifyAttributeRemoved(name, value);
    }

    /**
     * Removes a session attribute listener.
     *
     * @param listener The listener to remove.
     */
    public void removeSessionAttributeListener(FlexSessionAttributeListener listener) {
        // No need to check validity; removing a listener is always ok.
        if (listener != null && attributeListeners != null)
            attributeListeners.remove(listener);
    }

    /**
     * Removes a session destroy listener.
     *
     * @param listener The listener to remove.
     * @see flex.messaging.FlexSessionListener
     */
    public void removeSessionDestroyedListener(FlexSessionListener listener) {
        // No need to check validity; removing a listener is always ok.
        if (listener != null && destroyedListeners != null)
            destroyedListeners.remove(listener);
    }

    /**
     * Binds an attribute value to the session under the specified name.
     *
     * @param name  The name to bind the attribute under.
     * @param value The value of the attribute.
     */
    public void setAttribute(String name, Object value) {
        // Null value is the same as removeAttribute().
        if (value == null) {
            removeAttribute(name);
            return;
        }

        Object oldValue; // Used to determine which events to dispatch after the set is performed.

        // Only synchronize for the attribute mutation; event dispatch doesn't require it.
        synchronized (lock) {
            checkValid(); // Re-enters lock but should be fast because we're already holding it.

            if (attributes == null)
                attributes = new HashMap<String, Object>();

            oldValue = attributes.put(name, value);
        }

        if (oldValue == null) {
            notifyAttributeBound(name, value);
            notifyAttributeAdded(name, value);
        } else {
            notifyAttributeUnbound(name, oldValue);
            notifyAttributeReplaced(name, oldValue);
            notifyAttributeBound(name, value);
        }
    }

    /**
     * Stores remote credentials in the session for proxied calls to remote systems.
     *
     * @param credentials The remote credentials.
     */
    public void putRemoteCredentials(FlexRemoteCredentials credentials) {
        if (credentials != null) {
            // We only need to hold the lock to lazy-init the remoteCredentials variable.
            if (remoteCredentials == null) {
                synchronized (lock) {
                    // Init size to 4 because that's the number of shipping service types
                    // (messaging, remoting, proxy, data management).
                    if (remoteCredentials == null)
                        remoteCredentials = new HashMap(4);
                }
            }
            synchronized (remoteCredentials) {
                Map serviceMap = (Map) remoteCredentials.get(credentials.getService());
                if (serviceMap == null) {
                    // Init size to half the normal number of buckets; most services won't have a large
                    // number of destinations with remote credentials.
                    serviceMap = new HashMap(7);
                    remoteCredentials.put(credentials.getService(), serviceMap);
                }
                serviceMap.put(credentials.getDestination(), credentials);
            }
        }
    }

    /**
     * Returns the remote credentials stored in the session for the specified service destination.
     *
     * @param serviceId     The service id.
     * @param destinationId The destination id.
     * @return The stored remote credentials for the specified service destination.
     */
    public FlexRemoteCredentials getRemoteCredentials(String serviceId, String destinationId) {
        if (serviceId != null && destinationId != null) {
            if (remoteCredentials == null)
                return null;
            synchronized (remoteCredentials) {
                Map serviceMap = (Map) remoteCredentials.get(serviceId);
                return (serviceMap != null) ? (FlexRemoteCredentials) serviceMap.get(destinationId) : null;
            }
        }
        return null;
    }

    /**
     * Clears any stored remote credentials from the session for the specified service destination.
     *
     * @param serviceId     The service Id.
     * @param destinationId The destination Id.
     */
    public void clearRemoteCredentials(String serviceId, String destinationId) {
        if (serviceId != null && destinationId != null) {
            if (remoteCredentials == null)
                return;
            synchronized (remoteCredentials) {
                Map serviceMap = (Map) remoteCredentials.get(serviceId);
                if (serviceMap != null) {
                    serviceMap.put(destinationId, null);
                }
            }
        }
    }

    /**
     * Invalidates the FlexSession.
     */
    public void invalidate() {
        synchronized (lock) {
            if (!valid || invalidating)
                return; // Already shutting down.

            invalidating = true; // This thread gets to shut the FlexSession down.
            cancelTimeout();
            if (sessionProvider != null)
                sessionProvider.removeFlexSession(this);
        }

        // Unregister all FlexClients.
        if (!flexClients.isEmpty()) {
            for (FlexClient flexClient : flexClients)
                unregisterFlexClient(flexClient);
        }

        // Invalidate associated MessageClient subscriptions.
        if (messageClients != null && !messageClients.isEmpty()) {
            for (Iterator<MessageClient> iter = messageClients.iterator(); iter.hasNext(); ) {
                MessageClient messageClient = iter.next();
                messageClient.removeMessageClientDestroyedListener(this);
                messageClient.invalidate();
            }
            messageClients.clear();
        }


        // Notify destroy listeners that we're shutting the FlexSession down.
        if (destroyedListeners != null && !destroyedListeners.isEmpty()) {
            for (FlexSessionListener destroyListener : destroyedListeners) {
                destroyListener.sessionDestroyed(this);
            }
            destroyedListeners.clear();
        }

        // Unbind all attributes.
        if (attributes != null && !attributes.isEmpty()) {
            Set<String> keySet = attributes.keySet();
            String[] keys = keySet.toArray(new String[keySet.size()]);
            for (String key : keys)
                removeAttribute(key);

            attributes = null;
        }

        internalInvalidate();

        synchronized (lock) {
            valid = false;
            invalidating = false;
        }

        // Notify any waiting threads.
        if (waitMonitor != null) {
            for (FlexClient.EndpointQueue endpointQueue : waitMonitor.values()) {
                synchronized (endpointQueue) {
                    endpointQueue.notifyAll();
                }
            }
        }
    }

    /**
     * Hook for subclasses to perform any custom shutdown.
     * Invoked after the FlexSession has performed generic shutdown but right before the session's valid
     * property flips to false.
     */
    protected void internalInvalidate() {
    }

    /**
     * Returns a snapshot of the FlexClients associated with the FlexSession
     * when this method is invoked.
     * This list is not guaranteed to remain consistent with the actual list
     * of active FlexClients associated with the FlexSession over time.
     *
     * @return A snapshot of the current list of FlexSessions associated with the FlexClient.
     */
    public List<FlexClient> getFlexClients() {
        List<FlexClient> currentFlexClients = null;
        synchronized (lock) {
            checkValid(); // Re-enters lock but should be fast because we're already holding it.

            currentFlexClients = new ArrayList<FlexClient>(flexClients); // Make a copy of the current list to return.
        }
        return currentFlexClients;
    }

    /**
     * Returns a snapshot of the MessageClients (subscriptions) associated with the FlexSession
     * when this method is invoked.
     * This list is not guaranteed to remain consistent with the actual list
     * of active MessageClients associated with the FlexSession over time.
     *
     * @return A snapshot of the current list of MessageClients associated with the FlexSession.
     */
    public List<MessageClient> getMessageClients() {
        List<MessageClient> currentMessageClients = null;
        synchronized (lock) {
            checkValid(); // Re-enters lock but should be fast because we're already holding it.

            currentMessageClients = (messageClients != null) ? new ArrayList<MessageClient>(messageClients) // Make a copy of the current list to return.
                    : new ArrayList<MessageClient>(); // Return an empty list.
        }
        return currentMessageClients;
    }

    /**
     * Returns the Id for the session.
     *
     * @return The Id for the session.
     */
    public abstract String getId();

    /**
     * Returns whether the current user is in the specified role.
     *
     * @param role The role to test.
     * @return true if the user is in the role; otherwise false.
     */
    public boolean isUserInRole(String role) {
        ArrayList list = new ArrayList();
        list.add(role);
        return FlexContext.getMessageBroker().getLoginManager().checkRoles(userPrincipal, list);
    }

    /**
     * Returns whether the session is valid.
     *
     * @return true if the session is valid; otherwise false.
     */
    public boolean isValid() {
        synchronized (lock) {
            return valid;
        }
    }

    /**
     * Implements FlexClientListener interface.
     * Notification that a FlexClient was created.
     * This is a no-op because the FlexSession is never added as a static FlexClient created listener
     * but this method is required by the interface. We only listen for the destroyed event from
     * associated FlexClients.
     *
     * @param flexClient The FlexClient that was created.
     */
    public void clientCreated(FlexClient flexClient) {
    }

    /**
     * Implements FlexClientListener interface.
     * Notification that an associated FlexClient was destroyed.
     *
     * @param flexClient The FlexClient that was destroyed.
     */
    public void clientDestroyed(FlexClient flexClient) {
        unregisterFlexClient(flexClient);
    }

    /**
     * Used internally to associate a FlexClient with the FlexSession.
     *
     * @param flexClient The FlexClient to assocaite with the session.
     */
    public void registerFlexClient(FlexClient flexClient) {
        if (flexClients.addIfAbsent(flexClient)) {
            flexClient.addClientDestroyedListener(this);
            flexClient.registerFlexSession(this);
        }
    }

    /**
     * Used internally to disassociate a FlexClient from the FlexSession.
     *
     * @param flexClient The FlexClient to disassociate from the session.
     */
    public void unregisterFlexClient(FlexClient flexClient) {
        if (flexClients.remove(flexClient)) {
            flexClient.removeClientDestroyedListener(this);
            flexClient.unregisterFlexSession(this);
        }
    }

    /**
     * Used internally to associate a MessagClient (subscription) with the FlexSession.
     *
     * @param messageClient The MessageClient to associate with the session.
     */
    public void registerMessageClient(MessageClient messageClient) {
        if (messageClients == null) {
            synchronized (lock) {
                if (messageClients == null)
                    messageClients = new CopyOnWriteArrayList<MessageClient>();
            }
        }

        if (messageClients.addIfAbsent(messageClient))
            messageClient.addMessageClientDestroyedListener(this);
    }

    /**
     * Used internally to disassociate a MessageClient (subscription) from a FlexSession.
     *
     * @param messageClient The MessageClient to disassociate from the session.
     */
    public void unregisterMessageClient(MessageClient messageClient) {
        if (messageClients != null && messageClients.remove(messageClient))
            messageClient.removeMessageClientDestroyedListener(this);
    }

    /**
     * Default implementation invokes <code>invalidate()</code> upon timeout.
     *
     * @see flex.messaging.util.TimeoutCapable#timeout()
     */
    public void timeout() {
        invalidate();
    }

    //--------------------------------------------------------------------------
    //
    // Protected Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Ensures that the session has not been invalidated.
     */
    protected void checkValid() {
        synchronized (lock) {
            if (!valid) {
                LocalizedException e = new LocalizedException();
                e.setMessage(FLEX_SESSION_INVALIDATED);
                throw e;
            }
        }
    }

    /**
     * Notify attribute listeners that an attribute has been added.
     *
     * @param name  The name of the attribute.
     * @param value The new value of the attribute.
     */
    protected void notifyAttributeAdded(String name, Object value) {
        if (attributeListeners != null && !attributeListeners.isEmpty()) {
            FlexSessionBindingEvent event = new FlexSessionBindingEvent(this, name, value);
            // CopyOnWriteArrayList is iteration-safe from ConcurrentModificationExceptions.
            for (FlexSessionAttributeListener attribListener : attributeListeners)
                attribListener.attributeAdded(event);
        }
    }

    /**
     * Notify binding listener that it has been bound to the session.
     *
     * @param name  The attribute name.
     * @param value The attribute that has been bound.
     */
    protected void notifyAttributeBound(String name, Object value) {
        if ((value != null) && (value instanceof FlexSessionBindingListener)) {
            FlexSessionBindingEvent bindingEvent = new FlexSessionBindingEvent(this, name);
            ((FlexSessionBindingListener) value).valueBound(bindingEvent);
        }
    }

    /**
     * Notify attribute listeners that an attribute has been removed.
     *
     * @param name  The name of the attribute.
     * @param value The previous value of the attribute.
     */
    protected void notifyAttributeRemoved(String name, Object value) {
        if (attributeListeners != null && !attributeListeners.isEmpty()) {
            FlexSessionBindingEvent event = new FlexSessionBindingEvent(this, name, value);
            // CopyOnWriteArrayList is iteration-safe from ConcurrentModificationExceptions.
            for (FlexSessionAttributeListener attribListener : attributeListeners)
                attribListener.attributeRemoved(event);
        }
    }

    /**
     * Notify attribute listeners that an attribute has been replaced.
     *
     * @param name  The name of the attribute.
     * @param value The previous value of the attribute.
     */
    protected void notifyAttributeReplaced(String name, Object value) {
        if (attributeListeners != null && !attributeListeners.isEmpty()) {
            FlexSessionBindingEvent event = new FlexSessionBindingEvent(this, name, value);
            // CopyOnWriteArrayList is iteration-safe from ConcurrentModificationExceptions.
            for (FlexSessionAttributeListener attribListener : attributeListeners)
                attribListener.attributeReplaced(event);
        }
    }

    /**
     * Notify binding listener that it has been unbound from the session.
     *
     * @param name  The attribute name.
     * @param value The attribute that has been unbound.
     */
    protected void notifyAttributeUnbound(String name, Object value) {
        if ((value != null) && (value instanceof FlexSessionBindingListener)) {
            FlexSessionBindingEvent bindingEvent = new FlexSessionBindingEvent(this, name);
            ((FlexSessionBindingListener) value).valueUnbound(bindingEvent);
        }
    }

    /**
     * Invoked by subclass upon session creation to notify all registered
     * session create listeners of the event.
     * This method must be invoked in the subclass constructor.
     */
    protected void notifyCreated() {
        // This guard is here only to prevent duplicate notifications if there's a coding error
        // in the subclass. Not likely..
        synchronized (lock) {
            if (creationNotified)
                return;

            creationNotified = true;
        }

        if (!createdListeners.isEmpty()) {
            // CopyOnWriteArrayList is iteration-safe from ConcurrentModificationExceptions.
            for (Iterator<FlexSessionListener> iter = createdListeners.iterator(); iter.hasNext(); )
                iter.next().sessionCreated(this);
        }
    }
}
