org.restcomm.media.control.mgcp.endpoint.GenericMgcpEndpoint.java Source code

Java tutorial

Introduction

Here is the source code for org.restcomm.media.control.mgcp.endpoint.GenericMgcpEndpoint.java

Source

/*
 * TeleStax, Open Source Cloud Communications
 * Copyright 2011-2016, Telestax Inc and individual contributors
 * by the @authors tag. 
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.restcomm.media.control.mgcp.endpoint;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.log4j.Logger;
import org.restcomm.media.control.mgcp.command.NotificationRequest;
import org.restcomm.media.control.mgcp.command.param.NotifiedEntity;
import org.restcomm.media.control.mgcp.connection.MgcpConnection;
import org.restcomm.media.control.mgcp.connection.MgcpConnectionProvider;
import org.restcomm.media.control.mgcp.connection.MgcpConnectionState;
import org.restcomm.media.control.mgcp.exception.MgcpCallNotFoundException;
import org.restcomm.media.control.mgcp.exception.MgcpConnectionException;
import org.restcomm.media.control.mgcp.exception.MgcpConnectionNotFoundException;
import org.restcomm.media.control.mgcp.exception.UnsupportedMgcpEventException;
import org.restcomm.media.control.mgcp.message.MessageDirection;
import org.restcomm.media.control.mgcp.message.MgcpMessage;
import org.restcomm.media.control.mgcp.message.MgcpMessageObserver;
import org.restcomm.media.control.mgcp.message.MgcpParameterType;
import org.restcomm.media.control.mgcp.message.MgcpRequest;
import org.restcomm.media.control.mgcp.message.MgcpRequestType;
import org.restcomm.media.control.mgcp.pkg.MgcpEvent;
import org.restcomm.media.control.mgcp.pkg.MgcpRequestedEvent;
import org.restcomm.media.control.mgcp.pkg.MgcpSignal;
import org.restcomm.media.control.mgcp.pkg.SignalType;
import org.restcomm.media.control.mgcp.pkg.r.rto.RtpTimeoutEvent;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;

/**
 * Abstract representation of an MGCP Endpoint that groups connections by calls.
 * 
 * @author Henrique Rosa (henrique.rosa@telestax.com)
 *
 */
public class GenericMgcpEndpoint implements MgcpEndpoint {

    private static final Logger log = Logger.getLogger(GenericMgcpEndpoint.class);

    private static final MgcpRequestedEvent[] EMPTY_ENDPOINT_EVENTS = new MgcpRequestedEvent[0];

    // MGCP Components
    private final MgcpConnectionProvider connectionProvider;
    protected final MediaGroup mediaGroup;

    // Endpoint Properties
    private final EndpointIdentifier endpointId;
    private final ConcurrentHashMap<Integer, MgcpConnection> connections;

    // Endpoint State
    private final AtomicBoolean active;

    // Events and Signals
    private NotifiedEntity notifiedEntity;
    private ConcurrentHashMap<String, MgcpSignal> signals;
    // TODO requestedEndpointEvents needs to be synchronized!
    private MgcpRequestedEvent[] requestedEndpointEvents;
    private final Multimap<Integer, MgcpRequestedEvent> requestedConnectionEvents;

    // Observers
    private final Set<MgcpEndpointObserver> endpointObservers;
    private final Set<MgcpMessageObserver> messageObservers;

    public GenericMgcpEndpoint(EndpointIdentifier endpointId, MgcpConnectionProvider connectionProvider,
            MediaGroup mediaGroup) {
        // MGCP Components
        this.connectionProvider = connectionProvider;

        // Endpoint Properties
        this.endpointId = endpointId;
        this.connections = new ConcurrentHashMap<>(5);

        // Endpoint State
        this.active = new AtomicBoolean(false);

        // Media Components
        this.mediaGroup = mediaGroup;

        // Events and Signals
        this.notifiedEntity = new NotifiedEntity();
        this.signals = new ConcurrentHashMap<>(5);
        this.requestedEndpointEvents = EMPTY_ENDPOINT_EVENTS;
        this.requestedConnectionEvents = Multimaps
                .synchronizedSetMultimap(HashMultimap.<Integer, MgcpRequestedEvent>create());

        // Observers
        this.endpointObservers = Sets.newConcurrentHashSet();
        this.messageObservers = Sets.newConcurrentHashSet();
    }

    @Override
    public EndpointIdentifier getEndpointId() {
        return this.endpointId;
    }

    @Override
    public MediaGroup getMediaGroup() {
        return this.mediaGroup;
    }

