org.mobicents.media.control.mgcp.endpoint.GenericMgcpEndpoint Maven / Gradle / Ivy
/*
* 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.mobicents.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.mobicents.media.control.mgcp.command.NotificationRequest;
import org.mobicents.media.control.mgcp.command.param.NotifiedEntity;
import org.mobicents.media.control.mgcp.connection.MgcpConnection;
import org.mobicents.media.control.mgcp.connection.MgcpConnectionProvider;
import org.mobicents.media.control.mgcp.connection.MgcpConnectionState;
import org.mobicents.media.control.mgcp.exception.MgcpCallNotFoundException;
import org.mobicents.media.control.mgcp.exception.MgcpConnectionException;
import org.mobicents.media.control.mgcp.exception.MgcpConnectionNotFoundException;
import org.mobicents.media.control.mgcp.exception.UnsupportedMgcpEventException;
import org.mobicents.media.control.mgcp.message.MessageDirection;
import org.mobicents.media.control.mgcp.message.MgcpMessage;
import org.mobicents.media.control.mgcp.message.MgcpMessageObserver;
import org.mobicents.media.control.mgcp.message.MgcpParameterType;
import org.mobicents.media.control.mgcp.message.MgcpRequest;
import org.mobicents.media.control.mgcp.message.MgcpRequestType;
import org.mobicents.media.control.mgcp.pkg.MgcpEvent;
import org.mobicents.media.control.mgcp.pkg.MgcpRequestedEvent;
import org.mobicents.media.control.mgcp.pkg.MgcpSignal;
import org.mobicents.media.control.mgcp.pkg.SignalType;
import org.mobicents.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 ([email protected])
*
*/
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 connections;
// Endpoint State
private final AtomicBoolean active;
// Events and Signals
private NotifiedEntity notifiedEntity;
private ConcurrentHashMap signals;
// TODO requestedEndpointEvents needs to be synchronized!
private MgcpRequestedEvent[] requestedEndpointEvents;
private final Multimap requestedConnectionEvents;
// Observers
private final Set endpointObservers;
private final Set 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.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 deleteConnections(int callId) throws MgcpCallNotFoundException {
// Fetch all current connections
Collection current = this.connections.values();
List 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 deleteConnections() {
Set keys = this.connections.keySet();
List 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 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 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 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 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 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.
* To be overridden by subclasses.
*
* @param connection
*/
protected void onConnectionCreated(MgcpConnection connection) {
}
/**
* Event that is called when a new connection is deleted in the endpoint.
* To be overriden by subclasses.
*
* @param connection
*/
protected void onConnectionDeleted(MgcpConnection connection) {
}
/**
* Event that is called when endpoint becomes active.
* To be overriden by subclasses.
*
* @param connection
*/
protected void onActivated() {
}
/**
* Event that is called when endpoint becomes inactive.
* To be overriden by subclasses.
*
* @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 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 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 events = this.requestedConnectionEvents.get(connectionId);
for (MgcpRequestedEvent evt : events) {
if (evt.getQualifiedName().equalsIgnoreCase(event)) {
return evt;
}
}
return null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy