提交 aaa97237 authored 作者: David Varnes's avatar David Varnes

Initial commit - java ESL inbound/outbound client implementation

git-svn-id: http://svn.freeswitch.org/svn/freeswitch/trunk/contrib@16127 d0543943-73ff-0310-b7d9-9358b9ac24b2
上级 9430f1db
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.freeswitch.esl</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.maven.ide.eclipse.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.maven.ide.eclipse.maven2Nature</nature>
</natures>
</projectDescription>
#Sat Dec 12 17:33:34 EST 2009
activeProfiles=
eclipse.preferences.version=1
fullBuildGoals=process-test-resources
includeModules=false
resolveWorkspaceProjects=true
resourceFilterGoals=process-resources resources\:testResources
skipCompilerPlugin=true
version=1
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources"/>
<classpathentry kind="src" output="target/test-classes" path="src/test/java"/>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/>
<classpathentry kind="con" path="org.maven.ide.eclipse.MAVEN2_CLASSPATH_CONTAINER"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.freeswitch.esl.client.example</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.maven.ide.eclipse.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.maven.ide.eclipse.maven2Nature</nature>
</natures>
</projectDescription>
<?xml version="1.0" encoding="UTF-8"?>
<!--
* 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.freeswitch.esl.client</groupId>
<artifactId>esl-client-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>org.freeswitch.esl.client.example</artifactId>
<name>FreeSWITCH Event Socket Library - Java Client example usage</name>
<dependencies>
<dependency>
<groupId>org.freeswitch.esl.client</groupId>
<artifactId>org.freeswitch.esl.client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources"/>
<classpathentry kind="src" output="target/test-classes" path="src/test/java"/>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/>
<classpathentry kind="con" path="org.maven.ide.eclipse.MAVEN2_CLASSPATH_CONTAINER"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.freeswitch.esl.client.netty</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.maven.ide.eclipse.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.maven.ide.eclipse.maven2Nature</nature>
</natures>
</projectDescription>
#Sun Jan 03 23:33:57 EST 2010
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.5
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.source=1.5
#Sat Dec 12 17:44:54 EST 2009
activeProfiles=
eclipse.preferences.version=1
fullBuildGoals=process-test-resources
includeModules=false
resolveWorkspaceProjects=true
resourceFilterGoals=process-resources resources\:testResources
skipCompilerPlugin=true
version=1
As at 2010.01.03
Features still to be implemented
* More testing of the outbound socket client
* Send event command
* Send message command for inbound client
* Refactor the api of the inbound client into the abstract handler so it is also available in outbound handlers
* Per event(s) listener
* Provide XML event handling - not sure if this is useful since the raw event is not exposed, although it could be if needed.
* 'myevent' event subscription - not a priority here, easy if required.
* Provide timeout protection on the client.sendSyncApiCommand(). It will currently block for ever.
* Working examples in an example project (started).
Quality items
* Ask FS dev team for any ESL event generation test strategies/suites.
* Are all message header names in the enum ?
* Improve Javadoc coverage
* Add cross references to the FS wiki in the Javadocs .. eg list of api commands and events.
* Add unit test coverage
* Investigate availability of FS JIRA for issue tracking
<?xml version="1.0" encoding="UTF-8"?>
<!--
* 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.freeswitch.esl.client</groupId>
<artifactId>esl-client-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>org.freeswitch.esl.client</artifactId>
<name>FreeSWITCH Event Socket Library - Java Client</name>
<packaging>bundle</packaging>
<build>
<plugins>
<!-- this plugin will create the target jar as a correct OSGi bundle -->
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>2.0.1</version>
<extensions>true</extensions>
<configuration>
<unpackBundle>true</unpackBundle>
<instructions>
<Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
<Bundle-Version>${project.version}</Bundle-Version>
<Bundle-DocURL>http://www.freeswitch.org</Bundle-DocURL>
<Import-Package>*</Import-Package>
<Export-Package>
org.freeswitch.esl.client,
org.freeswitch.esl.client.transport.event,
org.freeswitch.esl.client.transport.message,
</Export-Package>
<!-- the debug package is a temporary hack -->
<Private-Package>
org.freeswitch.esl.client.internal,
org.freeswitch.esl.client.debug
</Private-Package>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.5.10</version>
</dependency>
<dependency>
<groupId>org.jboss.netty</groupId>
<artifactId>netty</artifactId>
<version>3.1.5.GA</version>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>0.9.18</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
\ No newline at end of file
/*
* 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;
import org.freeswitch.esl.client.transport.event.EslEvent;
/**
* Interface for observers wanting to be notified of incoming FreeSWITCH Event Socket events.
* </p>
* Incoming events arrive asynchronously and are processed into two queues, one for server
* initiated events, and one for the results of client requested background jobs.
* </p>
* Each queue is serviced by a different thread pool (to ensure lowest latency for event-driven events)
* and each queue is guaranteed to be processed (and listeners notified) in the order in which the
* events are received off the wire.
* </p>
* This design ensures that incoming event processing is not blocked by any long-running listener process.
* However multiple listeners will be notified sequentially, and so one slow listener can cause latency
* to other listeners.
*
* @author david varnes
* @version $Id$
*/
public interface IEslEventListener
{
/**
* Signal of a server initiated event.
*
* @param event as an {@link EslEvent}
*/
void eventReceived( EslEvent event );
/**
* Signal of an event containing the result of a client requested background job. The Job-UUID will
* be available as an event header of that name.
*
* @param event as an {@link EslEvent}
*/
void backgroundJobResultReceived( EslEvent event );
}
/*
* 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.inbound;
import org.freeswitch.esl.client.internal.AbstractEslClientHandler;
import org.freeswitch.esl.client.internal.IEslProtocolListener;
import org.freeswitch.esl.client.transport.CommandResponse;
import org.freeswitch.esl.client.transport.event.EslEvent;
import org.freeswitch.esl.client.transport.message.EslMessage;
import org.freeswitch.esl.client.transport.message.EslHeaders.Value;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.handler.execution.ExecutionHandler;
/**
* End users of the inbound {@link Client} should not need to use this class.
* </p>
* Specialised {@link AbstractEslClientHandler} that implements the connection logic for an
* 'Inbound' FreeSWITCH Event Socket connection. The responsibilities for this class are:
* </p>
* * To handle the auth request that the FreeSWITCH server will send immediately following a new
* connection when mode is Inbound.
* </p>
* * To signal the observing {@link IEslProtocolListener} (expected to be the Inbound client
* implementation when ESL events are received.
* </p>
* 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
* @version $Id$
*/
@ChannelPipelineCoverage( "one" )
public class InboundClientHandler extends AbstractEslClientHandler
{
private final String password;
private final IEslProtocolListener listener;
public InboundClientHandler( String password, IEslProtocolListener listener )
{
this.password = password;
this.listener = listener;
}
protected void handleEslEvent( ChannelHandlerContext ctx, EslEvent event )
{
log.debug( "Received event: [{}]", event );
listener.eventReceived( event );
}
protected void handleAuthRequest( ChannelHandlerContext ctx )
{
log.debug( "Auth requested, sending [auth {}]", "*****" );
EslMessage response = sendSyncSingleLineCommand( ctx.getChannel(), "auth " + password );
log.debug( "Auth response [{}]", response );
if ( response.getContentType().equals( Value.COMMAND_REPLY ) )
{
CommandResponse commandResponse = new CommandResponse( "auth " + password, response );
listener.authResponseReceived( commandResponse );
}
else
{
log.error( "Bad auth response message [{}]", response );
throw new IllegalStateException( "Incorrect auth response" );
}
}
@Override
protected void handleDisconnectionNotice()
{
log.debug( "Received disconnection notice" );
listener.disconnected();
}
}
/*
* 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.inbound;
/**
* Checked exception to handle connection failures.
*
* @author david varnes
* @version $Id$
*/
public class InboundConnectionFailure extends Exception
{
private static final long serialVersionUID = 1L;
public InboundConnectionFailure( String message )
{
super( message );
}
public InboundConnectionFailure( String message, Throwable cause )
{
super( message, cause );
}
}
/*
* 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.inbound;
import org.freeswitch.esl.client.internal.debug.ExecutionHandler;
import org.freeswitch.esl.client.transport.message.EslMessageDecoder;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelHandler;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;
import org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor;
/**
* End users of the {@link Client} should not need to use this class.
* </p>
* Convenience factory to assemble a Netty processing pipeline for inbound clients.
*
* @author david varnes
* @version $Id$
*/
public class InboundPipelineFactory implements ChannelPipelineFactory
{
private final ChannelHandler handler;
private final ChannelBuffer[] delimiters =
new ChannelBuffer[] { ChannelBuffers.wrappedBuffer( new byte[] { '\n' } ),
ChannelBuffers.wrappedBuffer( new byte[] { '\n', '\n' } ) };
public InboundPipelineFactory( ChannelHandler handler )
{
this.handler = handler;
}
public ChannelPipeline getPipeline() throws Exception
{
ChannelPipeline pipeline = Channels.pipeline();
// Add the text line codec combination first
pipeline.addLast( "framer", new DelimiterBasedFrameDecoder( 8192, delimiters ) );
pipeline.addLast( "stringDecoder", new StringDecoder() );
pipeline.addLast( "encoder", new StringEncoder() );
pipeline.addLast( "eslMessageDecoder", new EslMessageDecoder() );
// Add an executor to ensure separate thread for each upstream message from here
pipeline.addLast( "executor", new ExecutionHandler(
new OrderedMemoryAwareThreadPoolExecutor( 16, 1048576, 1048576 ) ) );
// now the inbound client logic
pipeline.addLast( "clientHandler", handler );
return pipeline;
}
}
/*
* 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:
* </p>
* * 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.
* </p>
* * Concrete sub classes are expected to 'terminate' the Netty IO processing pipeline (ie be the 'last'
* handler).
* </p>
* 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
* @version $Id$
*/
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();
}
}
}
/*
* 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;
/**
* This parser provides a static helper method to split a standard Header field
* into the name and value parts.
* <p/>
* This code was copied from the splitHeader() method in the
* <code>org.jboss.netty.handler.codec.http.HttpMessageDecoder</code> class, which
* is licensed under the Apache License version 2. Original author: Trustin Lee.
*
* @author Trustin Lee
* @author david varnes
*/
public class HeaderParser
{
/**
* Split a header in the form
* <pre>
* Header-Name: Some_header-value
* </pre>
* into a String array.
*
* @param sb the string header to parse
* @return a String[] array with header name at 0 and header value at 1
*/
public static String[] splitHeader(String sb) {
final int length = sb.length();
int nameStart;
int nameEnd;
int colonEnd;
int valueStart;
int valueEnd;
nameStart = findNonWhitespace(sb, 0);
for (nameEnd = nameStart; nameEnd < length; nameEnd ++) {
char ch = sb.charAt(nameEnd);
if (ch == ':' || Character.isWhitespace(ch)) {
break;
}
}
for (colonEnd = nameEnd; colonEnd < length; colonEnd ++) {
if (sb.charAt(colonEnd) == ':') {
colonEnd ++;
break;
}
}
valueStart = findNonWhitespace(sb, colonEnd);
if (valueStart == length) {
return new String[] {
sb.substring(nameStart, nameEnd),
""
};
}
valueEnd = findEndOfString(sb);
return new String[] {
sb.substring(nameStart, nameEnd),
sb.substring(valueStart, valueEnd)
};
}
private static int findNonWhitespace(String sb, int offset) {
int result;
for (result = offset; result < sb.length(); result ++) {
if (!Character.isWhitespace(sb.charAt(result))) {
break;
}
}
return result;
}
private static int findEndOfString(String sb) {
int result;
for (result = sb.length(); result > 0; result --) {
if (!Character.isWhitespace(sb.charAt(result - 1))) {
break;
}
}
return result;
}
}
/*
* 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 org.freeswitch.esl.client.inbound.Client;
import org.freeswitch.esl.client.transport.CommandResponse;
import org.freeswitch.esl.client.transport.event.EslEvent;
/**
* End users of the {@link Client} should not need to use this class.
* </p>
* Allow client implementations to observe events arriving from the server.
*
* @author david varnes
* @version $Id$
*/
public interface IEslProtocolListener
{
void authResponseReceived( CommandResponse response );
void eventReceived( EslEvent event );
void disconnected();
}
package org.freeswitch.esl.client.internal.debug;
/*
* Copyright 2009 Red Hat, Inc.
*
* Red Hat 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.
*/
import java.util.concurrent.Executor;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.util.EstimatableObjectWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* a {@link Runnable} which sends the specified {@link ChannelEvent} upstream.
* Most users will not see this type at all because it is used by
* {@link Executor} implementors only
*
* @author The Netty Project (netty-dev@lists.jboss.org)
* @author Trustin Lee (tlee@redhat.com)
*
* @version $Rev: 1685 $, $Date: 2009-08-28 16:15:49 +0900 (금, 28 8 2009) $
*
*/
public class ChannelEventRunnable implements Runnable, EstimatableObjectWrapper {
private final Logger log = LoggerFactory.getLogger( this.getClass() );
private final ChannelHandlerContext ctx;
private final ChannelEvent e;
volatile int estimatedSize;
/**
* Creates a {@link Runnable} which sends the specified {@link ChannelEvent}
* upstream via the specified {@link ChannelHandlerContext}.
*/
public ChannelEventRunnable(ChannelHandlerContext ctx, ChannelEvent e) {
this.ctx = ctx;
this.e = e;
}
/**
* Returns the {@link ChannelHandlerContext} which will be used to
* send the {@link ChannelEvent} upstream.
*/
public ChannelHandlerContext getContext() {
return ctx;
}
/**
* Returns the {@link ChannelEvent} which will be sent upstream.
*/
public ChannelEvent getEvent() {
return e;
}
/**
* Sends the event upstream.
*/
public void run() {
// log.info( "Sending [{}] upstream in [{}]", e, ctx );
try
{
ctx.sendUpstream(e);
}
catch ( Throwable t )
{
log.error( "Caught -->", t );
}
}
public Object unwrap() {
return e;
}
}
package org.freeswitch.esl.client.internal.debug;
/*
* Copyright 2009 Red Hat, Inc.
*
* Red Hat 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.
*/
import java.util.concurrent.Executor;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelDownstreamHandler;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.channel.ChannelState;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ChannelUpstreamHandler;
import org.jboss.netty.util.ExternalResourceReleasable;
import org.jboss.netty.util.internal.ExecutorUtil;
/**
* Forwards an upstream {@link ChannelEvent} to an {@link Executor}.
* <p>
* You can implement various thread model by adding this handler to a
* {@link ChannelPipeline}. The most common use case of this handler is to
* add a {@link ExecutionHandler} which was specified with
* {@link OrderedMemoryAwareThreadPoolExecutor}:
* <pre>
* ChannelPipeline pipeline = ...;
* pipeline.addLast("decoder", new MyProtocolDecoder());
* pipeline.addLast("encoder", new MyProtocolEncoder());
*
* // HERE
* <strong>pipeline.addLast("executor", new {@link ExecutionHandler}(new {@link OrderedMemoryAwareThreadPoolExecutor}(16, 1048576, 1048576)));</strong>
*
* pipeline.addLast("handler", new MyBusinessLogicHandler());
* </pre>
* to utilize more processors to handle {@link ChannelEvent}s. You can also
* use other {@link Executor} implementation than the recommended
* {@link OrderedMemoryAwareThreadPoolExecutor}.
*
* @author The Netty Project (netty-dev@lists.jboss.org)
* @author Trustin Lee (tlee@redhat.com)
*
* @version $Rev: 1685 $, $Date: 2009-08-28 16:15:49 +0900 (금, 28 8 2009) $
*
* @apiviz.landmark
* @apiviz.has java.util.concurrent.ThreadPoolExecutor
*/
@ChannelPipelineCoverage("all")
public class ExecutionHandler implements ChannelUpstreamHandler, ChannelDownstreamHandler, ExternalResourceReleasable {
private final Executor executor;
/**
* Creates a new instance with the specified {@link Executor}.
* Specify an {@link OrderedMemoryAwareThreadPoolExecutor} if unsure.
*/
public ExecutionHandler(Executor executor) {
if (executor == null) {
throw new NullPointerException("executor");
}
this.executor = executor;
}
/**
* Returns the {@link Executor} which was specified with the constructor.
*/
public Executor getExecutor() {
return executor;
}
/**
* Shuts down the {@link Executor} which was specified with the constructor
* and wait for its termination.
*/
public void releaseExternalResources() {
ExecutorUtil.terminate(getExecutor());
}
public void handleUpstream(
ChannelHandlerContext context, ChannelEvent e) throws Exception {
executor.execute(new ChannelEventRunnable(context, e));
}
public void handleDownstream(
ChannelHandlerContext ctx, ChannelEvent e) throws Exception {
if (e instanceof ChannelStateEvent) {
ChannelStateEvent cse = (ChannelStateEvent) e;
if (cse.getState() == ChannelState.INTEREST_OPS &&
(((Integer) cse.getValue()).intValue() & Channel.OP_READ) != 0) {
// setReadable(true) requested
boolean readSuspended = ctx.getAttachment() != null;
if (readSuspended) {
// Drop the request silently if MemoryAwareThreadPool has
// set the flag.
e.getFuture().setSuccess();
return;
}
}
}
ctx.sendDownstream(e);
}
}
/*
* 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.outbound;
import org.freeswitch.esl.client.internal.AbstractEslClientHandler;
import org.freeswitch.esl.client.transport.event.EslEvent;
import org.freeswitch.esl.client.transport.message.EslMessage;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.handler.execution.ExecutionHandler;
/**
* Specialised {@link AbstractEslClientHandler} that implements the base connecction logic for an
* 'Outbound' FreeSWITCH Event Socket connection. The responsibilities for this class are:
* </p>
* * To send a 'connect' command when the FreeSWITCH server first establishes a new connection with
* the socket client in Outbound mode. This will result in an incoming {@link EslMessage} that is
* transformed into an {@link EslEvent} that sub classes can handle.
* </p>
* 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
* @version $Id$
*/
@ChannelPipelineCoverage( "one" )
public abstract class AbstractOutboundClientHandler extends AbstractEslClientHandler
{
@Override
public void channelConnected( ChannelHandlerContext ctx, ChannelStateEvent e ) throws Exception
{
// Have received a connection from FreeSWITCH server, send connect response
log.debug( "Received new connection from server, sending connect message" );
EslMessage response = sendSyncSingleLineCommand( ctx.getChannel(), "connect" );
// A hack in the message decoder treats most of this incoming message as an event so parse now
EslEvent channelDataEvent = new EslEvent( response, true );
// Let implementing sub classes choose what to do next
handleConnectResponse( ctx, channelDataEvent );
}
protected abstract void handleConnectResponse( ChannelHandlerContext ctx, EslEvent event );
@Override
protected void handleAuthRequest( ChannelHandlerContext ctx )
{
// This should not happen in outbound mode
log.warn( "Auth request received in outbound mode, ignoring" );
}
@Override
protected void handleDisconnectionNotice()
{
log.debug( "Received disconnection notice" );
}
}
/*
* 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.outbound;
import org.freeswitch.esl.client.internal.debug.ExecutionHandler;
import org.freeswitch.esl.client.transport.message.EslMessageDecoder;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;
import org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor;
/**
* </p>
* An abstract factory to assemble a Netty processing pipeline for outbound clients.
*
* @author david varnes
* @version $Id$
*/
public abstract class AbstractOutboundPipelineFactory implements ChannelPipelineFactory
{
private final ChannelBuffer[] delimiters =
new ChannelBuffer[] { ChannelBuffers.wrappedBuffer( new byte[] { '\n' } ),
ChannelBuffers.wrappedBuffer( new byte[] { '\n', '\n' } ) };
public ChannelPipeline getPipeline() throws Exception
{
ChannelPipeline pipeline = Channels.pipeline();
// Add the text line codec combination first
pipeline.addLast( "framer", new DelimiterBasedFrameDecoder( 8192, delimiters ) );
pipeline.addLast( "stringDecoder", new StringDecoder() );
pipeline.addLast( "encoder", new StringEncoder() );
pipeline.addLast( "eslMessageDecoder", new EslMessageDecoder( true ) );
// Add an executor to ensure separate thread for each upstream message from here
pipeline.addLast( "executor", new ExecutionHandler(
new OrderedMemoryAwareThreadPoolExecutor( 16, 1048576, 1048576 ) ) );
// now the outbound client logic
pipeline.addLast( "clientHandler", makeHandler() );
return pipeline;
}
protected abstract AbstractOutboundClientHandler makeHandler();
}
/*
* 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.outbound;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.channel.group.ChannelGroupFuture;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Entry point to run a socket client that a running FreeSWITCH Event Socket Library module can
* make outbound connections to.
* </p>
* This class provides for what the FreeSWITCH documentation refers to as 'Outbound' connections
* from the Event Socket module. That is, with reference to the module running on the FreeSWITCH
* server, this client accepts an outbound connection from the server module.
* </p>
* See http://wiki.freeswitch.org/wiki/Mod_event_socket
*
* @author david varnes
* @version $Id$
*/
public class SocketClient
{
private final Logger log = LoggerFactory.getLogger( this.getClass() );
private final ChannelGroup allChannels = new DefaultChannelGroup( "esl-socket-client" );
private final int port;
private final ChannelFactory channelFactory;
private final AbstractOutboundPipelineFactory pipelineFactory;
public SocketClient( int port, AbstractOutboundPipelineFactory pipelineFactory )
{
this.port = port;
this.pipelineFactory = pipelineFactory;
this.channelFactory = new NioServerSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool() );
}
public void start()
{
ServerBootstrap bootstrap = new ServerBootstrap( channelFactory );
bootstrap.setPipelineFactory( pipelineFactory );
bootstrap.setOption( "child.tcpNoDelay", true );
bootstrap.setOption( "child.keepAlive", true );
Channel serverChannel = bootstrap.bind( new InetSocketAddress( port ) );
allChannels.add( serverChannel );
log.info( "SocketClient waiting for connections on port [{}] ...", port );
}
public void stop()
{
ChannelGroupFuture future = allChannels.close();
future.awaitUninterruptibly();
channelFactory.releaseExternalResources();
log.info( "SocketClient stopped" );
}
}
/*
* 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.outbound.example;
import org.freeswitch.esl.client.outbound.AbstractOutboundClientHandler;
import org.freeswitch.esl.client.transport.SendMsg;
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.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
/**
* Simple example of a handler for outbound connection from FreeSWITCH server.
* This class will log some of the FreeSWTICH call channel variables and
* then hangup the call.
*
* @author david varnes
* @version $Id$
*/
public class SimpleHangupOutboundHandler extends AbstractOutboundClientHandler
{
@Override
protected void handleConnectResponse( ChannelHandlerContext ctx, EslEvent event )
{
log.info( "Received connect response [{}]", event );
if ( event.getEventName().equalsIgnoreCase( "CHANNEL_DATA" ) )
{
// this is the response to the initial connect
log.info( "======================= incoming channel data =============================" );
log.info( "Event-Date-Local: [{}]", event.getEventDateLocal() );
log.info( "Unique-ID: [{}]", event.getEventHeaders().get( "Unique-ID" ) );
log.info( "Channel-ANI: [{}]", event.getEventHeaders().get( "Channel-ANI" ) );
log.info( "Answer-State: [{}]", event.getEventHeaders().get( "Answer-State" ) );
log.info( "Caller-Destination-Number: [{}]", event.getEventHeaders().get( "Caller-Destination-Number" ) );
log.info( "======================= = = = = = = = = = = = =============================" );
// now hangup the call
hangupCall( ctx.getChannel() );
}
else
{
throw new IllegalStateException( "Unexpected event after connect: [" + event.getEventName() + ']' );
}
}
@Override
protected void handleEslEvent( ChannelHandlerContext ctx, EslEvent event )
{
log.info( "Received event [{}]", event );
}
private void hangupCall( Channel channel )
{
SendMsg hangupMsg = new SendMsg();
hangupMsg.addCallCommand( "execute" );
hangupMsg.addExecuteAppName( "hangup" );
EslMessage response = sendSyncMultiLineCommand( channel, hangupMsg.getMsgLines() );
if ( response.getHeaderValue( Name.REPLY_TEXT ).startsWith( "+OK" ) )
{
log.info( "Call hangup successful" );
}
else
{
}
}
}
/*
* 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.outbound.example;
import org.freeswitch.esl.client.outbound.AbstractOutboundClientHandler;
import org.freeswitch.esl.client.outbound.AbstractOutboundPipelineFactory;
/**
* Factory for the simple hangup handler
*
* @author david varnes
* @version $Id$
*/
public class SimpleHangupPipelineFactory extends AbstractOutboundPipelineFactory
{
@Override
protected AbstractOutboundClientHandler makeHandler()
{
return new SimpleHangupOutboundHandler();
}
}
/*
* 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.transport;
import org.freeswitch.esl.client.transport.message.EslMessage;
import org.freeswitch.esl.client.transport.message.EslHeaders.Name;
/**
* Result object to carry the results of a command sent to the FreeSWITCH Event Socket.
*
* @author david varnes
* @version $Id$
*/
public class CommandResponse
{
private final String command;
private final String replyText;
private final EslMessage response;
private final boolean success;
public CommandResponse( String command, EslMessage response )
{
this.command = command;
this.response = response;
this.replyText = response.getHeaderValue( Name.REPLY_TEXT );
this.success = replyText.startsWith( "+OK" );
}
/**
* @return the original command sent to the server
*/
public String getCommand()
{
return command;
}
/**
* @return true if and only if the response Reply-Text line starts with "+OK"
*/
public boolean isOk()
{
return success;
}
/**
* @return the full response Reply-Text line.
*/
public String getReplyText()
{
return replyText;
}
/**
* @return {@link EslMessage} the full response from the server
*/
public EslMessage getResponse()
{
return response;
}
}
package org.freeswitch.esl.client.transport;
import java.util.ArrayList;
import java.util.List;
public class SendMsg
{
private final List<String> msgLines = new ArrayList<String>();
public SendMsg()
{
msgLines.add( "sendmsg" );
}
public SendMsg( String uuid )
{
msgLines.add( "sendmsg " + uuid );
}
public void addCallCommand( String command )
{
msgLines.add( "call-command: " + command );
}
public void addExecuteAppName( String appName )
{
msgLines.add( "execute-app-name: " + appName );
}
public void addExecuteAppArg( String arg )
{
msgLines.add( "execute-app-arg: " + arg );
}
public void addLoops( int count )
{
msgLines.add( "loops: " + count );
}
public void addHangupCause( String cause )
{
msgLines.add( "hangup-cause: " + cause );
}
public void addNomediaUuid( String info )
{
msgLines.add( "nomedia-uuid: " + info );
}
public void addEventLock()
{
msgLines.add( "event-lock: true" );
}
public void addGenericLine( String name, String value )
{
msgLines.add( name + ": " + value );
}
public List<String> getMsgLines()
{
return msgLines;
}
}
/*
* 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.transport.event;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.freeswitch.esl.client.internal.HeaderParser;
import org.freeswitch.esl.client.transport.message.EslHeaders;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FreeSWITCH Event Socket events are decoded into this data object.
* </p>
* An ESL event is modelled as a collection of text lines. An event always has several eventHeader
* lines, and optionally may have some eventBody lines. In addition the messageHeaders of the
* original containing {@link EslMessage} which carried the event are also available.
* </p>
* The eventHeader lines are parsed and cached in a map keyed by the eventHeader name string. An event
* is always expected to have an "Event-Name" eventHeader. Commonly used eventHeader names are coded
* in {@link EslEventHeaderNames}</br>
* Any eventBody lines are cached in a list.</br>
* The messageHeader lines from the original message are cached in a map keyed by {@link EslHeaders.Name}.
*
* @author david varnes
* @version $Id$
* @see EslEventHeaderNames
*/
public class EslEvent
{
private final Logger log = LoggerFactory.getLogger( this.getClass() );
private final Map<Name,String> messageHeaders;
private final Map<String,String> eventHeaders;
private final List<String> eventBody;
private boolean decodeEventHeaders = true;
public EslEvent( EslMessage rawMessage )
{
this( rawMessage, false );
}
public EslEvent( EslMessage rawMessage, boolean parseCommandReply )
{
messageHeaders = rawMessage.getHeaders();
eventHeaders = new HashMap<String,String>( rawMessage.getBodyLines().size() );
eventBody = new ArrayList<String>();
// plain or xml body
if ( rawMessage.getContentType().equals( Value.TEXT_EVENT_PLAIN ) )
{
parsePlainBody( rawMessage.getBodyLines() );
}
else if ( rawMessage.getContentType().equals( Value.TEXT_EVENT_XML ) )
{
throw new IllegalStateException( "XML events are not yet supported" );
}
else if ( rawMessage.getContentType().equals( Value.COMMAND_REPLY ) && parseCommandReply )
{
parsePlainBody( rawMessage.getBodyLines() );
}
else
{
throw new IllegalStateException( "Unexpected EVENT content-type: " +
rawMessage.getContentType() );
}
}
/**
* The message headers of the original ESL message from which this event was decoded.
* The message headers are stored in a map keyed by {@link EslHeaders.Name}. The string mapped value
* is the parsed content of the header line (ie, it does not include the header name).
*
* @return map of header values
*/
public Map<Name,String> getMessageHeaders()
{
return messageHeaders;
}
/**
* The event headers of this event. The headers are parsed and stored in a map keyed by the string
* name of the header, and the string mapped value is the parsed content of the event header line
* (ie, it does not include the header name).
*
* @return map of event header values
*/
public Map<String, String> getEventHeaders()
{
return eventHeaders;
}
/**
* Any event body lines that were present in the event.
*
* @return list of decoded event body lines, may be an empty list.
*/
public List<String> getEventBodyLines()
{
return eventBody;
}
/**
* Convenience method.
*
* @return the string value of the event header "Event-Name"
*/
public String getEventName()
{
return getEventHeaders().get( EslEventHeaderNames.EVENT_NAME );
}
/**
* Convenience method.
*
* @return long value of the event header "Event-Date-Timestamp"
*/
public long getEventDateTimestamp()
{
return Long.valueOf( getEventHeaders().get( EslEventHeaderNames.EVENT_DATE_TIMESTAMP ) );
}
/**
* Convenience method.
*
* @return long value of the event header "Event-Date-Local"
*/
public String getEventDateLocal()
{
return getEventHeaders().get( EslEventHeaderNames.EVENT_DATE_LOCAL );
}
/**
* Convenience method.
*
* @return long value of the event header "Event-Date-GMT"
*/
public String getEventDateGmt()
{
return getEventHeaders().get( EslEventHeaderNames.EVENT_DATE_GMT );
}
/**
* Convenience method.
*
* @return true if the eventBody list is not empty.
*/
public boolean hasEventBody()
{
return ! eventBody.isEmpty();
}
private void parsePlainBody( final List<String> rawBodyLines )
{
boolean isEventBody = false;
for ( String rawLine : rawBodyLines )
{
if ( ! isEventBody )
{
// split the line
String[] headerParts = HeaderParser.splitHeader( rawLine );
if ( decodeEventHeaders )
{
try
{
String decodedValue = URLDecoder.decode( headerParts[1], "UTF-8" );
log.trace( "decoded from: [{}]", headerParts[1] );
log.trace( "decoded to: [{}]", decodedValue );
eventHeaders.put( headerParts[0], decodedValue );
}
catch ( UnsupportedEncodingException e )
{
log.warn( "Could not URL decode [{}]", headerParts[1] );
eventHeaders.put( headerParts[0], headerParts[1] );
}
}
else
{
eventHeaders.put( headerParts[0], headerParts[1] );
}
if ( headerParts[0].equals( EslEventHeaderNames.CONTENT_LENGTH ) )
{
// the remaining lines will be considered body lines
isEventBody = true;
}
}
else
{
// ignore blank line (always is one following the content-length
if ( rawLine.length() > 0 )
{
eventBody.add( rawLine );
}
}
}
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder( "EslEvent: name=[" );
sb.append( getEventName() );
sb.append( "] headers=" );
sb.append( messageHeaders.size() );
sb.append( ", eventHeaders=" );
sb.append( eventHeaders.size() );
sb.append( ", eventBody=" );
sb.append( eventBody.size() );
sb.append( " lines." );
return sb.toString();
}
}
/*
* 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.transport.event;
/**
* Container class for some commonly used ESL event header names (note there are many more!).
*
* @author david varnes
* @version $Id$
*/
public class EslEventHeaderNames
{
/**
* {@code "Event-Name"}
*/
public static final String EVENT_NAME = "Event-Name";
/**
* {@code "Event-Date-Local"}
*/
public static final String EVENT_DATE_LOCAL = "Event-Date-Local";
/**
* {@code "Event-Date-GMT"}
*/
public static final String EVENT_DATE_GMT = "Event-Date-GMT";
/**
* {@code "Event-Date-Timestamp"}
*/
public static final String EVENT_DATE_TIMESTAMP = "Event-Date-Timestamp";
/**
* {@code "Event-Calling-File"}
*/
public static final String EVENT_CALLING_FILE = "Event-Calling-File";
/**
* {@code "Event-Calling-Function"}
*/
public static final String EVENT_CALLING_FUNCTION = "Event-Calling-Function";
/**
* {@code "Event-Calling-Line-Number"}
*/
public static final String EVENT_CALLING_LINE_NUMBER = "Event-Calling-Line-Number";
/**
* {@code "FreeSWITCH-Hostname"}
*/
public static final String FREESWITCH_HOSTNAME = "FreeSWITCH-Hostname";
/**
* {@code "FreeSWITCH-IPv4"}
*/
public static final String FREESWITCH_IPV4 = "FreeSWITCH-IPv4";
/**
* {@code "FreeSWITCH-IPv6"}
*/
public static final String FREESWITCH_IPV6 = "FreeSWITCH-IPv6";
/**
* {@code "Core-UUID"}
*/
public static final String CORE_UUID = "Core-UUID";
/**
* {@code "Content-Length"}
*/
public static final String CONTENT_LENGTH = "Content-Length";
/**
* {@code "Job-Command"}
*/
public static final String JOB_COMMAND = "Job-Command";
/**
* {@code "Job-UUID"}
*/
public static final String JOB_UUID = "Job-UUID";
private EslEventHeaderNames()
{
/* private class */
}
}
/*
* 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.transport.message;
/**
* Container class for enumeration of ESL message header names, and some commonly used
* header string values.
*
* @author david varnes
* @version $Id$
*/
public class EslHeaders
{
/**
* Standard ESL header names.
* </p>
* Note this enum will need to be kept in synch with any new headers introduced on the server side.
*
* @author david varnes
* @version $Id$
*/
public enum Name
{
/*
* Minor optimization - put most often used headers at the top for fastest resolution
* in static fromLiteral().
*/
/**
* {@code "Content-Type"}
*/
CONTENT_TYPE( "Content-Type" ),
/**
* {@code "Content-Length"}
*/
CONTENT_LENGTH( "Content-Length" ),
/**
* {@code "Reply-Text"}
*/
REPLY_TEXT( "Reply-Text" ),
/**
* {@code "Job-UUID"}
*/
JOB_UUID( "Job-UUID" ),
/**
* {@code "Socket-Mode"}
*/
SOCKET_MODE( "Socket-Mode" ),
/**
* {@code "Control"}
*/
Control( "Control" ),
;
private final String literal;
private Name( String literal )
{
this.literal = literal;
}
public String literal()
{
return literal;
}
public static Name fromLiteral( String literal )
{
for ( Name name : values() )
{
if ( name.literal.equalsIgnoreCase( literal ) )
{
return name;
}
}
return null;
}
}
/**
* Some common ESL header values. These are provided as a convenience for commonly used values.
* </p>
* This values are not coded as an enum to allow for the very large range of possible values,
* since they are just Strings.
*
* @author david varnes
* @version $Id$
*/
public static final class Value
{
/**
* {@code "+OK"}
*/
public static final String OK = "+OK";
/**
* {@code "auth/request"}
*/
public static final String AUTH_REQUEST = "auth/request";
/**
* {@code "api/response"}
*/
public static final String API_RESPONSE = "api/response";
/**
* {@code "command/reply"}
*/
public static final String COMMAND_REPLY = "command/reply";
/**
* {@code "text/event-plain"}
*/
public static final String TEXT_EVENT_PLAIN = "text/event-plain";
/**
* {@code "text/event-xml"}
*/
public static final String TEXT_EVENT_XML = "text/event-xml";
/**
* {@code "text/disconnect-notice"}
*/
public static final String TEXT_DISCONNECT_NOTICE = "text/disconnect-notice";
/**
* {@code "-ERR invalid"}
*/
public static final String ERR_INVALID = "-ERR invalid";
}
}
/*
* 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.transport.message;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.freeswitch.esl.client.transport.message.EslHeaders.Name;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Basic FreeSWITCH Event Socket messages from the server are decoded into this data object.
* </p>
* An ESL message is modelled as text lines. A message always has one or more header lines, and
* optionally may have some body lines.
* <p/>
* Header lines are parsed and cached in a map keyed by the {@link EslHeaders.Name} enum. A message
* is always expected to have a "Content-Type" header</br>
* Any Body lines are cached in a list.
*
* @author david varnes
* @version $Id$
* @see EslHeaders.Name
*/
public class EslMessage
{
private final Logger log = LoggerFactory.getLogger( this.getClass() );
private final Map<Name,String> headers = new HashMap<Name,String>();
private final List<String> body = new ArrayList<String>();
private Integer contentLength = null;
private int receivedContentLength = 0;
/**
* All the received message headers in a map keyed by {@link EslHeaders.Name}. The string mapped value
* is the parsed content of the header line (ie, it does not include the header name).
*
* @return map of header values
*/
public Map<Name,String> getHeaders()
{
return headers;
}
/**
* Convenience method
*
* @param headerName as a {@link EslHeaders.Name}
* @return true if an only if there is a header entry with the supplied header name
*/
public boolean hasHeader( Name headerName )
{
return headers.containsKey( headerName );
}
/**
* Convenience method
*
* @param headerName as a {@link EslHeaders.Name}
* @return same as getHeaders().get( headerName )
*/
public String getHeaderValue( Name headerName )
{
return headers.get( headerName );
}
/**
* Convenience method
*
* @return true if and only if a header exists with name "Content-Length"
*/
public boolean hasContentLength()
{
return headers.containsKey( Name.CONTENT_LENGTH );
}
/**
* Convenience method
*
* @return integer value of header with name "Content-Length"
*/
public Integer getContentLength()
{
if ( contentLength != null )
{
return contentLength;
}
if ( hasContentLength() )
{
contentLength = Integer.valueOf( headers.get( Name.CONTENT_LENGTH) );
}
return contentLength;
}
/**
* Convenience method
*
* @return header value of header with name "Content-Type"
*/
public String getContentType()
{
return headers.get( Name.CONTENT_TYPE );
}
/**
* Any received message body lines
*
* @return list with a string for each line received, may be an empty list
*/
public List<String> getBodyLines()
{
return body;
}
/**
* Used by the {@link EslMessageDecoder}.
*
* @param name
* @param value
*/
void addHeader( Name name, String value )
{
log.debug( "adding header [{}] [{}]", name, value );
headers.put( name, value );
}
/**
* Used by the {@link EslMessageDecoder}
*
* @param line
*/
void addBodyLine( String line )
{
if ( line == null )
{
return;
}
receivedContentLength += ( line.length() + 1 );
// ignore empty lines (signals the end of a body section)
if ( line.length() > 0 )
{
body.add( line );
}
}
/**
* Used by the {@link EslMessageDecoder}
*
* @return true if receivedContentLength < contentLength
*/
boolean waitingForContent()
{
log.debug( "received [{}], expecting [{}]", receivedContentLength, getContentLength() );
return receivedContentLength < getContentLength();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder( "EslMessage: contentType=[" );
sb.append( getContentType() );
sb.append( "] headers=" );
sb.append( headers.size() );
sb.append( ", body=" );
sb.append( body.size() );
sb.append( " lines." );
return sb.toString();
}
}
/*
* 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.transport.message;
import org.freeswitch.esl.client.internal.HeaderParser;
import org.freeswitch.esl.client.transport.message.EslHeaders.Name;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.UpstreamMessageEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Decoder used by the IO processing pipeline. Client consumers should never need to use
* this class.
*
* @author david varnes
* @version $Id$
*/
@ChannelPipelineCoverage( "one" )
public class EslMessageDecoder extends SimpleChannelUpstreamHandler
{
private final Logger log = LoggerFactory.getLogger( this.getClass() );
private EslMessage currentMessage;
private boolean treatUnknownHeadersAsBody = false;
public EslMessageDecoder()
{
}
public EslMessageDecoder( boolean treatUnknownHeadersAsBody )
{
this.treatUnknownHeadersAsBody = treatUnknownHeadersAsBody;
}
private enum State
{
NEW_MESSAGE,
READ_HEADER_LINE,
READ_BODY_LINE,
}
private State state = State.NEW_MESSAGE;
@Override
public void messageReceived( ChannelHandlerContext ctx, MessageEvent e ) throws Exception
{
// Assume this is a decoded string
String messageLine = e.getMessage().toString();
log.debug( "Received string: [{}]", e.getMessage() );
log.trace( "State [{}]", state );
switch ( state )
{
case NEW_MESSAGE:
if ( messageLine.isEmpty() )
{
break;
}
currentMessage = new EslMessage();
state = State.READ_HEADER_LINE;
// fall-through
case READ_HEADER_LINE:
if ( messageLine.isEmpty() )
{
if ( currentMessage.hasContentLength() )
{
// end of headers - body to come
state = State.READ_BODY_LINE;
}
else
{
// end of message
state = State.NEW_MESSAGE;
// send upstream
UpstreamMessageEvent upstreamEvent = new UpstreamMessageEvent( e.getChannel(),
currentMessage, e.getRemoteAddress() );
currentMessage = null;
ctx.sendUpstream( upstreamEvent );
}
}
else
{
// split the line
String[] headerParts = HeaderParser.splitHeader( messageLine );
Name headerName = Name.fromLiteral( headerParts[0] );
if ( headerName == null )
{
if ( treatUnknownHeadersAsBody )
{
// treat this as a body line - note that this will screw up genuine body
// content-length calculations
currentMessage.addBodyLine( messageLine );
}
else
{
throw new IllegalStateException( "Unhandled ESL header [" + headerParts[0] + ']' );
}
}
currentMessage.addHeader( headerName, headerParts[1] );
}
break;
case READ_BODY_LINE:
// monitor received content compared to expected content-length
currentMessage.addBodyLine( messageLine );
if ( ! currentMessage.waitingForContent() )
{
// end of message
state = State.NEW_MESSAGE;
// send upstream
UpstreamMessageEvent upstreamEvent = new UpstreamMessageEvent( e.getChannel(),
currentMessage, e.getRemoteAddress() );
currentMessage = null;
ctx.sendUpstream( upstreamEvent );
}
break;
default:
break;
}
}
}
/*
* 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.inbound;
import org.freeswitch.esl.client.IEslEventListener;
import org.freeswitch.esl.client.inbound.Client;
import org.freeswitch.esl.client.inbound.InboundConnectionFailure;
import org.freeswitch.esl.client.transport.event.EslEvent;
import org.freeswitch.esl.client.transport.message.EslMessage;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ClientTest
{
private final Logger log = LoggerFactory.getLogger( this.getClass() );
private String host = "localhost";
private int port = 8021;
private String password = "ClueCon";
@Test
public void do_connect() throws InterruptedException
{
Client client = new Client();
client.addEventListener( new IEslEventListener()
{
public void eventReceived( EslEvent event )
{
log.info( "Event received [{}]", event );
}
public void backgroundJobResultReceived( EslEvent event )
{
log.info( "Background job result received [{}]", event );
}
} );
log.info( "Client connecting .." );
try
{
client.connect( host, port, password, 2 );
}
catch ( InboundConnectionFailure e )
{
log.error( "Connect failed", e );
return;
}
log.info( "Client connected .." );
// client.setEventSubscriptions( "plain", "heartbeat CHANNEL_CREATE CHANNEL_DESTROY BACKGROUND_JOB" );
client.setEventSubscriptions( "plain", "all" );
client.addEventFilter( "Event-Name", "heartbeat" );
client.cancelEventSubscriptions();
client.setEventSubscriptions( "plain", "all" );
client.addEventFilter( "Event-Name", "heartbeat" );
client.addEventFilter( "Event-Name", "channel_create" );
client.addEventFilter( "Event-Name", "background_job" );
// client.sendSyncCommand( "echo", "Foo foo bar" );
// client.sendSyncCommand( "originate", "sofia/internal/101@192.168.100.201! sofia/internal/102@192.168.100.201!" );
// client.sendSyncApiCommand( "sofia status", "" );
String jobId = client.sendAsyncApiCommand( "status", "" );
log.info( "Job id [{}] for [status]", jobId );
client.sendSyncApiCommand( "version", "" );
// client.sendAsyncApiCommand( "status", "" );
// client.sendSyncApiCommand( "sofia status", "" );
// client.sendAsyncApiCommand( "status", "" );
EslMessage response = client.sendSyncApiCommand( "sofia status", "" );
log.info( "sofia status = [{}]", response.getBodyLines().get( 3 ) );
Thread.sleep( 25000 );
client.close();
}
@Test
public void do_multi_connects() throws InterruptedException
{
Client client = new Client();
log.info( "Client connecting .." );
try
{
client.connect( host, port, password, 2 );
}
catch ( InboundConnectionFailure e )
{
log.error( "Connect failed", e );
return;
}
log.info( "Client connected .." );
log.info( "Client connecting .." );
try
{
client.connect( "127.0.0.1", 8021, password, 2 );
}
catch ( InboundConnectionFailure e )
{
log.error( "Connect failed", e );
return;
}
log.info( "Client connected .." );
}
}
/*
* 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.outbound;
import org.freeswitch.esl.client.outbound.example.SimpleHangupPipelineFactory;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SocketClientTest
{
private final Logger log = LoggerFactory.getLogger( this.getClass() );
@Test
public void run_client() throws InterruptedException
{
log.info( "Test starting ..." );
SocketClient client = new SocketClient( 8084, new SimpleHangupPipelineFactory() );
client.start();
Thread.sleep( 45000 );
client.stop();
log.info( "Test ended" );
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
* 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.
-->
<configuration>
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</Pattern>
</layout>
</appender>
<logger name="org.freeswitch.esl.client.outbound.example">
<level value="debug" />
</logger>
<logger name="org.freeswitch.esl.client.inbound">
<level value="debug" />
</logger>
<logger name="org.freeswitch.esl.client.transport.message">
<level value="info" />
</logger>
<logger name="org.freeswitch.esl.client">
<level value="info" />
</logger>
<root>
<level value="warn" />
<appender-ref ref="STDOUT" />
</root>
</configuration>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<!--
* 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.freeswitch.esl.client</groupId>
<artifactId>esl-client-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>FreeSWITCH Event Socket Library - Java Client project</name>
<url>http://www.freeswitch.org</url>
<description>
The FreeSWITCH Event Socket Library provides a TCP based control and/or
monitoring interface to a running switch. This project provides a Java
client to the ESL.
</description>
<organization>
<name>FreeSWITCH.org</name>
<url>http://www.freeswitch.org</url>
</organization>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<inceptionYear>2009</inceptionYear>
<scm>
<connection>scm:svn:http://svn.freeswitch.org/svn/freeswitch/trunk/contrib/dvarnes/java/esl-client</connection>
<developerConnection>scm:svn:http://svn.freeswitch.org/svn/freeswitch/trunk/contrib/dvarnes/java/esl-client</developerConnection>
</scm>
<developers>
<developer>
<id>dvarnes</id>
<name>david varnes</name>
<email>david at varnes dot net</email>
<timezone>GMT+10</timezone>
</developer>
</developers>
<modules>
<module>org.freeswitch.esl.client</module>
<module>org.freeswitch.esl.client.example</module>
</modules>
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<!-- every child project inherits these plugins -->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-source-plugin</artifactId>
<version>2.1</version>
<executions>
<execution>
<id>attach-source</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<attach>true</attach>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论