All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dorkbox.network.connection.ConnectionManager Maven / Gradle / Ivy

/*
 * Copyright 2010 dorkbox, llc
 *
 * Licensed 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 dorkbox.network.connection;

import dorkbox.network.rmi.RmiMessages;
import dorkbox.network.util.ConcurrentHashMapFactory;
import dorkbox.util.ClassHelper;
import org.slf4j.Logger;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.CopyOnWriteArrayList;

//note that we specifically DO NOT implement equals/hashCode, because we cannot create two separate
// objects that are somehow equal to each other.
public
class ConnectionManager implements ListenerBridge, ISessionManager {

    public static Listener unRegisteredType_Listener = null;

    // these are final, because the REFERENCE to these will never change. They ARE NOT immutable objects (meaning their content can change)
    private final ConcurrentHashMapFactory>> listeners;
    private final ConcurrentHashMapFactory> localManagers;
    private final CopyOnWriteArrayList connections = new CopyOnWriteArrayList();

    /**
     * Used by the listener subsystem to determine types.
     */
    private final Class baseClass;
    protected final org.slf4j.Logger logger;
    volatile boolean shutdown = false;

    public
    ConnectionManager(final String loggerName, final Class baseClass) {
        this.logger = org.slf4j.LoggerFactory.getLogger(loggerName);

        this.baseClass = baseClass;

        this.listeners = new ConcurrentHashMapFactory>>() {
            private static final long serialVersionUID = 1L;

            @Override
            public
            CopyOnWriteArrayList> createNewObject(Object... args) {
                return new CopyOnWriteArrayList>();
            }
        };

        this.localManagers = new ConcurrentHashMapFactory>() {
            private static final long serialVersionUID = 1L;

            @Override
            public
            ConnectionManager createNewObject(Object... args) {
                return new ConnectionManager(loggerName + "-" + args[0] + " Specific", ConnectionManager.this.baseClass);
            }
        };
    }

    /**
     * Adds a listener to this connection/endpoint to be notified of
     * connect/disconnect/idle/receive(object) events.
     * 

* If the listener already exists, it is not added again. *

* When called by a server, NORMALLY listeners are added at the GLOBAL level * (meaning, I add one listener, and ALL connections are notified of that * listener. *

* It is POSSIBLE to add a server connection ONLY (ie, not global) listener * (via connection.addListener), meaning that ONLY that listener attached to * the connection is notified on that event (ie, admin type listeners) */ @SuppressWarnings("rawtypes") @Override public final void add(final ListenerRaw listener) { if (listener == null) { throw new IllegalArgumentException("listener cannot be null."); } // find the class that uses Listener.class. Class clazz = listener.getClass(); while (clazz.getSuperclass() != ListenerRaw.class) { clazz = clazz.getSuperclass(); } // this is the connection generic parameter for the listener Class genericClass = ClassHelper.getGenericParameterAsClassForSuperClass(clazz, 0); // if we are null, it means that we have no generics specified for our listener! //noinspection IfStatementWithIdenticalBranches if (genericClass == this.baseClass || genericClass == null) { // we are the base class, so we are fine. addListener0(listener); return; } else if (ClassHelper.hasInterface(Connection.class, genericClass) && !ClassHelper.hasParentClass(this.baseClass, genericClass)) { // now we must make sure that the PARENT class is NOT the base class. ONLY the base class is allowed! addListener0(listener); return; } // didn't successfully add the listener. throw new IllegalArgumentException("Unable to add incompatible connection type as a listener! : " + this.baseClass); } /** * INTERNAL USE ONLY */ @SuppressWarnings({"unchecked", "rawtypes"}) private void addListener0(final ListenerRaw listener) { Class type = listener.getObjectType(); CopyOnWriteArrayList> list = this.listeners.getOrCreate(type); list.addIfAbsent(listener); Logger logger2 = this.logger; if (logger2.isTraceEnabled()) { logger2.trace("listener added: {} <{}>", listener.getClass() .getName(), listener.getObjectType()); } } /** * Removes a listener from this connection/endpoint to NO LONGER be notified * of connect/disconnect/idle/receive(object) events. *

* When called by a server, NORMALLY listeners are added at the GLOBAL level * (meaning, I add one listener, and ALL connections are notified of that * listener. *

* It is POSSIBLE to remove a server-connection 'non-global' listener (via * connection.removeListener), meaning that ONLY that listener attached to * the connection is removed */ @SuppressWarnings("rawtypes") @Override public final void remove(final ListenerRaw listener) { if (listener == null) { throw new IllegalArgumentException("listener cannot be null."); } Class type = listener.getObjectType(); CopyOnWriteArrayList> list = this.listeners.get(type); if (list != null) { list.remove(listener); } Logger logger2 = this.logger; if (logger2.isTraceEnabled()) { logger2.trace("listener removed: {} <{}>", listener.getClass() .getName(), listener.getObjectType()); } } /** * Removes all registered listeners from this connection/endpoint to NO * LONGER be notified of connect/disconnect/idle/receive(object) events. */ @Override public final void removeAll() { this.listeners.clear(); Logger logger2 = this.logger; if (logger2.isTraceEnabled()) { logger2.trace("all listeners removed !!"); } } /** * Removes all registered listeners (of the object type) from this * connection/endpoint to NO LONGER be notified of * connect/disconnect/idle/receive(object) events. */ @Override public final void removeAll(final Class classType) { if (classType == null) { throw new IllegalArgumentException("classType cannot be null."); } this.listeners.remove(classType); Logger logger2 = this.logger; if (logger2.isTraceEnabled()) { logger2.trace("all listeners removed for type: {}", classType.getClass() .getName()); } } /** * Invoked when a message object was received from a remote peer. *

* If data is sent in response to this event, the connection data is automatically flushed to the wire. If the data is sent in a separate thread, * {@link EndPoint#send().flush()} must be called manually. *

* {@link ISessionManager} */ @Override public final void notifyOnMessage(final C connection, final Object message) { notifyOnMessage0(connection, message, false); } private boolean notifyOnMessage0(final C connection, final Object message, boolean foundListener) { Class objectType = message.getClass(); // this is the GLOBAL version (unless it's the call from below, then it's the connection scoped version) CopyOnWriteArrayList> list = this.listeners.get(objectType); if (list != null) { for (ListenerRaw listener : list) { if (this.shutdown) { return true; } listener.received(connection, message); } foundListener = true; } if (!(message instanceof RmiMessages)) { // we march through all super types of the object, and find the FIRST set // of listeners that are registered and cast it as that, and notify the method. // NOTICE: we do NOT call ALL TYPE -- meaning, if we have Object->Foo->Bar // and have listeners for Object and Foo // we will call Bar (from the above code) // we will call Foo (from this code) // we will NOT call Object (since we called Foo). If Foo was not registered, THEN we would call object! list = null; objectType = objectType.getSuperclass(); while (objectType != null) { // check to see if we have what we are looking for in our CURRENT class list = this.listeners.get(objectType); if (list != null) { break; } // NO MATCH, so walk up. objectType = objectType.getSuperclass(); } if (list != null) { for (ListenerRaw listener : list) { if (this.shutdown) { return true; } listener.received(connection, message); foundListener = true; } } } // now have to account for additional connection listener managers (non-global). ConnectionManager localManager = this.localManagers.get(connection); if (localManager != null) { // if we found a listener during THIS method call, we need to let the NEXT method call know, // so it doesn't spit out error for not handling a message (since that message MIGHT have // been found in this method). foundListener |= localManager.notifyOnMessage0(connection, message, foundListener); } // only run a flush once if (foundListener) { connection.send() .flush(); } else if (unRegisteredType_Listener != null) { unRegisteredType_Listener.received(connection, null); } else { Logger logger2 = this.logger; if (logger2.isErrorEnabled()) { this.logger.error("----------- LISTENER NOT REGISTERED FOR TYPE: {}", message.getClass() .getSimpleName()); } } return foundListener; } /** * Invoked when a Connection has been idle for a while. *

* {@link ISessionManager} */ @Override public final void notifyOnIdle(final C connection) { Set>>> entrySet = this.listeners.entrySet(); CopyOnWriteArrayList> list; for (Entry>> entry : entrySet) { list = entry.getValue(); if (list != null) { for (ListenerRaw listener : list) { if (this.shutdown) { return; } try { listener.idle(connection); } catch (IOException e) { logger.error("Unable to notify listener on idle.", e); } } connection.send() .flush(); } } // now have to account for additional (local) listener managers. ConnectionManager localManager = this.localManagers.get(connection); if (localManager != null) { localManager.notifyOnIdle(connection); } } /** * Invoked when a Channel is open, bound to a local address, and connected to a remote address. *

* {@link ISessionManager} */ @Override public void connectionConnected(final C connection) { // create a new connection! this.connections.add(connection); try { Set>>> entrySet = this.listeners.entrySet(); CopyOnWriteArrayList> list; for (Entry>> entry : entrySet) { list = entry.getValue(); if (list != null) { for (ListenerRaw listener : list) { if (this.shutdown) { return; } listener.connected(connection); } connection.send() .flush(); } } // now have to account for additional (local) listener managers. ConnectionManager localManager = this.localManagers.get(connection); if (localManager != null) { localManager.connectionConnected(connection); } } catch (Throwable t) { connectionError(connection, t); } } /** * Invoked when a Channel was disconnected from its remote peer. *

* {@link ISessionManager} */ @Override public void connectionDisconnected(final C connection) { Set>>> entrySet = this.listeners.entrySet(); CopyOnWriteArrayList> list; for (Entry>> entry : entrySet) { list = entry.getValue(); if (list != null) { for (ListenerRaw listener : list) { if (this.shutdown) { return; } listener.disconnected(connection); } } } // now have to account for additional (local) listener managers. ConnectionManager localManager = this.localManagers.get(connection); if (localManager != null) { localManager.connectionDisconnected(connection); // remove myself from the "global" listeners so we can have our memory cleaned up. this.localManagers.remove(connection); } this.connections.remove(connection); } /** * Invoked when there is an error of some kind during the up/down stream process *

* {@link ISessionManager} */ @Override public void connectionError(final C connection, final Throwable throwable) { Set>>> entrySet = this.listeners.entrySet(); CopyOnWriteArrayList> list; for (Entry>> entry : entrySet) { list = entry.getValue(); if (list != null) { for (ListenerRaw listener : list) { if (this.shutdown) { return; } listener.error(connection, throwable); } connection.send() .flush(); } } // now have to account for additional (local) listener managers. ConnectionManager localManager = this.localManagers.get(connection); if (localManager != null) { localManager.connectionError(connection, throwable); } } /** * Returns a non-modifiable list of active connections *

* {@link ISessionManager} */ @Override public List getConnections() { return Collections.unmodifiableList(this.connections); } final ConnectionManager addListenerManager(final Connection connection) { // when we are a server, NORMALLY listeners are added at the GLOBAL level (meaning, I add one listener, and ALL connections // are notified of that listener. // it is POSSIBLE to add a connection-specific listener (via connection.addListener), meaning that ONLY // that listener is notified on that event (ie, admin type listeners) ConnectionManager lm = this.localManagers.getOrCreate(connection, connection.toString()); Logger logger2 = this.logger; if (logger2.isTraceEnabled()) { this.logger.trace("Connection specific Listener Manager added on connection: {}", connection); } return lm; } final void removeListenerManager(final Connection connection) { this.localManagers.remove(connection); } /** * BE CAREFUL! Only for internal use! * * @return Returns a FAST list of active connections. */ public final Collection getConnections0() { return this.connections; } /** * BE CAREFUL! Only for internal use! * * @return Returns a FAST first connection (for client!). */ public final C getConnection0() throws IOException { if (this.connections.iterator() .hasNext()) { return this.connections.iterator() .next(); } else { throw new IOException("Not connected to a remote computer. Unable to continue!"); } } /** * BE CAREFUL! Only for internal use! * * @return a boolean indicating if there are any listeners registered with this manager. */ final boolean hasListeners() { return this.listeners.isEmpty(); } /** * Closes all associated resources/threads/connections */ final void stop() { this.shutdown = true; // disconnect the sessions closeConnections(); this.listeners.clear(); } /** * Close all connections ONLY */ final void closeConnections() { // close the sessions Iterator iterator = this.connections.iterator(); //noinspection WhileLoopReplaceableByForEach while (iterator.hasNext()) { Connection connection = iterator.next(); // Close the connection. Make sure the close operation ends because // all I/O operations are asynchronous in Netty. // Necessary otherwise workers won't close. connection.close(); } this.connections.clear(); } @Override public boolean equals(final Object o) { return false; } @Override public int hashCode() { return 0; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy