/*
* Copyright 2010 david varnes.
*
* 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 org.freeswitch.esl.client.internal;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.freeswitch.esl.client.transport.event.EslEvent;
import org.freeswitch.esl.client.transport.message.EslMessage;
import org.freeswitch.esl.client.transport.message.EslHeaders.Name;
import org.freeswitch.esl.client.transport.message.EslHeaders.Value;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelUpstreamHandler;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.handler.execution.ExecutionHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Specialised {@link ChannelUpstreamHandler} that implements the logic of an ESL connection that
* is common to both inbound and outbound clients. This
* handler expects to receive decoded {@link EslMessage} or {@link EslEvent} objects. The key
* responsibilities for this class are:
* <ul><li>
* To synthesise a synchronous command/response api. All IO operations using the underlying Netty
* library are intrinsically asynchronous which provides for excellent response and scalability. This
* class provides for a blocking wait mechanism for responses to commands issued to the server. A
* key assumption here is that the FreeSWITCH server will process synchronous requests in the order they
* are received.
* </li><li>
* Concrete sub classes are expected to 'terminate' the Netty IO processing pipeline (ie be the 'last'
* handler).
* </li></ul>
* Note: implementation requirement is that an {@link ExecutionHandler} is placed in the processing
* pipeline prior to this handler. This will ensure that each incoming message is processed in its
* own thread (although still guaranteed to be processed in the order of receipt).
*
* @author david varnes
*/
public abstract class AbstractEslClientHandler extends SimpleChannelUpstreamHandler
{
public static final String MESSAGE_TERMINATOR = "\n\n";
public static final String LINE_TERMINATOR = "\n";
protected final Logger log = LoggerFactory.getLogger( this.getClass() );
private final Lock syncLock = new ReentrantLock();
private final Queue<SyncCallback> syncCallbacks = new ConcurrentLinkedQueue<SyncCallback>();
@Override
public void messageReceived( ChannelHandlerContext ctx, MessageEvent e ) throws Exception
{
if ( e.getMessage() instanceof EslMessage )
{
EslMessage message = (EslMessage)e.getMessage();
String contentType = message.getContentType();
if ( contentType.equals( Value.TEXT_EVENT_PLAIN ) ||
contentType.equals( Value.TEXT_EVENT_XML ) )
{
// transform into an event
EslEvent eslEvent = new EslEvent( message );
handleEslEvent( ctx, eslEvent );
}
else
{
handleEslMessage( ctx, (EslMessage)e.getMessage() );
}
}
else
{
throw new IllegalStateException( "Unexpected message type: " + e.getMessage().getClass() );
}
}
/**
* Synthesise a synchronous command/response by creating a callback object which is placed in
* queue and blocks waiting for another IO thread to process an incoming {@link EslMessage} and
* attach it to the callback.
*
* @param channel
* @param command single string to send
* @return the {@link EslMessage} attached to this command's callback
*/
public EslMessage sendSyncSingleLineCommand( Channel channel, final String command )
{
SyncCallback callback = new SyncCallback();
syncLock.lock();
try
{
syncCallbacks.add( callback );
channel.write( command + MESSAGE_TERMINATOR );
}
finally
{
syncLock.unlock();
}
// Block until the response is available
return callback.get();
}
/**
* Synthesise a synchronous command/response by creating a callback object which is placed in
* queue and blocks waiting for another IO thread to process an incoming {@link EslMessage} and
* attach it to the callback.
*
* @param channel
* @param command List of command lines to send
* @return the {@link EslMessage} attached to this command's callback
*/
public EslMessage sendSyncMultiLineCommand( Channel channel, final List<String> commandLines )
{
SyncCallback callback = new SyncCallback();
// Build command with double line terminator at the end
StringBuilder sb = new StringBuilder();
for ( String line : commandLines )
{
sb.append( line );
sb.append( LINE_TERMINATOR );
}
sb.append( LINE_TERMINATOR );
syncLock.lock();
try
{
syncCallbacks.add( callback );
channel.write( sb.toString() );
}
finally
{
syncLock.unlock();
}
// Block until the response is available
return callback.get();
}
/**
* Returns the Job UUID of that the response event will have.
*
* @param channel
* @param command
* @return Job-UUID as a string
*/
public String sendAsyncCommand( Channel channel, final String command )
{
/*
* Send synchronously to get the Job-UUID to return, the results of the actual
* job request will be returned by the server as an async event.
*/
EslMessage response = sendSyncSingleLineCommand( channel, command );
if ( response.hasHeader( Name.JOB_UUID ) )
{
return response.getHeaderValue( Name.JOB_UUID );
}
else
{
throw new IllegalStateException( "Missing Job-UUID header in bgapi response" );
}
}
protected void handleEslMessage( ChannelHandlerContext ctx, EslMessage message )
{
log.info( "Received message: [{}]", message );
String contentType = message.getContentType();
if ( contentType.equals( Value.API_RESPONSE ) )
{
log.debug( "Api response received [{}]", message );
syncCallbacks.poll().handle( message );
}
else if ( contentType.equals( Value.COMMAND_REPLY ) )
{
log.debug( "Command reply received [{}]", message );
syncCallbacks.poll().handle( message );
}
else if ( contentType.equals( Value.AUTH_REQUEST ) )
{
log.debug( "Auth request received [{}]", message );
handleAuthRequest( ctx );
}
else if ( contentType.equals( Value.TEXT_DISCONNECT_NOTICE ) )
{
log.debug( "Disconnect notice received [{}]", message );
handleDisconnectionNotice();
}
else
{
log.warn( "Unexpected message content type [{}]", contentType );
}
}
protected abstract void handleEslEvent( ChannelHandlerContext ctx, EslEvent event );
protected abstract void handleAuthRequest( ChannelHandlerContext ctx );
protected abstract void handleDisconnectionNotice();
private static class SyncCallback
{
private static final Logger log = LoggerFactory.getLogger( SyncCallback.class );
private final CountDownLatch latch = new CountDownLatch( 1 );
private EslMessage response;
/**
* Block waiting for the countdown latch to be released, then return the
* associated response object.
* @return
*/
EslMessage get()
{
try
{
log.trace( "awaiting latch ... " );
latch.await();
}
catch ( InterruptedException e )
{
throw new RuntimeException( e );
}
log.trace( "returning response [{}]", response );
return response;
}
/**
* Attach this response to the callback and release the countdown latch.
* @param response
*/
void handle( EslMessage response )
{
this.response = response;
log.trace( "releasing latch for response [{}]", response );
latch.countDown();
}
}
}