    public boolean hasConnections() {
        return !this.connections.isEmpty();
    }

    @Override
    public MgcpConnection getConnection(int callId, int connectionId) {
        MgcpConnection connection = this.connections.get(connectionId);
        if (connection != null && connection.getCallIdentifier() == callId) {
            return connection;
        }
        return null;
    }

    private boolean registerConnection(int callId, MgcpConnection connection) {
        MgcpConnection old = this.connections.putIfAbsent(connection.getIdentifier(), connection);
        boolean registered = (old == null);

        if (registered) {
            if (log.isDebugEnabled()) {
                log.debug("Endpoint " + this.endpointId.toString() + " registered connection "
                        + connection.getHexIdentifier() + " to call " + connection.getCallIdentifierHex());
            }

            // Observe connection
            connection.observe(this);

            // Warn child class that connection was created
            onConnectionCreated(connection);

            // Activate endpoint on first registered connection
            if (!isActive()) {
                activate();
            }
        }
        return registered;
    }

    @Override
    public MgcpConnection createConnection(int callId, boolean local) {
        MgcpConnection connection = local ? this.connectionProvider.provideLocal(callId)
                : this.connectionProvider.provideRemote(callId);
        registerConnection(callId, connection);
        if (!connection.isLocal()) {
            connection.observe(this);
        }
        return connection;
    }

    @Override
    public MgcpConnection deleteConnection(int callId, int connectionId)
            throws MgcpCallNotFoundException, MgcpConnectionNotFoundException {
        MgcpConnection connection = this.connections.get(connectionId);

        if (connection == null) {
            throw new MgcpConnectionNotFoundException(this.endpointId + " could not find connection "
                    + Integer.toHexString(connectionId).toUpperCase() + " in call "
                    + Integer.toHexString(callId).toUpperCase());
        } else if (connection.getCallIdentifier() != callId) {
            throw new MgcpCallNotFoundException(this.endpointId + " could not find connection "
                    + Integer.toHexString(connectionId).toUpperCase() + " in call "
                    + Integer.toHexString(callId).toUpperCase());
        }

        connection = this.connections.remove(connectionId);

        if (log.isDebugEnabled()) {
            log.debug("Endpoint " + this.endpointId + " unregistered connection " + connection.getHexIdentifier()
                    + " from call " + connection.getCallIdentifierHex());
        }

        // Warn child class that connection was deleted
        onConnectionDeleted(connection);

        // Set endpoint state
        if (!hasConnections() && isActive()) {
            deactivate();
        }

        // Unregister from connection and close it if necessary
        try {
            connection.forget(this);

            if (!MgcpConnectionState.CLOSED.equals(connection.getState())) {
                connection.close();
            }
        } catch (MgcpConnectionException e) {
            log.warn(this.endpointId + " could not close connection " + connection.getHexIdentifier()
                    + " in elegant manner.", e);
        }
        return connection;
    }

    @Override
    public List<MgcpConnection> deleteConnections(int callId) throws MgcpCallNotFoundException {
        // Fetch all current connections
        Collection<MgcpConnection> current = this.connections.values();
        List<MgcpConnection> deleted = new ArrayList<>(current.size());

        for (MgcpConnection connection : current) {
            // Delete connection if owned by specific call-id
            if (connection.getCallIdentifier() == callId) {
                MgcpConnection removed = this.connections.remove(connection.getIdentifier());
                if (removed != null) {
                    deleted.add(removed);
                }
            }
        }

        // No connections were found for specific call
        if (deleted.size() == 0) {
            throw new MgcpCallNotFoundException(
                    this.endpointId + " could not find call " + Integer.toHexString(callId).toUpperCase());
        }

        // Log deleted calls
        if (log.isDebugEnabled()) {
            String hexIdentifiers = Arrays.toString(getConnectionHexId(deleted));
            log.debug("Endpoint " + this.endpointId.toString() + " deleted " + deleted.size()
                    + " connections from call " + callId + ": " + hexIdentifiers + ". Connection count: "
                    + this.connections.size());
        }

        // Update endpoint state if all connections were deleted
        if (!hasConnections() && isActive()) {
            deactivate();
        }
        return deleted;
    }

    @Override
    public List<MgcpConnection> deleteConnections() {
        Set<Integer> keys = this.connections.keySet();
        List<MgcpConnection> deleted = new ArrayList<>(keys.size());

        for (Integer key : keys) {
            MgcpConnection connection = this.connections.remove(key);
            if (connection != null) {
                // Unregister from connection and close it if needed
                try {
                    connection.forget(this);
                    if (!MgcpConnectionState.CLOSED.equals(connection.getState())) {
                        connection.close();
                    }
                } catch (MgcpConnectionException e) {
                    log.warn(this.endpointId + " could not close connection " + connection.getHexIdentifier()
                            + " in elegant manner.", e);
                }

                // Add connection to list of deleted connections
                deleted.add(connection);
            }
        }

        // Log deleted calls
        if (log.isDebugEnabled()) {
            String hexIdentifiers = Arrays.toString(getConnectionHexId(deleted));
            log.debug("Endpoint " + this.endpointId.toString() + " deleted " + deleted.size() + " connections: "
                    + hexIdentifiers + ". Connection count: " + this.connections.size());
        }

        // Deactivate endpoint if no connections exist
        if (!hasConnections() && isActive()) {
            deactivate();
        }
        return deleted;
    }

    private String[] getConnectionHexId(Collection<MgcpConnection> connections) {
        String[] hex = new String[connections.size()];
        int index = 0;
        for (MgcpConnection connection : connections) {
            hex[index] = connection.getHexIdentifier();
            index++;
        }
        return hex;
    }

    public boolean isActive() {
        return this.active.get();
    }

    private void activate() throws IllegalStateException {
        if (this.active.get()) {
            throw new IllegalArgumentException("Endpoint " + this.endpointId + " is already active.");
        } else {
            this.active.set(true);
            onActivated();

            if (log.isDebugEnabled()) {
                log.debug("Endpoint " + this.endpointId.toString() + " is active.");
            }

            notify(this, MgcpEndpointState.ACTIVE);
        }
    }

    protected void deactivate() throws IllegalStateException {
        if (this.active.get()) {
            this.active.set(false);
            onDeactivated();

            if (log.isDebugEnabled()) {
                log.debug("Endpoint " + this.endpointId.toString() + " is inactive.");
            }

            notify(this, MgcpEndpointState.INACTIVE);
        } else {
            throw new IllegalArgumentException("Endpoint " + this.endpointId + " is already inactive.");
        }
    }

    @Override
    public synchronized void requestNotification(NotificationRequest request) {
        // Update Notified Entity (IF required)
        if (request.getNotifiedEntity() != null) {
            this.notifiedEntity = request.getNotifiedEntity();
        }

        // Clear requested events
        this.requestedEndpointEvents = EMPTY_ENDPOINT_EVENTS;
        this.requestedConnectionEvents.clear();

        // Update registered events
        int eventCount = request.getRequestedEvents().length;
        List<MgcpRequestedEvent> endpointEvents = new ArrayList<>(eventCount);

        for (MgcpRequestedEvent requestedEvent : request.getRequestedEvents()) {
            if (requestedEvent.getConnectionId() > 0) {
                int connectionId = requestedEvent.getConnectionId();
                MgcpConnection connection = this.connections.get(connectionId);

                if (connection == null) {
                    log.warn("Requested event " + requestedEvent.toString() + " was dropped because connection "
                            + Integer.toHexString(connectionId) + "was not found.");
                } else {
                    try {
                        // Process connection event
                        connection.listen(requestedEvent);

                        // Register event notification
                        this.requestedConnectionEvents.put(connectionId, requestedEvent);
                        if (log.isDebugEnabled()) {
                            log.debug("Endpoint " + this.endpointId + " requested event "
                                    + requestedEvent.getQualifiedName() + " to connection "
                                    + requestedEvent.getConnectionId());
                        }
                    } catch (UnsupportedMgcpEventException e) {
                        log.warn("Requested event " + requestedEvent.toString()
                                + " was dropped because it was not supported by connection "
                                + connection.getHexIdentifier(), e);
                    }
                }
            } else {
                // Process endpoint event
                endpointEvents.add(requestedEvent);
                if (log.isDebugEnabled()) {
                    log.debug("Endpoint " + this.endpointId + " is listening for event "
                            + requestedEvent.getQualifiedName());
                }
            }
        }

        if (endpointEvents.size() > 0) {
            this.requestedEndpointEvents = endpointEvents.toArray(new MgcpRequestedEvent[endpointEvents.size()]);
        }

        /*
         * https://tools.ietf.org/html/rfc3435#section-2.3.4
         * 
         * When a (possibly empty) list of signal(s) is supplied, this list completely replaces the current list of active
         * time-out signals.
         * 
         * Currently active time-out signals that are not provided in the new list MUST be stopped and the new signal(s)
         * provided will now become active.
         * 
         * Currently active time-out signals that are provided in the new list of signals MUST remain active without
         * interruption, thus the timer for such time-out signals will not be affected. Consequently, there is currently no way
         * to restart the timer for a currently active time-out signal without turning the signal off first.
         * 
         * If the time-out signal is parameterized, the original set of parameters MUST remain in effect, regardless of what
         * values are provided subsequently. A given signal MUST NOT appear more than once in a SignalRequests.
         */
        if (request.countSignals() == 0) {
            // List is empty. Cancel all ongoing events.
            Iterator<String> keys = this.signals.keySet().iterator();
            while (keys.hasNext()) {
                MgcpSignal ongoing = this.signals.get(keys.next());
                if (ongoing != null) {
                    ongoing.cancel();
                }
            }
        } else {
            // Execute signals listed in RQNT and cancel remaining
            List<String> retained = new ArrayList<>(request.countSignals());
            for (MgcpSignal signal = request.pollSignal(); signal != null; signal = request.pollSignal()) {
                final SignalType signalType = signal.getSignalType();
                switch (signalType) {
                case TIME_OUT:
                    // Mark this key to retain ongoing signals
                    String signalName = signal.getName();
                    retained.add(signalName);

                    // Register and execute signal IF NOT duplicate
                    MgcpSignal original = this.signals.putIfAbsent(signalName, signal);
                    if (original == null) {
                        signal.observe(this);
                        signal.execute();
                    }
                    break;

                case BRIEF:
                    // Brief signals can be executed right away and do not need to be queued.
                    // Their execution is fast and do not generate events.
                    signal.execute();
                    break;

                default:
                    log.warn("Dropping signal " + signal.toString() + " on endpoint " + getEndpointId().toString()
                            + " because signal type " + signalType + "is not supported.");
                    break;
                }
            }

            // Cancel every ongoing signal that was not retained
            Iterator<String> keys = this.signals.keySet().iterator();
            while (keys.hasNext()) {
                String key = keys.next();
                if (!retained.contains(key)) {
                    cancelSignal(key);
                }
            }
        }
    }

    @Override
    public void cancelSignal(String signal) {
        MgcpSignal ongoing = this.signals.get(signal);
        if (ongoing != null) {
            if (log.isDebugEnabled()) {
                log.debug("Canceling signal " + ongoing.toString() + " on endpoint " + getEndpointId().toString());
            }
            ongoing.cancel();
        }
    }

    /**
     * Event that is called when a new connection is created in the endpoint. <br>
     * <b>To be overridden by subclasses.</b>
     * 
     * @param connection
     */
    protected void onConnectionCreated(MgcpConnection connection) {
    }

    /**
     * Event that is called when a new connection is deleted in the endpoint. <br>
     * <b>To be overriden by subclasses.</b>
     * 
     * @param connection
     */
    protected void onConnectionDeleted(MgcpConnection connection) {
    }

    /**
     * Event that is called when endpoint becomes active. <br>
     * <b>To be overriden by subclasses.</b>
     * 
     * @param connection
     */
    protected void onActivated() {
    }

    /**
     * Event that is called when endpoint becomes inactive. <br>
     * <b>To be overriden by subclasses.</b>
     * 
     * @param connection
     */
    protected void onDeactivated() {
    }

    @Override
    public void observe(MgcpMessageObserver observer) {
        this.messageObservers.add(observer);
        if (log.isTraceEnabled()) {
            log.trace("Endpoint " + this.endpointId.toString() + " registered MgcpMessageObserver@"
                    + observer.hashCode() + ". Count: " + this.messageObservers.size());
        }
    }

    @Override
    public void forget(MgcpMessageObserver observer) {
        this.messageObservers.remove(observer);
        if (log.isTraceEnabled()) {
            log.trace("Endpoint " + this.endpointId.toString() + " unregistered MgcpMessageObserver@"
                    + observer.hashCode() + ". Count: " + this.messageObservers.size());
        }
    }

    @Override
    public void notify(Object originator, InetSocketAddress from, InetSocketAddress to, MgcpMessage message,
            MessageDirection direction) {
        Iterator<MgcpMessageObserver> iterator = this.messageObservers.iterator();
        while (iterator.hasNext()) {
            MgcpMessageObserver observer = (MgcpMessageObserver) iterator.next();
            if (observer != originator) {
                observer.onMessage(from, to, message, direction);
            }
        }
    }

    @Override
    public void onEvent(Object originator, MgcpEvent event) {
        MgcpRequest request = null;

        // Process event (if eligible)
        if (originator instanceof MgcpSignal) {
            request = onEndpointEvent((MgcpSignal) originator, event);
        } else if (originator instanceof MgcpConnection) {
            request = onConnectionEvent((MgcpConnection) originator, event);
        }

        if (request != null) {
            // Send notification to call agent
            // TODO hard-coded port in FROM field
            InetSocketAddress from = new InetSocketAddress(this.endpointId.getDomainName(), 2427);
            InetSocketAddress to = new InetSocketAddress(this.notifiedEntity.getDomain(),
                    this.notifiedEntity.getPort());
            notify(this, from, to, request, MessageDirection.OUTGOING);
        }
    }

    private MgcpRequest onEndpointEvent(MgcpSignal signal, MgcpEvent event) {
        // Verify if endpoint is listening for such event
        final String composedName = event.getPackage() + "/" + event.getSymbol();
        if (isListening(composedName)) {
            // Unregister from current event
            this.signals.remove(signal.getName());

            // Build Notification
            MgcpRequest notify = new MgcpRequest();
            notify.setRequestType(MgcpRequestType.NTFY);
            notify.setTransactionId(0);
            notify.setEndpointId(this.endpointId.toString());

            NotifiedEntity entity = signal.getNotifiedEntity();
            if (entity != null) {
                notify.addParameter(MgcpParameterType.NOTIFIED_ENTITY, this.notifiedEntity.toString());
            }
            notify.addParameter(MgcpParameterType.OBSERVED_EVENT, event.toString());
            notify.addParameter(MgcpParameterType.REQUEST_ID, Integer.toString(signal.getRequestId(), 16));
            return notify;
        }
        return null;
    }

    private MgcpRequest onConnectionEvent(MgcpConnection connection, MgcpEvent event) {
        if (log.isDebugEnabled()) {
            log.debug(this.endpointId + " received MGCP event " + event.toString() + " from connection "
                    + connection.getHexIdentifier());
        }

        // Verify if endpoint is listening for such event
        final String composedName = event.getPackage() + "/" + event.getSymbol();
        final int connectionId = connection.getIdentifier();

        MgcpRequestedEvent requestedEvent = isListening(connectionId, composedName);
        if (requestedEvent != null) {
            boolean removed = this.requestedConnectionEvents.remove(connectionId, requestedEvent);
            if (removed) {
                // Build Notification
                MgcpRequest notify = new MgcpRequest();
                notify.setRequestType(MgcpRequestType.NTFY);
                notify.setTransactionId(0);
                notify.setEndpointId(this.endpointId.toString());

                notify.addParameter(MgcpParameterType.OBSERVED_EVENT, event.toString());
                notify.addParameter(MgcpParameterType.REQUEST_ID, String.valueOf(requestedEvent.getRequestId()));
                return notify;
            }
        } else {
            // Special case: Connection timeout after allowed lifetime.
            if (event instanceof RtpTimeoutEvent) {
                try {
                    deleteConnection(connection.getCallIdentifier(), connectionId);
                } catch (MgcpConnectionNotFoundException | MgcpCallNotFoundException e) {
                    log.warn("Could not delete timed out connection " + connection.getHexIdentifier(), e);
                }
            }
        }
        return null;
    }

    @Override
    public void observe(MgcpEndpointObserver observer) {
        this.endpointObservers.add(observer);
        if (log.isTraceEnabled()) {
            log.trace("Registered MgcpEndpointObserver@" + observer.hashCode() + ". Count: "
                    + this.endpointObservers.size());
        }
    }

    @Override
    public void forget(MgcpEndpointObserver observer) {
        this.endpointObservers.remove(observer);
        if (log.isTraceEnabled()) {
            log.trace("Unregistered MgcpEndpointObserver@" + observer.hashCode() + ". Count: "
                    + this.endpointObservers.size());
        }
    }

    @Override
    public void notify(MgcpEndpoint endpoint, MgcpEndpointState state) {
        Iterator<MgcpEndpointObserver> iterator = this.endpointObservers.iterator();
        while (iterator.hasNext()) {
            MgcpEndpointObserver observer = iterator.next();
            observer.onEndpointStateChanged(this, state);
        }
    }

    private boolean isListening(String event) {
        for (MgcpRequestedEvent evt : this.requestedEndpointEvents) {
            if (evt.getQualifiedName().equalsIgnoreCase(event)) {
                return true;
            }
        }
        return false;
    }

    private MgcpRequestedEvent isListening(int connectionId, String event) {
        Collection<MgcpRequestedEvent> events = this.requestedConnectionEvents.get(connectionId);
        for (MgcpRequestedEvent evt : events) {
            if (evt.getQualifiedName().equalsIgnoreCase(event)) {
                return evt;
            }
        }
        return null;
    }

}