From 48fb810a46d4557face11d4e0064ad5da231abc3 Mon Sep 17 00:00:00 2001 From: "yjmyzz@126.com" <2city@4tory> Date: Wed, 12 Feb 2020 21:26:00 +0800 Subject: [PATCH] 0.9.3 --- .gitignore | 4 + README.md | 111 +- TODO.txt | 34 - build.gradle | 24 +- gradle/wrapper/gradle-wrapper.jar | Bin 52141 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 5 - gradlew | 78 +- gradlew.bat | 14 +- pom.xml | 177 ++ .../{inbound => }/IEslEventListener.java | 91 +- .../esl/client/dptools/DpTools.java | 19 - .../esl/client/dptools/Execute.java | 1709 ----------------- .../esl/client/dptools/ExecuteException.java | 22 - .../freeswitch/esl/client/inbound/Client.java | 844 ++++---- .../inbound/InboundChannelInitializer.java | 32 - .../client/inbound/InboundClientHandler.java | 168 +- .../inbound/InboundConnectionFailure.java | 23 +- .../inbound/InboundPipelineFactory.java | 58 + .../internal/AbstractEslClientHandler.java | 495 ++--- .../esl/client/internal/Context.java | 299 --- .../esl/client/internal/HeaderParser.java | 99 + .../IEslProtocolListener.java | 69 +- .../esl/client/internal/IModEslApi.java | 74 - .../internal/debug/ChannelEventRunnable.java | 89 + .../internal/debug/ExecutionHandler.java | 113 ++ .../AbstractOutboundClientHandler.java | 70 + .../AbstractOutboundPipelineFactory.java | 52 + .../esl/client/outbound/IClientHandler.java | 9 - .../outbound/IClientHandlerFactory.java | 5 - .../outbound/OutboundChannelInitializer.java | 40 - .../outbound/OutboundClientHandler.java | 82 - .../esl/client/outbound/SocketClient.java | 105 +- .../example/SimpleHangupOutboundHandler.java | 83 + .../example/SimpleHangupPipelineFactory.java | 35 + .../esl/client/transport/CommandResponse.java | 88 +- .../esl/client/transport/HeaderParser.java | 93 - .../esl/client/transport/SendMsg.java | 323 ++-- .../esl/client/transport/event/EslEvent.java | 353 ++-- .../transport/event/EslEventHeaderNames.java | 126 +- .../transport/message/EslFrameDecoder.java | 354 ++-- .../client/transport/message/EslHeaders.java | 215 ++- .../client/transport/message/EslMessage.java | 263 +-- src/test/java/OutboundTest.java | 116 -- .../freeswitch/esl/client/ClientExample.java | 34 - .../org/freeswitch/esl/client/ClientTest.java | 57 + .../esl/client/SocketClientTest.java | 13 + .../message/EslFrameDecoderTest.java | 153 -- 47 files changed, 2899 insertions(+), 4421 deletions(-) delete mode 100644 TODO.txt delete mode 100644 gradle/wrapper/gradle-wrapper.jar delete mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 pom.xml rename src/main/java/org/freeswitch/esl/client/{inbound => }/IEslEventListener.java (52%) delete mode 100644 src/main/java/org/freeswitch/esl/client/dptools/DpTools.java delete mode 100644 src/main/java/org/freeswitch/esl/client/dptools/Execute.java delete mode 100644 src/main/java/org/freeswitch/esl/client/dptools/ExecuteException.java delete mode 100644 src/main/java/org/freeswitch/esl/client/inbound/InboundChannelInitializer.java create mode 100644 src/main/java/org/freeswitch/esl/client/inbound/InboundPipelineFactory.java delete mode 100644 src/main/java/org/freeswitch/esl/client/internal/Context.java create mode 100644 src/main/java/org/freeswitch/esl/client/internal/HeaderParser.java rename src/main/java/org/freeswitch/esl/client/{inbound => internal}/IEslProtocolListener.java (73%) delete mode 100644 src/main/java/org/freeswitch/esl/client/internal/IModEslApi.java create mode 100644 src/main/java/org/freeswitch/esl/client/internal/debug/ChannelEventRunnable.java create mode 100644 src/main/java/org/freeswitch/esl/client/internal/debug/ExecutionHandler.java create mode 100644 src/main/java/org/freeswitch/esl/client/outbound/AbstractOutboundClientHandler.java create mode 100644 src/main/java/org/freeswitch/esl/client/outbound/AbstractOutboundPipelineFactory.java delete mode 100644 src/main/java/org/freeswitch/esl/client/outbound/IClientHandler.java delete mode 100644 src/main/java/org/freeswitch/esl/client/outbound/IClientHandlerFactory.java delete mode 100644 src/main/java/org/freeswitch/esl/client/outbound/OutboundChannelInitializer.java delete mode 100644 src/main/java/org/freeswitch/esl/client/outbound/OutboundClientHandler.java create mode 100644 src/main/java/org/freeswitch/esl/client/outbound/example/SimpleHangupOutboundHandler.java create mode 100644 src/main/java/org/freeswitch/esl/client/outbound/example/SimpleHangupPipelineFactory.java delete mode 100644 src/main/java/org/freeswitch/esl/client/transport/HeaderParser.java delete mode 100644 src/test/java/OutboundTest.java delete mode 100644 src/test/java/org/freeswitch/esl/client/ClientExample.java create mode 100644 src/test/java/org/freeswitch/esl/client/ClientTest.java create mode 100644 src/test/java/org/freeswitch/esl/client/SocketClientTest.java delete mode 100644 src/test/java/org/freeswitch/esl/client/transport/message/EslFrameDecoderTest.java diff --git a/.gitignore b/.gitignore index 1ce7157..b0f511d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ buildNumber.properties ### Gradle template .gradle build/ +gradle # Ignore Gradle GUI config gradle-app.setting @@ -36,3 +37,6 @@ gradle-app.setting !gradle-wrapper.jar +# idea +.idea + diff --git a/README.md b/README.md index 6b7886a..e6842d5 100644 --- a/README.md +++ b/README.md @@ -11,70 +11,90 @@ esl-client This project is a fork of the unmaintained, original project at -Status: In Progress... +Status: done -Example +Inbound Example ------------------------------------------------------------------------------ ```java -package com.ecovate.freeswitch.lb; +package org.freeswitch.esl.client; -import com.google.common.base.Throwables; import org.freeswitch.esl.client.inbound.Client; -import org.freeswitch.esl.client.inbound.IEslEventListener; -import org.freeswitch.esl.client.internal.IModEslApi.EventFormat; -import org.freeswitch.esl.client.outbound.Context; -import org.freeswitch.esl.client.outbound.IClientHandler; -import org.freeswitch.esl.client.outbound.IClientHandlerFactory; -import org.freeswitch.esl.client.outbound.SocketClient; import org.freeswitch.esl.client.transport.event.EslEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.net.InetSocketAddress; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; -public class FreeSwitchEventListener { +public class ClientTest { - private static Logger logger = LoggerFactory.getLogger(FreeSwitchEventListener.class); + private static class DemoEventListener implements IEslEventListener { - public static void main(String[] args) { - try { + @Override + public void eventReceived(EslEvent event) { + System.out.println("eventReceived:" + event.getEventName()); + } - final Client inboudClient = new Client(); - inboudClient.connect(new InetSocketAddress("localhost", 8021), "ClueCon", 10); - inboudClient.addEventListener(new IEslEventListener() { @Override - public void onEslEvent(EslEvent eslEvent) { + public void backgroundJobResultReceived(EslEvent event) { + System.out.println("backgroundJobResultReceived:" + event.getEventName()); + } + } + public static void main(String[] args) throws InterruptedException { + String host = "localhost"; + int port = 8021; + String password = "ClueCon"; + int timeoutSeconds = 10; + Client inboundClient = new Client(2, 8); + try { + inboundClient.connect(host, port, password, timeoutSeconds); + inboundClient.addEventListener(new DemoEventListener()); + inboundClient.setEventSubscriptions("plain", "all"); + } catch (Exception e) { + System.out.println("connect fail"); } - }); - inboudClient.setEventSubscriptions(EventFormat.PLAIN, "all"); - - final SocketClient outboundServer = new SocketClient( - new InetSocketAddress("localhost", 8084), - new IClientHandlerFactory() { - @Override - public IClientHandler createClientHandler() { - return new IClientHandler() { - @Override - public void handleEslEvent(Context context, EslEvent eslEvent) { - } - - @Override - public void onConnect(Context context, EslEvent eslEvent) { - } - }; - } - }); - - - } catch (Throwable t) { - Throwables.propagate(t); + + //health-check + ScheduledExecutorService service = new ScheduledThreadPoolExecutor(1); + service.scheduleAtFixedRate(() -> { + System.out.println(System.currentTimeMillis() + " " + inboundClient.canSend()); + if (!inboundClient.canSend()) { + try { + //重连 + inboundClient.connect(host, port, password, timeoutSeconds); + inboundClient.cancelEventSubscriptions(); + inboundClient.setEventSubscriptions("plain", "all"); + } catch (Exception e) { + System.out.println("connect fail"); + } + } + }, 1, 500, TimeUnit.MILLISECONDS); + + System.out.println("other process ..."); } - } +} + +``` +Outbound Example +------------------------------------------------------------------------------ +```java +package org.freeswitch.esl.client; + +import org.freeswitch.esl.client.outbound.SocketClient; +import org.freeswitch.esl.client.outbound.example.SimpleHangupPipelineFactory; + +public class SocketClientTest { + + public static void main(String[] args) { + SocketClient client = new SocketClient(8086, new SimpleHangupPipelineFactory(), 2, 16); + client.start(); + System.out.println("started ..."); + } } + ``` Authors @@ -84,6 +104,7 @@ Authors - [Dave Rusek](mailto:dave.rusek@readytalk.com) - [David Varnes](mailto:david.varnes@gmail.com) (original author) - [Tobias Bieniek](https://github.com/Turbo87) +- [菩提树下的杨过](https://www.cnblogs.com/yjmyzz/) License ------------------------------------------------------------------------------ diff --git a/TODO.txt b/TODO.txt deleted file mode 100644 index ac8d19b..0000000 --- a/TODO.txt +++ /dev/null @@ -1,34 +0,0 @@ -As at 2010.08.29 - -Features still to be implemented - * [Fixed]: Fix the event parser to properly use Content-Length header, reports of misshandling missing \n - * Problem using latest netty release in an OSGi container - * Chase down the apparent issue with Netty when using executor in pipeline (wierd). - * Improve exception handling - implement exceptionCaught() - * More testing of the outbound socket client template code - * Implement Send event command - * Testing of SendMsg command - * Refactor the api of the inbound client into the abstract handler so it is also available in outbound handlers as well - * 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. - * Implement 'myevent' event subscription for inbound - not a priority here, easy if required. - * Provide timeout protection on the client.sendSyncApiCommand(). It will currently block for ever if get no response from server. - * Working examples in an example project (started). - * Add OSGi example - -Distribution - * [Fixed - using Sonatype] Find out how (if ok) to put binary distribution on files.freeswitch.org - * [Done] Cut a release, look at usage of tags in FS git repo. - * [Started] FreeSWITCH wiki pages - * Basic usage docs - * Package jar, javadocs, source and dependencies (slf4j, netty) for people to trial without having to build. - * Host the javadoc API somewhere in the org.freeswitch domain .. files ? - -Quality items - * [No] Is there a git equivalent to svn $Id$ tag ? - * 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 diff --git a/build.gradle b/build.gradle index 7fcc84f..72ca33b 100644 --- a/build.gradle +++ b/build.gradle @@ -2,19 +2,31 @@ apply plugin: 'java' apply plugin: 'maven' group = 'org.freeswitch.esl.client' -version = '0.10.0-SNAPSHOT' +version = '0.9.3' sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { - maven { url "http://repo.maven.apache.org/maven2" } + maven{url "http://maven.aliyun.com/nexus/content/groups/public"} + maven { url "http://repo.maven.apache.org/maven2" } +} + +//打包源代码 +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +artifacts { + archives sourcesJar } dependencies { - compile 'io.netty:netty-all:4.1.17.Final' - compile 'com.google.guava:guava:23.4-jre' - compile 'org.slf4j:slf4j-api:1.7.25' - runtime 'ch.qos.logback:logback-classic:1.2.3' + compile 'org.jboss.netty:netty:3.2.1.Final' + compile 'org.slf4j:slf4j-api:1.6.1' + compile 'com.google.guava:guava:28.0-jre' + compile 'ch.qos.logback:logback-classic:1.2.3' testCompile 'junit:junit:4.8.1' + testCompile 'ch.qos.logback:logback-classic:0.9.24' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 085a1cdc27db1185342f15a00441734e74fe3735..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52141 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
)Mf(9O=$zt#xQMm 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,31 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -90,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -114,6 +113,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` @@ -154,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec9973..0f8d593 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +46,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c149e56 --- /dev/null +++ b/pom.xml @@ -0,0 +1,177 @@ + + + + 4.0.0 + org.freeswitch.esl.client + org.freeswitch.esl.client + 0.9.3 + FreeSWITCH Event Socket Library - Java Client + jar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + UTF-8 + + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + package + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.google.guava + guava + 28.0-jre + + + + + + org.jboss.netty + netty + 3.2.1.Final + + + + + org.slf4j + slf4j-api + 1.6.1 + + + + + junit + junit + 4.8.1 + test + + + + ch.qos.logback + logback-classic + 1.2.3 + test + + + + org.apache.commons + commons-lang3 + 3.4 + test + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/freeswitch/esl/client/inbound/IEslEventListener.java b/src/main/java/org/freeswitch/esl/client/IEslEventListener.java similarity index 52% rename from src/main/java/org/freeswitch/esl/client/inbound/IEslEventListener.java rename to src/main/java/org/freeswitch/esl/client/IEslEventListener.java index aef3a73..91b3f08 100644 --- a/src/main/java/org/freeswitch/esl/client/inbound/IEslEventListener.java +++ b/src/main/java/org/freeswitch/esl/client/IEslEventListener.java @@ -1,39 +1,52 @@ -/* - * 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.Context; -import org.freeswitch.esl.client.transport.event.EslEvent; - -/** - * Interface for observers wanting to be notified of incoming FreeSWITCH Event Socket events. - *

- * Events are guaranteed to be processed (and listeners notified) in the order in which the - * events are received off the wire. - *

- * 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. - */ -public interface IEslEventListener { - /** - * Signal of a server initiated event. - * - * @param event as an {@link EslEvent} - */ - void onEslEvent(Context ctx, 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; + +import org.freeswitch.esl.client.transport.event.EslEvent; + +/** + * Interface for observers wanting to be notified of incoming FreeSWITCH Event Socket events. + *

+ * 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. + *

+ * 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. + *

+ * 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 + */ +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); +} diff --git a/src/main/java/org/freeswitch/esl/client/dptools/DpTools.java b/src/main/java/org/freeswitch/esl/client/dptools/DpTools.java deleted file mode 100644 index f4a5172..0000000 --- a/src/main/java/org/freeswitch/esl/client/dptools/DpTools.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.freeswitch.esl.client.dptools; - -import org.freeswitch.esl.client.internal.IModEslApi; -import org.freeswitch.esl.client.transport.SendMsg; - -public class DpTools { - - private final IModEslApi api; - - public DpTools(IModEslApi api) { - this.api = api; - } - - public DpTools answer() { - api.sendMessage(new SendMsg().addCallCommand("answer")); - return this; - } - -} diff --git a/src/main/java/org/freeswitch/esl/client/dptools/Execute.java b/src/main/java/org/freeswitch/esl/client/dptools/Execute.java deleted file mode 100644 index 20f8da9..0000000 --- a/src/main/java/org/freeswitch/esl/client/dptools/Execute.java +++ /dev/null @@ -1,1709 +0,0 @@ -package org.freeswitch.esl.client.dptools; - -import java.util.UUID; - -import org.freeswitch.esl.client.internal.IModEslApi; -import org.freeswitch.esl.client.transport.CommandResponse; -import org.freeswitch.esl.client.transport.SendMsg; -import org.freeswitch.esl.client.transport.message.EslMessage; - -public class Execute { - - IModEslApi api; - String _uuid; - - public Execute(IModEslApi api, String uuid) { - this.api = api; - this._uuid = uuid; - } - - /** - * Sends an info packet with a sipfrag. If the phone supports it will show - * message on the display. - * - * @param message - * to display - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_Send_Display - * - */ - public void sendDiplay(String message) throws ExecuteException { - sendExeMesg("send_display", message); - } - - /** - * Answers an incoming call or session. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_answer - * - */ - public void answer() throws ExecuteException { - sendExeMesg("answer"); - - } - - /** - * Make an attended transfer. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_att_xfer - * - * @param channelUrl - * ex: sofia/default/${attxfer_callthis} - */ - public void attAnswer(String channelUrl) throws ExecuteException { - sendExeMesg("att_xfer", channelUrl); - } - - /** - * - * @param key - * the button you want to respond to after the * button is - * pressed. If you wanted to respond to *1, you would put 1 in - * place of KEY. You are limited to a single digit. - * @param leg - * which call leg(s) to listen on. Acceptable parameters are a, b - * or ab. - * @param flags - * modifies the behavior. The following flags are available: a - - * Respond on A leg, b - Respond on B leg, o - Respond on - * opposite leg, s - Respond on same leg, i - Execute inline, 1 - - * Unbind this meta_app after it is used one time - * @param application - * is which application you want to execute. - * @param params - * are the arguments you want or need to provide to the - * APPLICATION. - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_bind_meta_app - * - */ - public void bindMetaApp(String key, String leg, String flags, - String application, String params) throws ExecuteException { - sendExeMesg("bind_meta_app", key + " " + leg + flags + " " + application + "::" + params); - } - - /** - * Cancels currently running application on the given UUID. Dialplan - * execution proceeds to the next application. Optionally clears all - * unprocessed events (queued applications) on the channel. - * - * @param all - * clear all unprocessed events (queued applications) on the - * channel, otherwise just the current application. - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_break - * - */ - public void breakChannel(boolean all) throws ExecuteException { - sendExeMesg("break", all ? "all" : ""); - } - - /** - * Provides the ability to bridge two endpoints. Generally used to route an - * incoming call to one or more endpoints. Multiple endpoints can be dialed - * simultaneously or sequentially using the comma and pipe delimiters, - * respectively. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_bridge - * - */ - public void bridge(String endpoint) throws ExecuteException { - sendExeMesg("bridge", endpoint); - } - - /** - * Export a channel variable across a bridge. This application differs from - * export in that it works with *any* kind of bridge, not just a bridge - * called from the dialplan. For example, bridge_export will export its - * variables if the leg is uuid_transfer'd whereas export will not - * - * @param key - * channel variable name - * @param value - * channel variable value - * @param local - * to only export to the B leg false, otherwise true - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_bridge_export - * - */ - public void bridgeExport(String key, String value, boolean local) - throws ExecuteException { - StringBuilder sb = new StringBuilder(); - if(!local) - sb.append("nolocal:"); - sb.append(key); - sb.append("="); - sb.append(value); - sendExeMesg("bridge_export",sb.toString()); - } - - /** - * Send a text message to a IM client. - * - * @param proto - * ex: sip - * @param from - * ex: 1000@127.0.0.1 - * @param to - * ex: 1001@127.0.0.1 - * @param message - * ex: Hello chat from freeswitch - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_chat - * - */ - public void chat(String proto, String from, String to, String message) - throws ExecuteException { - sendExeMesg("chat", proto + "|" + from + "|" + to + "|" + message); - } - - /** - * cng plc is just an app that says to perform plc on any lost packets and - * execute on originate. It is like execute on answer, etc. but only for - * outbound calls during originate. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_cng_plc - * - */ - public void cngPlc() throws ExecuteException { - sendExeMesg("cng_plc"); - } - - /** - * Start or join a conference - */ - public void conference(String name) throws ExecuteException { - conference(name,null,null,null); - } - - /** - * Start or join a conference - */ - public void conference(String name, String profile) throws ExecuteException { - conference(name,profile,null,null); - } - - /** - * Start or join a conference - */ - public void conference(String name, String profile, String pin) throws ExecuteException { - conference(name,profile,pin,null); - } - - /** - * Start or join a conference - */ - public void conference(String name, String profile, String pin, String flags) throws ExecuteException { - StringBuilder sb = new StringBuilder(name); - if(nn(profile)) - sb.append("@").append(profile); - if(nn(pin)) - sb.append("+").append(pin); - if(nn(flags)) - sb.append("+flags{").append(flags).append("}"); - sendExeMesg("conference", sb.toString()); - } - - /** - * Deflect sends a Refer to the client. The deflect application allows - * FreeSWITCH to be removed from the list of connection hops and tell the - * originator to reroute the call. When using the deflect application, - * FreeSWITCH first hangs up the channel and then send a REFER message and a - * new INVITE message to the originator. The originator, which could be a - * gateway or sip proxy, should read the INVITE and reroute the call - * accordingly. - * - * @param endpoint - * SIP URI to contact (with or without "sip:") - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_deflect - * - */ - public void deflect(String endpoint) throws ExecuteException { - sendExeMesg("deflect", endpoint); - } - - /** - * Places the calling channel in delayed loopback mode. It simply returns - * everything sent, including voice, DTMF, etc but with the specified delay - * [ms]. It is generally useful for checking if RTP audio path works both - * ways. Normal echo app can fail when tested on speaker phone. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_delay_echo - * - */ - public void delayEcho(long ms) throws ExecuteException { - sendExeMesg("delay_echo", ms + ""); - } - - /** - * Implements speech recognition. - * - * @param args - * [] grammar - * [] grammaron grammaroff - * grammarsalloff nogrammar param - * pause resume start_input_timers stop - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_detect_speech - * - */ - public void detectSpeech(String args) throws ExecuteException { - sendExeMesg("detect_speech", args); - } - - /** - * Displace file. Plays a file or stream to a channel. - * - * @param path - * any sound format FreeSWITCH supports, wav, local_steam, shout - * etc. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_displace_session - * - */ - public void displaceSession(String path) throws ExecuteException { - displaceSession(path, null, 0); - } - - /** - * Displace file. Plays a file or stream to a channel. - * - * @param path - * any sound format FreeSWITCH supports, wav, local_steam, shout - * etc. - * @param flags - * flags to stream, null if none - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_displace_session - * - */ - public void displaceSession(String path, String flags) - throws ExecuteException { - displaceSession(path, flags, 0); - - } - - /** - * Displace file. Plays a file or stream to a channel. - * - * @param path - * any sound format FreeSWITCH supports, wav, local_steam, shout - * etc. - * @param flags - * flags to stream, null if none - * @param timeLimitMillis - * optional time limit, 0 for none - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_displace_session - * - */ - public void displaceSession(String path, String flags, - long timeLimitMillis) throws ExecuteException { - StringBuilder sb = new StringBuilder(path); - if(nn(flags)) - sb.append(" ").append(flags); - if(timeLimitMillis > 0 ) - sb.append(" +").append(timeLimitMillis); - - sendExeMesg("displace_session",sb.toString()); - } - - /** - * Provides the ability to spy on a channel. It often referred to as call - * barge. For persistent spying on a user see Mod_spy. - * - * DTMF signals during eavesdrop, 2 to speak with the uuid, 1 to speak with - * the other half, 3 to engage a three way, 0 to restore eavesdrop, * to - * next channel. - * - * @param uuid - * uuid of the call or 'all' for all - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_eavesdrop - * - */ - public void eavesdrop(String uuid) - throws ExecuteException { - - eavesdrop(uuid, false, null, null, null, null); - } - - /** - * Provides the ability to spy on a channel. It often referred to as call - * barge. For persistent spying on a user see Mod_spy. - * - * DTMF signals during eavesdrop, 2 to speak with the uuid, 1 to speak with - * the other half, 3 to engage a three way, 0 to restore eavesdrop, * to - * next channel. - * - * @param uuid - * uuid of the call or 'all' for all - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_eavesdrop - * - */ - public void eavesdrop(String uuid, boolean enableDTMF) - throws ExecuteException { - - eavesdrop(uuid, enableDTMF, null, null, null, null); - } - - /** - * Provides the ability to spy on a channel. It often referred to as call - * barge. For persistent spying on a user see Mod_spy. - * - * DTMF signals during eavesdrop, 2 to speak with the uuid, 1 to speak with - * the other half, 3 to engage a three way, 0 to restore eavesdrop, * to - * next channel. - * - * @param uuid - * uuid of the call or 'all' for all - * @param groupId - * if specified, eavesdrop only works with channels that have an - * "eavesdrop_group" channel variable set to the same name. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_eavesdrop - * - */ - public void eavesdrop(String uuid, boolean enableDTMF, String groupId) - throws ExecuteException { - - eavesdrop(uuid, enableDTMF, groupId, null, null, null); - } - - /** - * Provides the ability to spy on a channel. It often referred to as call - * barge. For persistent spying on a user see Mod_spy. - * - * DTMF signals during eavesdrop, 2 to speak with the uuid, 1 to speak with - * the other half, 3 to engage a three way, 0 to restore eavesdrop, * to - * next channel. - * - * @param uuid - * uuid of the call or 'all' for all - * @param groupId - * if specified, eavesdrop only works with channels that have an - * "eavesdrop_group" channel variable set to the same name. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_eavesdrop - * - */ - public void eavesdrop(String uuid, boolean enableDTMF, String groupId, - String failedWav) throws ExecuteException { - eavesdrop(uuid, enableDTMF, groupId, failedWav, null, null); - } - - /** - * Provides the ability to spy on a channel. It often referred to as call - * barge. For persistent spying on a user see Mod_spy. - * - * DTMF signals during eavesdrop, 2 to speak with the uuid, 1 to speak with - * the other half, 3 to engage a three way, 0 to restore eavesdrop, * to - * next channel. - * - * @param uuid - * uuid of the call or 'all' for all - * @param groupId - * if specified, eavesdrop only works with channels that have an - * "eavesdrop_group" channel variable set to the same name. - * @param failedWav - * ex: /sounds/failed.wav - * @param newChannelWav - * ex: /sounds/new_chan_announce.wav - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_eavesdrop - * - */ - public void eavesdrop(String uuid, boolean enableDTMF, String groupId, - String failedWav, String newChannelWav) throws ExecuteException { - - eavesdrop(uuid, enableDTMF, groupId, failedWav, newChannelWav, null); - - } - - /** - * Provides the ability to spy on a channel. It often referred to as call - * barge. For persistent spying on a user see Mod_spy. - * - * DTMF signals during eavesdrop, 2 to speak with the uuid, 1 to speak with - * the other half, 3 to engage a three way, 0 to restore eavesdrop, * to - * next channel. - * - * @param uuid - * uuid of the call or 'all' for all - * @param groupId - * if specified, eavesdrop only works with channels that have an - * "eavesdrop_group" channel variable set to the same name. - * @param failedWav - * ex: /sounds/failed.wav - * @param newChannelWav - * ex: /sounds/new_chan_announce.wav - * @param idleWav - * ex: /sounds/idle.wav - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_eavesdrop - * - */ - public void eavesdrop(String uuid, boolean enableDTMF, String groupId, - String failedWav, String newChannelWav, String idleWav) - throws ExecuteException { - - if (nn(groupId)) - set("eavesdrop_require_group", groupId); - if (nn(failedWav)) - set("eavesdrop_indicate_failed", failedWav); - if (nn(newChannelWav)) - set("eavesdrop_indicate_new", newChannelWav); - if (nn(idleWav)) - set("eavesdrop_indicate_idle", idleWav); - - set("eavesdrop_enable_dtmf", String.valueOf(enableDTMF)); - - sendExeMesg("eavesdrop", uuid); - } - - - /** - * Places the calling channel in loopback mode. It simply returns everything - * sent, including voice, DTMF, etc. Consider it an "echo test". It is - * generally useful for checking delay in a call path. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_echo - * - */ - public void echo() throws ExecuteException { - sendExeMesg("echo"); - } - - /** - * This application is used to play a file endlessly and the playing cannot - * be stopped externally. - * - * @param file - * to play - */ - public void endlessPlayback(String file) throws ExecuteException { - sendExeMesg("endless_playback", file); - } - - /** - * Eval can be used to execute an internal API or simply log some text to - * the console. - * - * @param string - * to eval - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_eval - * - */ - public void eval(String string) throws ExecuteException { - sendExeMesg("eval", string); - } - - /** - * Event application can be used to fire aribtrary events. - * - * @param event - * to send ex: - * Event-Subclass=myevent::notify,Event-Name=CUSTOM,key1 - * =value1,key2=value2 - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_event - * - */ - public void event(String event) throws ExecuteException { - sendExeMesg("event", event); - } - - /** - * execute an extension from within another extension with this dialplan - * application. - * - * - * @param extension - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_execute_extension - * - */ - public void executeExtension(String extension) throws ExecuteException { - executeExtension(extension, null, null); - } - /** - * execute an extension from within another extension with this dialplan - * application. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_execute_extension - * - */ - public void executeExtension(String extension, String dialplan) throws ExecuteException { - executeExtension(extension, dialplan, null); - } - - /** - * execute an extension from within another extension with this dialplan - * application. - * - * @param context - * (will only be set if optionalDialplan is not null) - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_execute_extension - * - */ - public void executeExtension(String extension, String dialplan, - String context) throws ExecuteException { - StringBuilder sb = new StringBuilder(extension); - if(nn(dialplan)) { - sb.append(" ").append(dialplan); - if(nn(context)) - sb.append(" ").append(context); - } - sendExeMesg("execute_extension", sb.toString()); - } - - /** - * Exports a channel variable from the A leg to the B leg. Variables and - * their values will be replicated in any new channels created from the one - * export was called. - * - * @param key - * channel variable name - * @param value - * channel variable value - * @param local - * to only export to the B leg false, otherwise true - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_export - * - * @throws ExecuteException - */ - public void export(String key, String value, boolean local) - throws ExecuteException { - StringBuilder sb = new StringBuilder(); - if(!local) - sb.append("nolocal:"); - sb.append(key); - sb.append("="); - sb.append(value); - sendExeMesg("export", sb.toString()); - } - - /** - * When a fax is detected, the call will be routed to the ext in the context - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_fax_detect - * - */ - public void faxDetect(String context, String ext) throws ExecuteException { - sendExeMesg("tone_detect", "fax 1100 r +5000 transfer " + ext + " XML " - + context); - } - - /** - * Flushes DTMFs received on a channel. Useful in cases where callers may - * have entered extra digits in one dialog and you want to flush them out - * before sending them into another dialog. - * - * @see #playAndGetDigits - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_flush_dtmf - * - */ - public void flushDTMF() throws ExecuteException { - sendExeMesg("flush_dtmf"); - } - - /** - * Generate TGML tones. - * - * @param tone - * ex: Generate a 500ms beep at 800Hz, tone = "%(500,0,800)" - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_gentones - * - * @see - * http://wiki.freeswitch.org/wiki/TGML - * - */ - public void gentones(String tone) - throws ExecuteException { - gentones(tone, 0); - } - - /** - * Generate TGML tones. - * - * @param tone - * ex: Generate a 500ms beep at 800Hz, tone = "%(500,0,800)" - * @param loops - * set to a non zero nuber, -1 for infinate loop - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_gentones - * - * @see - * http://wiki.freeswitch.org/wiki/TGML - * - */ - public void gentones(String tone, int loops) - throws ExecuteException { - sendExeMesg("gentones", tone - + (loops != 0 ? "|" + loops : "")); - } - - /** - * adds/deletes groups to/from the db(internal db or ODBC) and allows calls - * to these groups in conjunction with the bridge-application. Depends on - * mod_db and mod_dptools. - * - * @param action - * (insert|delete|call ) - * @param groupName - * ex: :01@example.com - * @param url - * ex: sofia/gateway/provider/0123456789 - * @throws ExecuteException - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_group - * - */ - public void group(String action, String groupName, String url) - throws ExecuteException { - sendExeMesg("group", action + ":" + groupName + ":" + url); - } - - /** - * Hangs up a channel, with an optional reason supplied. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_hangup - * - */ - public void hangup() throws ExecuteException { - sendExeMesg("hangup", null); - } - - /** - * Hangs up a channel, with an optional reason supplied. - * - * @param reason - * if not null the hangup reason, ex: USER_BUSY - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_hangup - * - */ - public void hangup(String reason) throws ExecuteException { - sendExeMesg("hangup", reason); - } - - /** - * Dumps channel information to console. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_info - * - */ - public void info() throws ExecuteException { - sendExeMesg("info", null); - } - - /** - * Dumps channel information to console. - * - * @param level - * if not null the level to log. Ex: notice Ex: - * bridge_pre_execute_bleg_app=info - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_info - * - */ - public void info(String level) throws ExecuteException { - sendExeMesg("info", level); - } - - /** - * Allows one channel to bridge itself to the a or b leg of another call. - * The remaining leg of the original call gets hungup - * - * @param bleg - * intercept the b leg of the call - * @throws ExecuteException - */ - public void intercept(String uuid, boolean bleg) throws ExecuteException { - sendExeMesg("intercept", (bleg ? "-bleg " : "") + uuid); - } - - /** - * Logs a string of text to the console - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_log - * - * @see - * http://wiki.freeswitch.org/wiki/Mod_logfile - * - */ - public void log(String message) - throws ExecuteException { - log(null,message); - } - - /** - * Logs a string of text to the console - * - * @param level - * ex: DEBUG, INFO - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_log - * - * @see - * http://wiki.freeswitch.org/wiki/Mod_logfile - * - */ - public void log(String level, String message) - throws ExecuteException { - sendExeMesg("log", (nn(level) ? level + " " : "") - + message); - } - - /** - * Creates a directory. Also creates parent directories by default(When they - * don't exist). - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_mkdir - * - */ - public void mkdir(String path) throws ExecuteException { - sendExeMesg("mkdir", path); - } - - /** - * Places a channel "on hold" in the switch, instead of in the phone. Allows - * for a number of different options, including: Set caller in a place where - * the channel won't be hungup on, while waiting for someone to talk to. - * Generic "hold" mechanism, where you transfer a caller to it. Please note - * that to retrieve a call that has been "parked", you'll have to bridge to - * them or transfer the call to a valid location. Also, remember that - * parking a call does *NOT* supply music on hold or any other media. Park - * is quite literally a way to put a call in limbo until you you - * bridge/uuid_bridge or transfer/uuid_transfer it. For a different means of - * using 'park', see mod_fifo. - * - * @throws ExecuteException - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_park - * - */ - public void park() throws ExecuteException { - sendExeMesg("park"); - } - - /** - * Speak a phrase of text using a predefined phrase macro. (For more - * information on TTS see mod_cepstral and OpenMRCP.) See also the speech - * phrase management page for more information and examples; This command - * relies on the configuration in the phrases section of the freeswitch.xml - * file and including xml files in lang/en/*.xml. Following is a sample of - * phrases management: - * - * @param macroName - * ex: spell, timespec, saydate - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_phrase - * - */ - public void phrase(String macroName, String data) throws ExecuteException { - sendExeMesg("phrase", macroName + "," + data); - } - - /** - * Permits proper answering of multiple simultaneous calls to the same - * pickup group. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_pickup - * - */ - public void pickup(String group) throws ExecuteException { - sendExeMesg("pickup", group); - } - - /** - * Play while doing speech recognition. Result is stored in the - * detect_speech_result channel variable. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_play_and_detect_speech - * - */ - public String playAndDetectSpeech(String file, String engine, - String grammer) throws ExecuteException { - return playAndDetectSpeech(file, engine, null, grammer); - } - - /** - * Play while doing speech recognition. Result is stored in the - * detect_speech_result channel variable. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_play_and_detect_speech - * - */ - public String playAndDetectSpeech(String file, String engine, - String grammer, String params) throws ExecuteException { - CommandResponse resp = sendExeMesg("play_and_detect_speech", - file + " detect:" + engine + " {" + (nn(params) ? params : "") + "}" + grammer); - if (resp.isOk()) { - EslMessage eslMessage = api.sendApiCommand("uuid_getvar", _uuid - + " detect_speech_result"); - if (eslMessage.getBodyLines().size() > 0) - return eslMessage.getBodyLines().get(0); - } else { - throw new ExecuteException(resp.getReplyText()); - } - return null; - } - - /** - * Play a prompt and get digits. - * - * @return collected digits or null if none - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_play_and_get_digits - * - */ - public String playAndGetDigits(int min, int max, int tries, int timeout, - String terminator, String file, String invalidFile, String regexp, - int digitTimeout) throws ExecuteException { - - String id = UUID.randomUUID().toString(); - - CommandResponse resp = sendExeMesg("play_and_get_digits", - String.valueOf(min) - + " " + max - + " " + tries - + " " + timeout - + " " + terminator - + " " + file - + " " + invalidFile - + " " + id - + " " + regexp - + " " + digitTimeout); - - if (resp.isOk()) { - EslMessage eslMessage = api.sendApiCommand("uuid_getvar", _uuid - + " " + id); - if (eslMessage.getBodyLines().size() > 0) - return eslMessage.getBodyLines().get(0); - } else { - throw new ExecuteException(resp.getReplyText()); - } - return null; - } - - /** - * Plays a sound file on the current channel. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_playback - * - */ - public void playback(String file) - throws ExecuteException { - playback(file, null); - } - - /** - * Plays a sound file on the current channel. - * - * @param data - * ex: var1=val1,var2=val2 adds specific vars that will be sent - * in PLAYBACK_START and PLAYBACK_STOP events - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_playback - * - */ - public void playback(String file, String data) - throws ExecuteException { - StringBuilder sb = new StringBuilder(file); - if(nn(data)) { - sb.append(" {"); - sb.append(data); - sb.append("}"); - } - sendExeMesg("playback",sb.toString()); - } - - /** - * Manage the audio being played into a channel from a sound file - * - * @param step - * <+[step]>|<-[step]> - * - * @see http://wiki.freeswitch.org/wiki/Mod_commands#uuid_fileman - */ - public void playbackSpeed(int step) throws ExecuteException { - playbackControl("speed " + step); - } - - /** - * Manage the audio being played into a channel from a sound file - * - * @param step - * <+[step]>|<-[step]> - * - * @see http://wiki.freeswitch.org/wiki/Mod_commands#uuid_fileman - */ - public void playbackVolume(int step) throws ExecuteException { - playbackControl("volume " + step); - } - - /** - * Manage the audio being played into a channel from a sound file - * - * @see http://wiki.freeswitch.org/wiki/Mod_commands#uuid_fileman - */ - public void playbackPause() throws ExecuteException { - playbackControl("pause"); - } - - /** - * Manage the audio being played into a channel from a sound file - * - * @see http://wiki.freeswitch.org/wiki/Mod_commands#uuid_fileman - */ - public void playbackTruncate() throws ExecuteException { - playbackControl("truncate"); - } - - /** - * Manage the audio being played into a channel from a sound file - * - * @see http://wiki.freeswitch.org/wiki/Mod_commands#uuid_fileman - */ - public void playbackRestart() throws ExecuteException { - playbackControl("restart"); - } - - /** - * Manage the audio being played into a channel from a sound file - * - * @param samples - * <+[samples]>|<-[samples]> Samples are the literally the number - * of samples in the file to jump forward or backward. In an 8kHz - * file, 8000 samples would represent one second, in a 16kHz file - * 16000 samples would be one second, etc - * - * @see http://wiki.freeswitch.org/wiki/Mod_commands#uuid_fileman - */ - - public void playbackSeek(int samples) throws ExecuteException { - playbackControl("seek " + samples); - } - - private void playbackControl(String cmd) throws ExecuteException { - api.sendApiCommand("uuid_getvar", _uuid + " " + cmd); - } - - /** - * equivalent to a SIP status code 183 with SDP. (This is the same as cmd - * Progress in Asterisk.) It establishes media (early media) but does not - * answer. You can use this for example to send an in-band error message to - * the caller before disconnecting them (pre_answer, playback, reject with a - * cause code of xxxx). - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_pre_answer - * - */ - public void preAnswer() throws ExecuteException { - sendExeMesg("pre_answer"); - } - - /** - * Sends an event of either type PRESENCE_IN or PRESENCE_OUT. Currently, - * this function is not very useful in conjunction with sofia. This does not - * affect the presence of hook state for use with BLF either, but sending an - * event that expresses the user's hook state does. - * - * @param in - * true if in, false if out - * @param rpid - * ex: dnd, unavailable - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_presence - * - */ - public void presence(String user, boolean in, String rpid, String message) - throws ExecuteException { - - sendExeMesg("presence", in ? "in" : "out" + "|" + user + "|" + rpid + "|" + message); - } - - /** - * Set caller privacy on calls. - * - * @param type - * ex: no, yes, name, full, member - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_privacy - * - */ - public void privacy(String type) throws ExecuteException { - sendExeMesg("privacy", type); - } - - /** - * Send DTMF digits after a bridge is successful from the session using the - * method(s) configured on the endpoint in use. use the character w for a .5 - * second delay and the character W for a 1 second delay. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_queue_dtmf - * - */ - public void queueDTMF(String digits) throws ExecuteException { - queueDTMF(digits, 0); - } - - /** - * Send DTMF digits after a bridge is successful from the session using the - * method(s) configured on the endpoint in use. use the character w for a .5 - * second delay and the character W for a 1 second delay. - * - * @param durationsMillis - * ignored if <=0 - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_queue_dtmf - * - */ - public void queueDTMF(String digits, int durationsMillis) - throws ExecuteException { - sendExeMesg("dtmf_digits", digits - + (durationsMillis > 0 ? "@" + durationsMillis : "")); - } - - /** - * Read DTMF (touch-tone) digits. - * - * @param min - * Minimum number of digits to fetch. - * @param max - * Maximum number of digits to fetch. - * @param soundFile - * Sound file to play before digits are fetched. - * @param timeout - * Number of milliseconds to wait on each digit - * @param terminators - * Digits used to end input if less than digits have been - * pressed. (Typically '#') - * - * @return read string or null - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_read - * - */ - public String read(int min, int max, String soundFile, long timeout, - String terminators) throws ExecuteException { - - String id = UUID.randomUUID().toString(); - - CommandResponse resp = sendExeMesg("read", - String.valueOf(min) + " " + max + " " + soundFile + " " + id + " " + timeout + " " + terminators); - - if (resp.isOk()) { - EslMessage eslMessage = api.sendApiCommand("uuid_getvar", _uuid - + " " + id); - if (eslMessage.getBodyLines().size() > 0) - return eslMessage.getBodyLines().get(0); - } else { - throw new ExecuteException(resp.getReplyText()); - } - return null; - } - - /** - * Record is used for recording messages, like in a voicemail system. This - * application will record a file to file - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_record - * - */ - public void record(String file) throws ExecuteException { - record("record", null, 0, 0, 0, true, false, null, null, null, null, - null, null, 0); - } - - - /** - * Record is used for recording messages, like in a voicemail system. This - * application will record a file to file - * - * @param timeLimitSeconds - * the maximum duration of the recording. - * @param silenceThreshold - * is the energy level. - * @param wateResources - * By default record doesn't send RTP packets. This is generally - * acceptable, but for longer recordings or depending on the RTP - * timer of your gateway, your channel may hangup with cause - * MEDIA_TIMEOUT. Setting this variable will 'waste' bandwidth by - * sending RTP even during recording. The value can be - * true/false/. By default the silence - * factor is 1400 if true - * @param silenceHits - * how many positive hits on being below that thresh you can - * tolerate to stop default hits are sample rate * 3 / the number - * of samples per frame so the default, if missing, is 3. - * @param append - * append or overwite if file exists - * @param recordTile - * store in the file header meta data (provided the file format - * supports meta headers). - * @param recordCopyright - * store in the file header meta data (provided the file format - * supports meta headers). - * @param recordSoftware - * store in the file header meta data (provided the file format - * supports meta headers). - * @param recordArtist - * store in the file header meta data (provided the file format - * supports meta headers). - * @param recordComment - * store in the file header meta data (provided the file format - * supports meta headers). - * @param recordDate - * store in the file header meta data (provided the file format - * supports meta headers). - * @param recordRate - * the sample rate of the recording. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_record - * - */ - public void record(String file, boolean append, boolean wateResources, - int timeLimitSeconds, int silenceThreshold, int silenceHits, - String recordTile, String recordCopyright, String recordSoftware, - String recordArtist, String recordComment, String recordDate, - int recordRate) throws ExecuteException { - record("record", file, timeLimitSeconds, silenceThreshold, silenceHits, - wateResources, append, recordTile, recordCopyright, - recordSoftware, recordArtist, recordComment, recordDate, - recordRate); - } - - /** - * Records an entire phone call or session. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_record - * - */ - public void recordSession(String file) throws ExecuteException { - record("record_session", null, 0, 0, 0, true, false, null, null, null, null, - null, null, 0); - } - - - /** - * Records an entire phone call or session. - * - * @param timeLimitSeconds - * the maximum duration of the recording. - * @param silenceThreshold - * is the energy level. - * @param wateResources - * By default record doesn't send RTP packets. This is generally - * acceptable, but for longer recordings or depending on the RTP - * timer of your gateway, your channel may hangup with cause - * MEDIA_TIMEOUT. Setting this variable will 'waste' bandwidth by - * sending RTP even during recording. The value can be - * true/false/. By default the silence - * factor is 1400 if true - * @param silenceHits - * how many positive hits on being below that thresh you can - * tolerate to stop default hits are sample rate * 3 / the number - * of samples per frame so the default, if missing, is 3. - * - * @param append - * append or overwite if file exists - * @param recordTile - * store in the file header meta data (provided the file format - * supports meta headers). - * @param recordCopyright - * store in the file header meta data (provided the file format - * supports meta headers). - * @param recordSoftware - * store in the file header meta data (provided the file format - * supports meta headers). - * @param recordArtist - * store in the file header meta data (provided the file format - * supports meta headers). - * @param recordComment - * store in the file header meta data (provided the file format - * supports meta headers). - * @param recordDate - * store in the file header meta data (provided the file format - * supports meta headers). - * @param recordRate - * the sample rate of the recording. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_record - * - */ - public void recordSession(String file, boolean append, boolean wateResources, - int timeLimitSeconds, int silenceThreshold, int silenceHits, - String recordTile, String recordCopyright, String recordSoftware, - String recordArtist, String recordComment, String recordDate, - int recordRate) throws ExecuteException { - record("record_session", file, timeLimitSeconds, silenceThreshold, silenceHits, - wateResources, append, recordTile, recordCopyright, - recordSoftware, recordArtist, recordComment, recordDate, - recordRate); - } - - private void record(String action, String file, int optionalTimeLimitSeconds, - int optionalSilenceThreshold, int optionalSilenceHits, - boolean wateResources, boolean append, String optionalRecordTile, - String optionalRecordCopyright, String optionalRecordSoftware, - String optionalRecordArtist, String optionalRecordComment, - String optionalRecordDate, int optionalRecordRate) - throws ExecuteException { - - if (nn(optionalRecordTile)) - set("RECORD_TITLE", optionalRecordTile); - if (nn(optionalRecordCopyright)) - set("RECORD_COPYRIGHT", optionalRecordCopyright); - if (nn(optionalRecordSoftware)) - set("RECORD_SOFTWARE", optionalRecordSoftware); - if (nn(optionalRecordArtist)) - set("RECORD_ARTIST", optionalRecordArtist); - if (nn(optionalRecordComment)) - set("RECORD_COMMENT", optionalRecordComment); - if (nn(optionalRecordDate)) - set("RECORD_DATE", optionalRecordDate); - if (optionalRecordRate > 0) - set("record_sample_rate", String.valueOf(optionalRecordRate)); - - set("RECORD_APPEND", String.valueOf(append)); - set("record_waste_resources", String.valueOf(wateResources)); - - StringBuilder sb = new StringBuilder(file); - if (optionalTimeLimitSeconds > 0) { - sb.append(" ").append(optionalTimeLimitSeconds); - if (optionalSilenceThreshold > 0) { - sb.append(" ").append(optionalSilenceThreshold); - if (optionalSilenceHits > 0) { - sb.append(" ").append(optionalSilenceHits); - } - } - } - - sendExeMesg(action, sb.toString()); - } - - /** - * Can redirect a channel to another endpoint, you must take care to not - * redirect incompatible channels, as that wont have the desired effect. Ie - * if you redirect to a SIP url it should be a SIP channel. By providing a - * single SIP URI FreeSWITCH will issue a 302 "Moved Temporarily": - * - * @param endpoint - * ex:"sip:foo@bar.com " or "sip:foo@bar.com,sip:foo@end.com" - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_redirect - * - */ - public void redirect(String endpoint) throws ExecuteException { - sendExeMesg("redirect", endpoint); - } - - /** - * Send SIP session respond code to the SIP device. - * - * @param code - * ex: "407" or "480 Try again later" - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_respond - * - */ - public void respond(String code) throws ExecuteException { - sendExeMesg("respond", code); - } - - /** - * This causes an 180 Ringing to be sent to the originator. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_ring_ready - * - */ - public void ringReady() throws ExecuteException { - sendExeMesg("ring_ready"); - } - - /** - * The say application will use the pre-recorded sound files to read or say - * various things like dates, times, digits, etc. The say application can - * read digits and numbers as well as dollar amounts, date/time values and - * IP addresses. It can also spell out alpha-numeric text, including - * punctuation marks. There's a transcript of the pre-recorded files in the - * sources under docs/phrase/phrase_en.xml - * - * @param moduleName - * Module name is usually the channel language, e.g. "en" or "es" - * @param sayType - * Say type is one of the following NUMBER ITEMS PERSONS MESSAGES - * CURRENCY TIME_MEASUREMENT CURRENT_DATE CURRENT_TIME - * CURRENT_DATE_TIME TELEPHONE_NUMBER TELEPHONE_EXTENSION URL - * IP_ADDRESS EMAIL_ADDRESS POSTAL_ADDRESS ACCOUNT_NUMBER - * NAME_SPELLED NAME_PHONETIC SHORT_DATE_TIME - * @param sayMethod - * Say method is one of the following N/A PRONOUNCED ITERATED - * COUNTED - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_say - * - */ - public void say(String moduleName, String text, String sayType, - String sayMethod) throws ExecuteException { - - say(moduleName, text, sayType, sayMethod, null); - } - - /** - * The say application will use the pre-recorded sound files to read or say - * various things like dates, times, digits, etc. The say application can - * read digits and numbers as well as dollar amounts, date/time values and - * IP addresses. It can also spell out alpha-numeric text, including - * punctuation marks. There's a transcript of the pre-recorded files in the - * sources under docs/phrase/phrase_en.xml - * - * @param moduleName - * Module name is usually the channel language, e.g. "en" or "es" - * @param sayType - * Say type is one of the following NUMBER ITEMS PERSONS MESSAGES - * CURRENCY TIME_MEASUREMENT CURRENT_DATE CURRENT_TIME - * CURRENT_DATE_TIME TELEPHONE_NUMBER TELEPHONE_EXTENSION URL - * IP_ADDRESS EMAIL_ADDRESS POSTAL_ADDRESS ACCOUNT_NUMBER - * NAME_SPELLED NAME_PHONETIC SHORT_DATE_TIME - * @param sayMethod - * Say method is one of the following N/A PRONOUNCED ITERATED - * COUNTED - * @param gender - * Say gender is one of the following (For languages with - * gender-specific grammar, like French and German) FEMININE - * MASCULINE NEUTER - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_say - * - */ - public void say(String moduleName, String text, String sayType, - String sayMethod, String gender) throws ExecuteException { - - StringBuilder sb = new StringBuilder(moduleName); - sb.append(" ").append(sayType); - sb.append(" ").append(sayMethod); - if (nn(gender)) - sb.append(" ").append(gender); - sb.append(" ").append(text); - - sendExeMesg("say", sb.toString()); - } - - /** - * Schedule future broadcast. - * - * @param seconds - * the epoc time in the future, or the number of seconds in the - * future - * @param interval - * is the param seconds an epoc time or interval - * @param path - * ex: /tmp/howdy.wav - * @param leg - * can be aleg,bleg,both - */ - public void schedBroadcast(long seconds, boolean interval, String path, - String leg) throws ExecuteException { - StringBuilder sb = new StringBuilder(); - if (interval) - sb.append('+'); - sb.append(seconds); - sb.append(" ").append(path); - sb.append(" ").append(leg); - sendExeMesg("sched_broadcast", sb.toString()); - } - - /** - * The sched_hangup application allows you to schedule a hangup action for a - * call, basically to limit call duration. - * - * @param seconds - * the epoc time in the future, or the number of seconds in the - * future - * @param interval - * is the param seconds an epoc time or interval - */ - public void schedHangup(long seconds, boolean interval) - throws ExecuteException { - schedHangup(seconds, interval, null); - } - - - /** - * The sched_hangup application allows you to schedule a hangup action for a - * call, basically to limit call duration. - * - * @param seconds - * the epoc time in the future, or the number of seconds in the - * future - * @param interval - * is the param seconds an epoc time or interval - * @param cause - * ex:allotted_timeout - */ - public void schedHangup(long seconds, boolean interval, String cause) - throws ExecuteException { - StringBuilder sb = new StringBuilder(); - if (interval) - sb.append('+'); - sb.append(seconds); - if (nn(cause)) - sb.append(" ").append(cause); - sendExeMesg("sched_hangup", sb.toString()); - } - - /** - * Schedule a transfer in the future. - * - * @param seconds - * the epoc time in the future, or the number of seconds in the - * future - * @param interval - * is the param seconds an epoc time or interval - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_sched_transfer - * - */ - public void schedTransfer(long seconds, boolean interval, String extension) - throws ExecuteException { - schedTransfer(seconds, interval, extension, null, null); - } - - /** - * Schedule a transfer in the future. - * - * @param seconds - * the epoc time in the future, or the number of seconds in the - * future - * @param interval - * is the param seconds an epoc time or interval - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_sched_transfer - * - */ - public void schedTransfer(long seconds, boolean interval, String extension, - String dialPlan) throws ExecuteException { - schedTransfer(seconds, interval, extension, dialPlan, null); - } - - /** - * Schedule a transfer in the future. - * - * @param seconds - * the epoc time in the future, or the number of seconds in the - * future - * @param interval - * is the param seconds an epoc time or interval - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_sched_transfer - * - */ - public void schedTransfer(long seconds, boolean interval, String extension, - String dialPlan, String context) throws ExecuteException { - StringBuilder sb = new StringBuilder(); - if (interval) - sb.append('+'); - sb.append(seconds); - sb.append(" ").append(extension); - if (nn(dialPlan)) { - sb.append(" ").append(dialPlan); - if (nn(context)) { - sb.append(" ").append(context); - } - } - sendExeMesg("sched_transfer", sb.toString()); - } - - /** - * Send DTMF digits from the session using the method(s) configured on the - * endpoint in use. Use the character w for a .5 second delay and the - * character W for a 1 second delay. - */ - public void sendDTMF(String digits)throws ExecuteException { - sendDTMF(digits, 0); - } - - /** - * Send DTMF digits from the session using the method(s) configured on the - * endpoint in use. Use the character w for a .5 second delay and the - * character W for a 1 second delay. - */ - public void sendDTMF(String digits, int durationMillis) - throws ExecuteException { - StringBuilder sb = new StringBuilder(digits); - if (durationMillis > 0) - sb.append('@').append(durationMillis); - - sendExeMesg("send_dtmf", sb.toString()); - } - - /** - * Set a channel variable for the channel calling the application. - * - * @param key - * channel_variable name - * @param value - * channel_variable value - */ - public void set(String key, String value) throws ExecuteException { - sendExeMesg("set", key + "=" + value); - } - - public void speak(String engine, String voice, String message) throws ExecuteException { - sendExeMesg("speak", engine + "|" + voice + "|" + message); - } - - /** - * Immediately transfer the calling channel to a new context. If there - * happens to be an xml extension named then control is - * "warped" directly to that extension. Otherwise it goes through the entire - * context checking for a match. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_transfer - * - */ - public void transfer(String destinationNumber) throws ExecuteException { - transfer(destinationNumber, null, null); - } - - /** - * Immediately transfer the calling channel to a new context. If there - * happens to be an xml extension named then control is - * "warped" directly to that extension. Otherwise it goes through the entire - * context checking for a match. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_transfer - * - */ - public void transfer(String destinationNumber, String dialplan) - throws ExecuteException { - transfer(destinationNumber, dialplan, null); - } - - /** - * Immediately transfer the calling channel to a new context. If there - * happens to be an xml extension named then control is - * "warped" directly to that extension. Otherwise it goes through the entire - * context checking for a match. - * - * @see - * http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_transfer - * - */ - public void transfer(String destinationNumber, String dialplan, - String context) throws ExecuteException { - StringBuilder sb = new StringBuilder(destinationNumber); - if (nn(dialplan)) { - sb.append(" ").append(dialplan); - if (nn(context)) - sb.append(" ").append(context); - } - sendExeMesg("transfer", sb.toString()); - } - - public String ApiCommand(String command, String args) { - EslMessage eslMessage = api.sendApiCommand(command, args); - StringBuilder sb = new StringBuilder(); - for(String line : eslMessage.getBodyLines()) - sb.append(line); - return sb.toString(); - } - - private CommandResponse sendExeMesg(String app) throws ExecuteException { - return sendExeMesg(app, null); - } - - private CommandResponse sendExeMesg(String app, String args) - throws ExecuteException { - SendMsg msg = new SendMsg(); - msg.addCallCommand("execute"); - msg.addExecuteAppName(app); - if (nn(args)) - msg.addExecuteAppArg(args); - CommandResponse resp = api.sendMessage(msg); - if (!resp.isOk()) - throw new ExecuteException(resp.getReplyText()); - else - return resp; - } - - - private boolean nn(Object obj) {return obj != null;} - -} diff --git a/src/main/java/org/freeswitch/esl/client/dptools/ExecuteException.java b/src/main/java/org/freeswitch/esl/client/dptools/ExecuteException.java deleted file mode 100644 index c08c9fe..0000000 --- a/src/main/java/org/freeswitch/esl/client/dptools/ExecuteException.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.freeswitch.esl.client.dptools; - -public class ExecuteException extends Exception { - - private static final long serialVersionUID = 1L; - - public ExecuteException() { - } - - public ExecuteException(String message) { - super(message); - } - - public ExecuteException(Throwable cause) { - super(cause); - } - - public ExecuteException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/main/java/org/freeswitch/esl/client/inbound/Client.java b/src/main/java/org/freeswitch/esl/client/inbound/Client.java index 229195b..5fabd57 100644 --- a/src/main/java/org/freeswitch/esl/client/inbound/Client.java +++ b/src/main/java/org/freeswitch/esl/client/inbound/Client.java @@ -1,342 +1,502 @@ -/* - * 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 com.google.common.base.Throwables; -import io.netty.bootstrap.Bootstrap; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelOption; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.nio.NioSocketChannel; -import org.freeswitch.esl.client.internal.Context; -import org.freeswitch.esl.client.internal.IModEslApi; -import org.freeswitch.esl.client.transport.CommandResponse; -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.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.SocketAddress; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Entry point to connect to a running FreeSWITCH Event Socket Library module, as a client. - *

- * This class provides what the FreeSWITCH documentation refers to as an 'Inbound' connection - * to the Event Socket module. That is, with reference to the socket listening on the FreeSWITCH - * server, this client occurs as an inbound connection to the server. - *

- * See http://wiki.freeswitch.org/wiki/Mod_event_socket - */ -public class Client implements IModEslApi { - - private final Logger log = LoggerFactory.getLogger(this.getClass()); - private final List eventListeners = new CopyOnWriteArrayList<>(); - private final AtomicBoolean authenticatorResponded = new AtomicBoolean(false); - private final ConcurrentHashMap> backgroundJobs = - new ConcurrentHashMap<>(); - - private boolean authenticated; - private CommandResponse authenticationResponse; - private Optional clientContext = Optional.empty(); - private ExecutorService callbackExecutor = Executors.newSingleThreadExecutor(); - - public void addEventListener(IEslEventListener listener) { - if (listener != null) { - eventListeners.add(listener); - } - } - - @Override - public boolean canSend() { - return clientContext.isPresent() - && clientContext.get().canSend() - && authenticated; - } - - private void checkConnected() { - if (!canSend()) { - throw new IllegalStateException("Not connected to FreeSWITCH Event Socket"); - } - } - - public void setCallbackExecutor(ExecutorService callbackExecutor) { - this.callbackExecutor = callbackExecutor; - } - - /** - * Attempt to establish an authenticated connection to the nominated FreeSWITCH ESL server socket. - * This call will block, waiting for an authentication handshake to occur, or timeout after the - * supplied number of seconds. - * - * @param clientAddress a SocketAddress representing the endpoint to connect to - * @param password server event socket is expecting (set in event_socket_conf.xml) - * @param timeoutSeconds number of seconds to wait for the server socket before aborting - */ - public void connect(SocketAddress clientAddress, String password, int timeoutSeconds) throws InboundConnectionFailure { - // If already connected, disconnect first - if (canSend()) { - close(); - } - - log.info("Connecting to {} ...", clientAddress); - - EventLoopGroup workerGroup = new NioEventLoopGroup(); - - // Configure this client - Bootstrap bootstrap = new Bootstrap() - .group(workerGroup) - .channel(NioSocketChannel.class) - .option(ChannelOption.SO_KEEPALIVE, true); - - // Add ESL handler and factory - InboundClientHandler handler = new InboundClientHandler(password, protocolListener); - bootstrap.handler(new InboundChannelInitializer(handler)); - - // Attempt connection - ChannelFuture future = bootstrap.connect(clientAddress); - - // Wait till attempt succeeds, fails or timeouts - if (!future.awaitUninterruptibly(timeoutSeconds, TimeUnit.SECONDS)) { - throw new InboundConnectionFailure("Timeout connecting to " + clientAddress); - } - // Did not timeout - final Channel channel = future.channel(); - // But may have failed anyway - if (!future.isSuccess()) { - log.warn("Failed to connect to [{}]", clientAddress, future.cause()); - - workerGroup.shutdownGracefully(); - - throw new InboundConnectionFailure("Could not connect to " + clientAddress, future.cause()); - } - - log.info("Connected to {}", clientAddress); - - // Wait for the authentication handshake to call back - while (!authenticatorResponded.get()) { - try { - Thread.sleep(250); - } catch (InterruptedException e) { - // ignore - } - } - - this.clientContext = Optional.of(new Context(channel, handler)); - - if (!authenticated) { - throw new InboundConnectionFailure("Authentication failed: " + authenticationResponse.getReplyText()); - } - - log.info("Authenticated"); - } - - /** - * Sends a FreeSWITCH API command to the server and blocks, waiting for an immediate response from the - * server. - *

- * The outcome of the command from the server is retured in an {@link EslMessage} object. - * - * @param command API command to send - * @param arg command arguments - * @return an {@link EslMessage} containing command results - */ - @Override - public EslMessage sendApiCommand(String command, String arg) { - checkConnected(); - return clientContext.get().sendApiCommand(command, arg); - } - - /** - * Submit a FreeSWITCH API command to the server to be executed in background mode. A synchronous - * response from the server provides a UUID to identify the job execution results. When the server - * has completed the job execution it fires a BACKGROUND_JOB Event with the execution results.

- * Note that this Client must be subscribed in the normal way to BACKGOUND_JOB Events, in order to - * receive this event. - * - * @param command API command to send - * @param arg command arguments - * @return String Job-UUID that the server will tag result event with. - */ - @Override - public CompletableFuture sendBackgroundApiCommand(String command, String arg) { - checkConnected(); - return clientContext.get().sendBackgroundApiCommand(command, arg); - } - - /** - * Set the current event subscription for this connection to the server. Examples of the events - * argument are: - *

-	 *   ALL
-	 *   CHANNEL_CREATE CHANNEL_DESTROY HEARTBEAT
-	 *   CUSTOM conference::maintenance
-	 *   CHANNEL_CREATE CHANNEL_DESTROY CUSTOM conference::maintenance sofia::register sofia::expire
-	 * 
- * Subsequent calls to this method replaces any previous subscriptions that were set. - *

- * Note: current implementation can only process 'plain' events. - * - * @param format can be { plain | xml } - * @param events { all | space separated list of events } - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse setEventSubscriptions(EventFormat format, String events) { - checkConnected(); - return clientContext.get().setEventSubscriptions(format, events); - } - - /** - * Cancel any existing event subscription. - * - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse cancelEventSubscriptions() { - checkConnected(); - return clientContext.get().cancelEventSubscriptions(); - } - - /** - * Add an event filter to the current set of event filters on this connection. Any of the event headers - * can be used as a filter. - *

- * Note that event filters follow 'filter-in' semantics. That is, when a filter is applied - * only the filtered values will be received. Multiple filters can be added to the current - * connection. - *

- * Example filters: - *
-	 *    eventHeader        valueToFilter
-	 *    ----------------------------------
-	 *    Event-Name         CHANNEL_EXECUTE
-	 *    Channel-State      CS_NEW
-	 * 
- * - * @param eventHeader to filter on - * @param valueToFilter the value to match - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse addEventFilter(String eventHeader, String valueToFilter) { - checkConnected(); - return clientContext.get().addEventFilter(eventHeader, valueToFilter); - } - - /** - * Delete an event filter from the current set of event filters on this connection. See - * - * @param eventHeader to remove - * @param valueToFilter to remove - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse deleteEventFilter(String eventHeader, String valueToFilter) { - checkConnected(); - return clientContext.get().deleteEventFilter(eventHeader, valueToFilter); - } - - /** - * Send a {@link SendMsg} command to FreeSWITCH. This client requires that the {@link SendMsg} - * has a call UUID parameter. - * - * @param sendMsg a {@link SendMsg} with call UUID - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse sendMessage(SendMsg sendMsg) { - checkConnected(); - return clientContext.get().sendMessage(sendMsg); - } - - /** - * Enable log output. - * - * @param level using the same values as in console.conf - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse setLoggingLevel(LoggingLevel level) { - checkConnected(); - return clientContext.get().setLoggingLevel(level); - } - - /** - * Disable any logging previously enabled with setLogLevel(). - * - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse cancelLogging() { - checkConnected(); - return clientContext.get().cancelLogging(); - } - - /** - * Close the socket connection - * - * @return a {@link CommandResponse} with the server's response. - */ - public CommandResponse close() { - checkConnected(); - - try { - if (clientContext.isPresent()) { - return new CommandResponse("exit", clientContext.get().sendCommand("exit")); - } else { - throw new IllegalStateException("not connected/authenticated"); - } - } catch (Throwable t) { - throw Throwables.propagate(t); - } - - - } - - /* - * Internal observer of the ESL protocol - */ - private final IEslProtocolListener protocolListener = new IEslProtocolListener() { - - @Override - public void authResponseReceived(CommandResponse response) { - authenticatorResponded.set(true); - authenticated = response.isOk(); - authenticationResponse = response; - log.debug("Auth response success={}, message=[{}]", authenticated, response.getReplyText()); - } - - @Override - public void eventReceived(final Context ctx, final EslEvent event) { - log.debug("Event received [{}]", event); - for (final IEslEventListener listener : eventListeners) { - callbackExecutor.execute(() -> listener.onEslEvent(ctx, event)); - } - } - - @Override - public void disconnected() { - log.info("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; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.freeswitch.esl.client.IEslEventListener; +import org.freeswitch.esl.client.internal.IEslProtocolListener; +import org.freeswitch.esl.client.transport.CommandResponse; +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.jboss.netty.bootstrap.ClientBootstrap; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Entry point to connect to a running FreeSWITCH Event Socket Library module, as a client. + *

+ * This class provides what the FreeSWITCH documentation refers to as an 'Inbound' connection + * to the Event Socket module. That is, with reference to the socket listening on the FreeSWITCH + * server, this client occurs as an inbound connection to the server. + *

+ * See http://wiki.freeswitch.org/wiki/Mod_event_socket + * + * @author david varnes + */ +public class Client { + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final List eventListeners = new CopyOnWriteArrayList(); + + private final ThreadFactory eslEventNotifierThreadFactory = new ThreadFactory() { + AtomicInteger threadNumber = new AtomicInteger(1); + + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "EslEventNotifier-" + threadNumber.getAndIncrement()); + } + }; + + private final Executor eventListenerExecutor = new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(1024), eslEventNotifierThreadFactory, new ThreadPoolExecutor.AbortPolicy()); + + + private final ThreadFactory eslBackgroundJobNotifierThreadFactory = new ThreadFactory() { + AtomicInteger threadNumber = new AtomicInteger(1); + + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "EslBackgroundJobNotifier-" + threadNumber.getAndIncrement()); + } + }; + + private final Executor backgroundJobListenerExecutor = new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(1024), eslBackgroundJobNotifierThreadFactory, new ThreadPoolExecutor.AbortPolicy()); + + private AtomicBoolean authenticatorResponded = new AtomicBoolean(false); + private boolean authenticated; + private CommandResponse authenticationResponse; + private Channel channel; + + private static final ThreadFactory bossThreadFactory = new ThreadFactoryBuilder().setNameFormat("boss-pool-%d").build(); + private static final ThreadFactory workerThreadFactory = new ThreadFactoryBuilder().setNameFormat("worker-pool-%d").build(); + + private static ExecutorService bossPool = null; + private static ExecutorService workerPool = null; + private static NioClientSocketChannelFactory nioClientSocketChannelFactory = null; + + private int bossMaxNumber = 1; + private int workerMinNumber = Runtime.getRuntime().availableProcessors() * 2; + private int workerMaxNumber = 16; + + + public Client(int bossMaxNum, int workerMaxNum) { + if (bossMaxNum > 0) { + bossMaxNumber = bossMaxNum; + } + if (workerMaxNum > workerMinNumber) { + workerMaxNumber = workerMaxNum; + } + + } + + + public boolean canSend() { + return channel != null && channel.isConnected() && authenticated; + } + + public void addEventListener(IEslEventListener listener) { + if (listener != null) { + eventListeners.add(listener); + } + } + + /** + * Attempt to establish an authenticated connection to the nominated FreeSWITCH ESL server socket. + * This call will block, waiting for an authentication handshake to occur, or timeout after the + * supplied number of seconds. + * + * @param host can be either ip address or hostname + * @param port tcp port that server socket is listening on (set in event_socket_conf.xml) + * @param password server event socket is expecting (set in event_socket_conf.xml) + * @param timeoutSeconds number of seconds to wait for the server socket before aborting + */ + public void connect(String host, int port, String password, int timeoutSeconds) throws InboundConnectionFailure { + // If already connected, disconnect first + if (canSend()) { + close(); + } else { + //canSend()=false but channel is still opened or connected + closeChannel(); + } + + if (nioClientSocketChannelFactory != null) { + nioClientSocketChannelFactory.releaseExternalResources(); + } + + bossPool = new ThreadPoolExecutor(1, bossMaxNumber, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(128), bossThreadFactory, new ThreadPoolExecutor.AbortPolicy()); + + workerPool = new ThreadPoolExecutor(workerMinNumber, workerMaxNumber, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(2048), workerThreadFactory, new ThreadPoolExecutor.AbortPolicy()); + + nioClientSocketChannelFactory = new NioClientSocketChannelFactory(bossPool, workerPool); + + // Configure this client + ClientBootstrap bootstrap = new ClientBootstrap(nioClientSocketChannelFactory); + + // Add ESL handler and factory + InboundClientHandler handler = new InboundClientHandler(password, protocolListener); + bootstrap.setPipelineFactory(new InboundPipelineFactory(handler)); + + // Attempt connection + ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port)); + + // Wait till attempt succeeds, fails or timeouts + if (!future.awaitUninterruptibly(timeoutSeconds, TimeUnit.SECONDS)) { + throw new InboundConnectionFailure("Timeout connecting to " + host + ":" + port); + } + // Did not timeout + channel = future.getChannel(); + // But may have failed anyway + if (!future.isSuccess()) { + log.warn("Failed to connect to [{}:{}]", host, port); + log.warn(" * reason: {}", future.getCause()); + + channel = null; + bootstrap.releaseExternalResources(); + + throw new InboundConnectionFailure("Could not connect to " + host + ":" + port, future.getCause()); + } + + // Wait for the authentication handshake to call back + while (!authenticatorResponded.get()) { + try { + Thread.sleep(250); + } catch (InterruptedException e) { + // ignore + } + } + + if (!authenticated) { + throw new InboundConnectionFailure("Authentication failed: " + authenticationResponse.getReplyText()); + } + } + + /** + * Sends a FreeSWITCH API command to the server and blocks, waiting for an immediate response from the + * server. + *

+ * The outcome of the command from the server is retured in an {@link EslMessage} object. + * + * @param command API command to send + * @param arg command arguments + * @return an {@link EslMessage} containing command results + */ + public EslMessage sendSyncApiCommand(String command, String arg) { + checkConnected(); + InboundClientHandler handler = (InboundClientHandler) channel.getPipeline().getLast(); + StringBuilder sb = new StringBuilder(); + if (command != null && !command.isEmpty()) { + sb.append("api "); + sb.append(command); + } + if (arg != null && !arg.isEmpty()) { + sb.append(' '); + sb.append(arg); + } + + return handler.sendSyncSingleLineCommand(channel, sb.toString()); + } + + /** + * Submit a FreeSWITCH API command to the server to be executed in background mode. A synchronous + * response from the server provides a UUID to identify the job execution results. When the server + * has completed the job execution it fires a BACKGROUND_JOB Event with the execution results.

+ * Note that this Client must be subscribed in the normal way to BACKGOUND_JOB Events, in order to + * receive this event. + * + * @param command API command to send + * @param arg command arguments + * @return String Job-UUID that the server will tag result event with. + */ + public String sendAsyncApiCommand(String command, String arg) { + checkConnected(); + InboundClientHandler handler = (InboundClientHandler) channel.getPipeline().getLast(); + StringBuilder sb = new StringBuilder(); + if (command != null && !command.isEmpty()) { + sb.append("bgapi "); + sb.append(command); + } + if (arg != null && !arg.isEmpty()) { + sb.append(' '); + sb.append(arg); + } + + return handler.sendAsyncCommand(channel, sb.toString()); + } + + /** + * Set the current event subscription for this connection to the server. Examples of the events + * argument are: + *

+     *   ALL
+     *   CHANNEL_CREATE CHANNEL_DESTROY HEARTBEAT
+     *   CUSTOM conference::maintenance
+     *   CHANNEL_CREATE CHANNEL_DESTROY CUSTOM conference::maintenance sofia::register sofia::expire
+     * 
+ * Subsequent calls to this method replaces any previous subscriptions that were set. + *

+ * Note: current implementation can only process 'plain' events. + * + * @param format can be { plain | xml } + * @param events { all | space separated list of events } + * @return a {@link CommandResponse} with the server's response. + */ + public CommandResponse setEventSubscriptions(String format, String events) { + // temporary hack + if (!"plain".equals(format)) { + throw new IllegalStateException("Only 'plain' event format is supported at present"); + } + + checkConnected(); + InboundClientHandler handler = (InboundClientHandler) channel.getPipeline().getLast(); + StringBuilder sb = new StringBuilder(); + if (format != null && !format.isEmpty()) { + sb.append("event "); + sb.append(format); + } + if (events != null && !events.isEmpty()) { + sb.append(' '); + sb.append(events); + } + EslMessage response = handler.sendSyncSingleLineCommand(channel, sb.toString()); + + return new CommandResponse(sb.toString(), response); + } + + /** + * Cancel any existing event subscription. + * + * @return a {@link CommandResponse} with the server's response. + */ + public CommandResponse cancelEventSubscriptions() { + checkConnected(); + InboundClientHandler handler = (InboundClientHandler) channel.getPipeline().getLast(); + EslMessage response = handler.sendSyncSingleLineCommand(channel, "noevents"); + + return new CommandResponse("noevents", response); + } + + /** + * Add an event filter to the current set of event filters on this connection. Any of the event headers + * can be used as a filter. + *

+ * Note that event filters follow 'filter-in' semantics. That is, when a filter is applied + * only the filtered values will be received. Multiple filters can be added to the current + * connection. + *

+ * Example filters: + *
+     *    eventHeader        valueToFilter
+     *    ----------------------------------
+     *    Event-Name         CHANNEL_EXECUTE
+     *    Channel-State      CS_NEW
+     * 
+ * + * @param eventHeader to filter on + * @param valueToFilter the value to match + * @return a {@link CommandResponse} with the server's response. + */ + public CommandResponse addEventFilter(String eventHeader, String valueToFilter) { + checkConnected(); + InboundClientHandler handler = (InboundClientHandler) channel.getPipeline().getLast(); + StringBuilder sb = new StringBuilder(); + if (eventHeader != null && !eventHeader.isEmpty()) { + sb.append("filter "); + sb.append(eventHeader); + } + if (valueToFilter != null && !valueToFilter.isEmpty()) { + sb.append(' '); + sb.append(valueToFilter); + } + EslMessage response = handler.sendSyncSingleLineCommand(channel, sb.toString()); + + return new CommandResponse(sb.toString(), response); + } + + /** + * Delete an event filter from the current set of event filters on this connection. See + * {@link Client#addEventFilter(java.lang.String, java.lang.String)} + * + * @param eventHeader to remove + * @param valueToFilter to remove + * @return a {@link CommandResponse} with the server's response. + */ + public CommandResponse deleteEventFilter(String eventHeader, String valueToFilter) { + checkConnected(); + InboundClientHandler handler = (InboundClientHandler) channel.getPipeline().getLast(); + StringBuilder sb = new StringBuilder(); + if (eventHeader != null && !eventHeader.isEmpty()) { + sb.append("filter delete "); + sb.append(eventHeader); + } + if (valueToFilter != null && !valueToFilter.isEmpty()) { + sb.append(' '); + sb.append(valueToFilter); + } + EslMessage response = handler.sendSyncSingleLineCommand(channel, sb.toString()); + + return new CommandResponse(sb.toString(), response); + } + + /** + * Send a {@link SendMsg} command to FreeSWITCH. This client requires that the {@link SendMsg} + * has a call UUID parameter. + * + * @param sendMsg a {@link SendMsg} with call UUID + * @return a {@link CommandResponse} with the server's response. + */ + public CommandResponse sendMessage(SendMsg sendMsg) { + checkConnected(); + InboundClientHandler handler = (InboundClientHandler) channel.getPipeline().getLast(); + EslMessage response = handler.sendSyncMultiLineCommand(channel, sendMsg.getMsgLines()); + + return new CommandResponse(sendMsg.toString(), response); + } + + /** + * Enable log output. + * + * @param level using the same values as in console.conf + * @return a {@link CommandResponse} with the server's response. + */ + public CommandResponse setLoggingLevel(String level) { + checkConnected(); + InboundClientHandler handler = (InboundClientHandler) channel.getPipeline().getLast(); + StringBuilder sb = new StringBuilder(); + if (level != null && !level.isEmpty()) { + sb.append("log "); + sb.append(level); + } + EslMessage response = handler.sendSyncSingleLineCommand(channel, sb.toString()); + + return new CommandResponse(sb.toString(), response); + } + + /** + * Disable any logging previously enabled with setLogLevel(). + * + * @return a {@link CommandResponse} with the server's response. + */ + public CommandResponse cancelLogging() { + checkConnected(); + InboundClientHandler handler = (InboundClientHandler) channel.getPipeline().getLast(); + EslMessage response = handler.sendSyncSingleLineCommand(channel, "nolog"); + + return new CommandResponse("nolog", response); + } + + /** + * Close the socket connection + * + * @return a {@link CommandResponse} with the server's response. + */ + public CommandResponse close() { + checkConnected(); + InboundClientHandler handler = (InboundClientHandler) channel.getPipeline().getLast(); + EslMessage response = handler.sendSyncSingleLineCommand(channel, "exit"); + return new CommandResponse("exit", response); + } + + /** + * close netty channel + * + * @return + */ + public ChannelFuture closeChannel() { + if (channel != null && channel.isOpen()) { + return channel.close(); + } + return null; + } + + /** + * remove all eslEventlistener + */ + public void removeAllEventListener() { + if (eventListeners != null) { + eventListeners.clear(); + } + } + + /* + * Internal observer of the ESL protocol + */ + private final IEslProtocolListener protocolListener = new IEslProtocolListener() { + @Override + public void authResponseReceived(CommandResponse response) { + authenticatorResponded.set(true); + authenticated = response.isOk(); + authenticationResponse = response; + log.debug("Auth response success={}, message=[{}]", authenticated, response.getReplyText()); + } + + @Override + public void eventReceived(final EslEvent event) { + log.debug("Event received [{}]", event); + /* + * Notify listeners in a different thread in order to: + * - not to block the IO threads with potentially long-running listeners + * - generally be defensive running other people's code + * Use a different worker thread pool for async job results than for event driven + * events to keep the latency as low as possible. + */ + if ("BACKGROUND_JOB".equals(event.getEventName())) { + for (final IEslEventListener listener : eventListeners) { + backgroundJobListenerExecutor.execute(new Runnable() { + @Override + public void run() { + try { + listener.backgroundJobResultReceived(event); + } catch (Throwable t) { + log.error("Error caught notifying listener of job result [" + event + ']', t); + } + } + }); + } + } else { + for (final IEslEventListener listener : eventListeners) { + eventListenerExecutor.execute(new Runnable() { + @Override + public void run() { + try { + listener.eventReceived(event); + } catch (Throwable t) { + log.error("Error caught notifying listener of event [" + event + ']', t); + } + } + }); + } + } + } + + @Override + public void disconnected() { + log.info("Disconnected .."); + } + }; + + private void checkConnected() { + if (!canSend()) { + throw new IllegalStateException("Not connected to FreeSWITCH Event Socket"); + } + } +} diff --git a/src/main/java/org/freeswitch/esl/client/inbound/InboundChannelInitializer.java b/src/main/java/org/freeswitch/esl/client/inbound/InboundChannelInitializer.java deleted file mode 100644 index ea70595..0000000 --- a/src/main/java/org/freeswitch/esl/client/inbound/InboundChannelInitializer.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.freeswitch.esl.client.inbound; - -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.string.StringEncoder; -import org.freeswitch.esl.client.transport.message.EslFrameDecoder; - -/** - * End users of the {@link Client} should not need to use this class. - *

- * Convenience factory to assemble a Netty processing pipeline for inbound clients. - */ -class InboundChannelInitializer extends ChannelInitializer { - - private final ChannelHandler handler; - - public InboundChannelInitializer(ChannelHandler handler) { - this.handler = handler; - } - - @Override - public void initChannel(SocketChannel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast("decoder", new EslFrameDecoder(8192)); - - // now the inbound client logic - pipeline.addLast("clientHandler", handler); - pipeline.addLast("encoder", new StringEncoder()); - } -} diff --git a/src/main/java/org/freeswitch/esl/client/inbound/InboundClientHandler.java b/src/main/java/org/freeswitch/esl/client/inbound/InboundClientHandler.java index 01312b4..5bd8326 100644 --- a/src/main/java/org/freeswitch/esl/client/inbound/InboundClientHandler.java +++ b/src/main/java/org/freeswitch/esl/client/inbound/InboundClientHandler.java @@ -1,80 +1,88 @@ -/* - * 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 io.netty.channel.ChannelHandlerContext; -import org.freeswitch.esl.client.internal.AbstractEslClientHandler; -import org.freeswitch.esl.client.internal.Context; -import org.freeswitch.esl.client.transport.CommandResponse; -import org.freeswitch.esl.client.transport.event.EslEvent; -import org.freeswitch.esl.client.transport.message.EslHeaders; - -/** - * End users of the inbound {@link Client} should not need to use this class. - *

- * Specialised {@link AbstractEslClientHandler} that implements the connection logic for an - * 'Inbound' FreeSWITCH Event Socket connection. The responsibilities for this class are: - *

  • - * To handle the auth request that the FreeSWITCH server will send immediately following a new - * connection when mode is Inbound. - *
  • - * To signal the observing {@link IEslProtocolListener} (expected to be the Inbound client - * implementation) when ESL events are received. - *
- * 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). - */ -class InboundClientHandler extends AbstractEslClientHandler { - - private final String password; - private final IEslProtocolListener listener; - - public InboundClientHandler(String password, IEslProtocolListener listener) { - this.password = password; - this.listener = listener; - } - - @Override - protected void handleEslEvent(ChannelHandlerContext ctx, EslEvent event) { - log.debug("Received event: [{}]", event); - listener.eventReceived(new Context(ctx.channel(), this), event); - } - - @Override - protected void handleAuthRequest(ChannelHandlerContext ctx) { - log.debug("Auth requested, sending [auth {}]", "*****"); - - sendApiSingleLineCommand(ctx.channel(), "auth " + password) - .thenAccept(response -> { - log.debug("Auth response [{}]", response); - if (response.getContentType().equals(EslHeaders.Value.COMMAND_REPLY)) { - final 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; + +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.EslHeaders.Value; +import org.freeswitch.esl.client.transport.message.EslMessage; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.execution.ExecutionHandler; + +/** + * End users of the inbound {@link Client} should not need to use this class. + *

+ * Specialised {@link AbstractEslClientHandler} that implements the connection logic for an + * 'Inbound' FreeSWITCH Event Socket connection. The responsibilities for this class are: + *

  • + * To handle the auth request that the FreeSWITCH server will send immediately following a new + * connection when mode is Inbound. + *
  • + * To signal the observing {@link IEslProtocolListener} (expected to be the Inbound client + * implementation) when ESL events are received. + *
+ * 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 class InboundClientHandler extends AbstractEslClientHandler +{ + private final String password; + private final IEslProtocolListener listener; + + public InboundClientHandler( String password, IEslProtocolListener listener ) + { + this.password = password; + this.listener = listener; + } + + @Override + protected void handleEslEvent(ChannelHandlerContext ctx, EslEvent event ) + { + log.debug( "Received event: [{}]", event ); + listener.eventReceived( event ); + } + + @Override + 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(); + } + +} diff --git a/src/main/java/org/freeswitch/esl/client/inbound/InboundConnectionFailure.java b/src/main/java/org/freeswitch/esl/client/inbound/InboundConnectionFailure.java index 8759af1..c037d88 100644 --- a/src/main/java/org/freeswitch/esl/client/inbound/InboundConnectionFailure.java +++ b/src/main/java/org/freeswitch/esl/client/inbound/InboundConnectionFailure.java @@ -17,15 +17,20 @@ /** * Checked exception to handle connection failures. + * + * @author david varnes */ -public class InboundConnectionFailure extends Exception { - private static final long serialVersionUID = 1L; +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); - } + public InboundConnectionFailure( String message ) + { + super( message ); + } + + public InboundConnectionFailure( String message, Throwable cause ) + { + super( message, cause ); + } } diff --git a/src/main/java/org/freeswitch/esl/client/inbound/InboundPipelineFactory.java b/src/main/java/org/freeswitch/esl/client/inbound/InboundPipelineFactory.java new file mode 100644 index 0000000..11e4a68 --- /dev/null +++ b/src/main/java/org/freeswitch/esl/client/inbound/InboundPipelineFactory.java @@ -0,0 +1,58 @@ +/* + * 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.EslFrameDecoder; +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.string.StringEncoder; +import org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor; + +/** + * End users of the {@link Client} should not need to use this class. + *

+ * Convenience factory to assemble a Netty processing pipeline for inbound clients. + * + * @author david varnes + */ +public class InboundPipelineFactory implements ChannelPipelineFactory +{ + private final ChannelHandler handler; + + public InboundPipelineFactory( ChannelHandler handler ) + { + this.handler = handler; + } + + @Override + public ChannelPipeline getPipeline() throws Exception + { + ChannelPipeline pipeline = Channels.pipeline(); + pipeline.addLast( "encoder", new StringEncoder() ); + pipeline.addLast( "decoder", new EslFrameDecoder( 8192 ) ); + // 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; + } +} diff --git a/src/main/java/org/freeswitch/esl/client/internal/AbstractEslClientHandler.java b/src/main/java/org/freeswitch/esl/client/internal/AbstractEslClientHandler.java index a827210..ecf5bda 100644 --- a/src/main/java/org/freeswitch/esl/client/internal/AbstractEslClientHandler.java +++ b/src/main/java/org/freeswitch/esl/client/internal/AbstractEslClientHandler.java @@ -1,242 +1,253 @@ -/* - * 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 io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import org.freeswitch.esl.client.transport.event.EslEvent; -import org.freeswitch.esl.client.transport.event.EslEventHeaderNames; -import org.freeswitch.esl.client.transport.message.EslHeaders.Name; -import org.freeswitch.esl.client.transport.message.EslHeaders.Value; -import org.freeswitch.esl.client.transport.message.EslMessage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.concurrent.*; -import java.util.concurrent.locks.ReentrantLock; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Strings.isNullOrEmpty; - -/** - * Specialised {@link SimpleChannelInboundHandler} 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: - *

  • - * 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. - *
  • - * Concrete sub classes are expected to 'terminate' the Netty IO processing pipeline (ie be the 'last' - * handler). - *
- * 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). - */ -public abstract class AbstractEslClientHandler extends SimpleChannelInboundHandler { - - public static final String MESSAGE_TERMINATOR = "\n\n"; - public static final String LINE_TERMINATOR = "\n"; - - protected final Logger log = LoggerFactory.getLogger(this.getClass()); - // used to preserve association between adding future to queue and sending message on channel - private final ReentrantLock syncLock = new ReentrantLock(); - private final ConcurrentLinkedQueue> apiCalls = - new ConcurrentLinkedQueue<>(); - - private final ConcurrentHashMap> backgroundJobs = - new ConcurrentHashMap<>(); - private final ExecutorService backgroundJobExecutor = Executors.newCachedThreadPool(); - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) throws Exception { - - for (final CompletableFuture apiCall : apiCalls) { - apiCall.completeExceptionally(e.getCause()); - } - - for (final CompletableFuture backgroundJob : backgroundJobs.values()) { - backgroundJob.completeExceptionally(e.getCause()); - } - - ctx.close(); - - ctx.fireExceptionCaught(e); - - } - - @Override - protected void channelRead0(ChannelHandlerContext ctx, EslMessage message) throws Exception { - final String contentType = message.getContentType(); - if (contentType.equals(Value.TEXT_EVENT_PLAIN) || - contentType.equals(Value.TEXT_EVENT_XML)) { - // transform into an event - final EslEvent eslEvent = new EslEvent(message); - if (eslEvent.getEventName().equals("BACKGROUND_JOB")) { - final String backgroundUuid = eslEvent.getEventHeaders().get(EslEventHeaderNames.JOB_UUID); - final CompletableFuture future = backgroundJobs.remove(backgroundUuid); - if (null != future) { - future.complete(eslEvent); - } - } else { - handleEslEvent(ctx, eslEvent); - } - } else { - handleEslMessage(ctx, message); - } - } - - protected void handleEslMessage(ChannelHandlerContext ctx, EslMessage message) { - log.info("Received message: [{}]", message); - final String contentType = message.getContentType(); - - switch (contentType) { - case Value.API_RESPONSE: - log.debug("Api response received [{}]", message); - apiCalls.poll().complete(message); - break; - - case Value.COMMAND_REPLY: - log.debug("Command reply received [{}]", message); - apiCalls.poll().complete(message); - break; - - case Value.AUTH_REQUEST: - log.debug("Auth request received [{}]", message); - handleAuthRequest(ctx); - break; - - case Value.TEXT_DISCONNECT_NOTICE: - log.debug("Disconnect notice received [{}]", message); - handleDisconnectionNotice(); - break; - - default: - log.warn("Unexpected message content type [{}]", contentType); - break; - } - } - - /** - * 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 CompletableFuture sendApiSingleLineCommand(Channel channel, final String command) { - final CompletableFuture future = new CompletableFuture<>(); - try { - syncLock.lock(); - apiCalls.add(future); - channel.writeAndFlush(command + MESSAGE_TERMINATOR); - } finally { - syncLock.unlock(); - } - - return future; - - } - - /** - * Sends a FreeSWITCH API command to the channel and blocks, waiting for an immediate response from the - * server. - *

- * The outcome of the command from the server is returned in an {@link EslMessage} object. - * - * @param channel - * @param command API command to send - * @param arg command arguments - * @return an {@link EslMessage} containing command results - */ - public CompletableFuture sendSyncApiCommand(Channel channel, String command, String arg) { - - checkArgument(!isNullOrEmpty(command), "command may not be null or empty"); - checkArgument(!isNullOrEmpty(arg), "arg may not be null or empty"); - - return sendApiSingleLineCommand(channel, "api " + command + ' ' + arg); - } - - /** - * 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 - * @return the {@link EslMessage} attached to this command's callback - */ - public CompletableFuture sendApiMultiLineCommand(Channel channel, final List commandLines) { - // Build command with double line terminator at the end - final StringBuilder sb = new StringBuilder(); - for (final String line : commandLines) { - sb.append(line); - sb.append(LINE_TERMINATOR); - } - sb.append(LINE_TERMINATOR); - - final CompletableFuture future = new CompletableFuture<>(); - try { - syncLock.lock(); - apiCalls.add(future); - channel.write(sb.toString()); - channel.flush(); - } finally { - syncLock.unlock(); - } - - return future; - - } - - /** - * Returns the Job UUID of that the response event will have. - * - * @param channel - * @param command - * @return Job-UUID as a string - */ - public CompletableFuture sendBackgroundApiCommand(Channel channel, final String command) { - - return sendApiSingleLineCommand(channel, command) - .thenComposeAsync(result -> { - if (result.hasHeader(Name.JOB_UUID)) { - final String jobId = result.getHeaderValue(Name.JOB_UUID); - final CompletableFuture resultFuture = new CompletableFuture<>(); - backgroundJobs.put(jobId, resultFuture); - return resultFuture; - } else { - final CompletableFuture resultFuture = new CompletableFuture<>(); - resultFuture.completeExceptionally(new IllegalStateException("Missing Job-UUID header in bgapi response")); - return resultFuture; - } - }, backgroundJobExecutor); - } - - protected abstract void handleEslEvent(ChannelHandlerContext ctx, EslEvent event); - - protected abstract void handleAuthRequest(ChannelHandlerContext ctx); - - protected abstract void handleDisconnectionNotice(); - -} +/* + * 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.transport.event.EslEvent; +import org.freeswitch.esl.client.transport.message.EslHeaders.Name; +import org.freeswitch.esl.client.transport.message.EslHeaders.Value; +import org.freeswitch.esl.client.transport.message.EslMessage; +import org.jboss.netty.channel.*; +import org.jboss.netty.handler.execution.ExecutionHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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; + +/** + * 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: + *

  • + * 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. + *
  • + * Concrete sub classes are expected to 'terminate' the Netty IO processing pipeline (ie be the 'last' + * handler). + *
+ * 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 syncCallbacks = new ConcurrentLinkedQueue(); + + @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 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(); + } + } + +} diff --git a/src/main/java/org/freeswitch/esl/client/internal/Context.java b/src/main/java/org/freeswitch/esl/client/internal/Context.java deleted file mode 100644 index a9c1ab6..0000000 --- a/src/main/java/org/freeswitch/esl/client/internal/Context.java +++ /dev/null @@ -1,299 +0,0 @@ -package org.freeswitch.esl.client.internal; - -import io.netty.channel.Channel; -import org.freeswitch.esl.client.transport.CommandResponse; -import org.freeswitch.esl.client.transport.SendMsg; -import org.freeswitch.esl.client.transport.event.EslEvent; -import org.freeswitch.esl.client.transport.message.EslMessage; - -import java.util.concurrent.CompletableFuture; - -import static com.google.common.base.Preconditions.*; -import static com.google.common.base.Strings.isNullOrEmpty; -import static com.google.common.base.Throwables.propagate; -import static com.google.common.util.concurrent.Futures.getUnchecked; -import static org.freeswitch.esl.client.internal.IModEslApi.EventFormat.*; - -public class Context implements IModEslApi { - - private final AbstractEslClientHandler handler; - private final Channel channel; - - public Context(Channel channel, AbstractEslClientHandler clientHandler) { - this.handler = clientHandler; - this.channel = channel; - } - - @Override - public boolean canSend() { - return channel != null && channel.isActive(); - } - - /** - * Sends a mod_event_socket command to FreeSWITCH server and blocks, waiting for an immediate response from the - * server. - *

- * The outcome of the command from the server is returned in an {@link org.freeswitch.esl.client.transport.message.EslMessage} object. - * - * @param command a mod_event_socket command to send - * @return an {@link org.freeswitch.esl.client.transport.message.EslMessage} containing command results - */ - public EslMessage sendCommand(String command) { - - checkArgument(!isNullOrEmpty(command), "command cannot be null or empty"); - - try { - - return getUnchecked(handler.sendApiSingleLineCommand(channel, command.toLowerCase().trim())); - - } catch (Throwable t) { - throw propagate(t); - } - } - - /** - * Sends a FreeSWITCH API command to the server and blocks, waiting for an immediate response from the - * server. - *

- * The outcome of the command from the server is returned in an {@link org.freeswitch.esl.client.transport.message.EslMessage} object. - * - * @param command API command to send - * @param arg command arguments - * @return an {@link org.freeswitch.esl.client.transport.message.EslMessage} containing command results - */ - @Override - public EslMessage sendApiCommand(String command, String arg) { - - checkArgument(!isNullOrEmpty(command), "command cannot be null or empty"); - - try { - - final StringBuilder sb = new StringBuilder(); - sb.append("api ").append(command); - if (!isNullOrEmpty(arg)) { - sb.append(' ').append(arg); - } - - return getUnchecked(handler.sendApiSingleLineCommand(channel, sb.toString())); - - } catch (Throwable t) { - throw propagate(t); - } - } - - /** - * Submit a FreeSWITCH API command to the server to be executed in background mode. A synchronous - * response from the server provides a UUID to identify the job execution results. When the server - * has completed the job execution it fires a BACKGROUND_JOB Event with the execution results.

- * Note that this Client must be subscribed in the normal way to BACKGROUND_JOB Events, in order to - * receive this event. - * - * @param command API command to send - * @param arg command arguments - * @return String Job-UUID that the server will tag result event with. - */ - @Override - public CompletableFuture sendBackgroundApiCommand(String command, String arg) { - - checkArgument(!isNullOrEmpty(command), "command cannot be null or empty"); - - final StringBuilder sb = new StringBuilder(); - sb.append("bgapi ").append(command); - if (!isNullOrEmpty(arg)) { - sb.append(' ').append(arg); - } - - return handler.sendBackgroundApiCommand(channel, sb.toString()); - } - - /** - * Set the current event subscription for this connection to the server. Examples of the events - * argument are: - *

-	 *   ALL
-	 *   CHANNEL_CREATE CHANNEL_DESTROY HEARTBEAT
-	 *   CUSTOM conference::maintenance
-	 *   CHANNEL_CREATE CHANNEL_DESTROY CUSTOM conference::maintenance sofia::register sofia::expire
-	 * 
- * Subsequent calls to this method replaces any previous subscriptions that were set. - *

- * Note: current implementation can only process 'plain' events. - * - * @param format can be { plain | xml } - * @param events { all | space separated list of events } - * @return a {@link org.freeswitch.esl.client.transport.CommandResponse} with the server's response. - */ - @Override - public CommandResponse setEventSubscriptions(EventFormat format, String events) { - - // temporary hack - checkState(format.equals(PLAIN), "Only 'plain' event format is supported at present"); - - try { - - final StringBuilder sb = new StringBuilder(); - sb.append("event ").append(format.toString()); - if (!isNullOrEmpty(events)) { - sb.append(' ').append(events); - } - - final EslMessage response = getUnchecked(handler.sendApiSingleLineCommand(channel, sb.toString())); - return new CommandResponse(sb.toString(), response); - - } catch (Throwable t) { - throw propagate(t); - } - - } - - /** - * Cancel any existing event subscription. - * - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse cancelEventSubscriptions() { - - try { - final EslMessage response = getUnchecked(handler.sendApiSingleLineCommand(channel, "noevents")); - return new CommandResponse("noevents", response); - } catch (Throwable t) { - throw propagate(t); - } - } - - /** - * Add an event filter to the current set of event filters on this connection. Any of the event headers - * can be used as a filter. - *

- * Note that event filters follow 'filter-in' semantics. That is, when a filter is applied - * only the filtered values will be received. Multiple filters can be added to the current - * connection. - *

- * Example filters: - *
-	 *    eventHeader        valueToFilter
-	 *    ----------------------------------
-	 *    Event-Name         CHANNEL_EXECUTE
-	 *    Channel-State      CS_NEW
-	 * 
- * - * @param eventHeader to filter on - * @param valueToFilter the value to match - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse addEventFilter(String eventHeader, String valueToFilter) { - - checkArgument(!isNullOrEmpty(eventHeader), "eventHeader cannot be null or empty"); - - try { - final StringBuilder sb = new StringBuilder(); - sb.append("filter ").append(eventHeader); - if (!isNullOrEmpty(valueToFilter)) { - sb.append(' ').append(valueToFilter); - } - - final EslMessage response = getUnchecked(handler.sendApiSingleLineCommand(channel, sb.toString())); - return new CommandResponse(sb.toString(), response); - - } catch (Throwable t) { - throw propagate(t); - } - } - - /** - * Delete an event filter from the current set of event filters on this connection. See - * - * @param eventHeader to remove - * @param valueToFilter to remove - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse deleteEventFilter(String eventHeader, String valueToFilter) { - - checkArgument(!isNullOrEmpty(eventHeader), "eventHeader cannot be null or empty"); - - try { - - final StringBuilder sb = new StringBuilder(); - sb.append("filter delete ").append(eventHeader); - if (!isNullOrEmpty(valueToFilter)) { - sb.append(' ').append(valueToFilter); - } - - final EslMessage response = getUnchecked(handler.sendApiSingleLineCommand(channel, sb.toString())); - return new CommandResponse(sb.toString(), response); - - } catch (Throwable t) { - throw propagate(t); - } - } - - /** - * Send a {@link SendMsg} command to FreeSWITCH. This client requires that the {@link SendMsg} - * has a call UUID parameter. - * - * @param sendMsg a {@link SendMsg} with call UUID - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse sendMessage(SendMsg sendMsg) { - - checkNotNull(sendMsg, "sendMsg cannot be null"); - - try { - final EslMessage response = getUnchecked(handler.sendApiMultiLineCommand(channel, sendMsg.getMsgLines())); - return new CommandResponse(sendMsg.toString(), response); - } catch (Throwable t) { - throw propagate(t); - } - - } - - /** - * Enable log output. - * - * @param level using the same values as in console.conf - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse setLoggingLevel(LoggingLevel level) { - - try { - final StringBuilder sb = new StringBuilder(); - sb.append("log ").append(level.toString()); - - final EslMessage response = getUnchecked(handler.sendApiSingleLineCommand(channel, sb.toString())); - return new CommandResponse(sb.toString(), response); - } catch (Throwable t) { - throw propagate(t); - } - - } - - /** - * Disable any logging previously enabled with setLogLevel(). - * - * @return a {@link CommandResponse} with the server's response. - */ - @Override - public CommandResponse cancelLogging() { - - try { - final EslMessage response = getUnchecked(handler.sendApiSingleLineCommand(channel, "nolog")); - return new CommandResponse("nolog", response); - } catch (Throwable t) { - throw propagate(t); - } - } - - public void closeChannel() { - try { - if(channel != null && channel.isOpen()) - channel.close(); - } catch (Throwable t) { - throw propagate(t); - } - } -} diff --git a/src/main/java/org/freeswitch/esl/client/internal/HeaderParser.java b/src/main/java/org/freeswitch/esl/client/internal/HeaderParser.java new file mode 100644 index 0000000..18ef2a8 --- /dev/null +++ b/src/main/java/org/freeswitch/esl/client/internal/HeaderParser.java @@ -0,0 +1,99 @@ +/* + * 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.jboss.netty.handler.codec.http.HttpMessageDecoder; + +/** + * This parser provides a static helper method to split a standard Header field + * into the name and value parts. + *

+ * This code was copied from the splitHeader() method in the {@link HttpMessageDecoder} 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 + *

+     *   Header-Name: Some_header-value
+     * 
+ * 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; + } +} diff --git a/src/main/java/org/freeswitch/esl/client/inbound/IEslProtocolListener.java b/src/main/java/org/freeswitch/esl/client/internal/IEslProtocolListener.java similarity index 73% rename from src/main/java/org/freeswitch/esl/client/inbound/IEslProtocolListener.java rename to src/main/java/org/freeswitch/esl/client/internal/IEslProtocolListener.java index ffc3151..906dbb4 100644 --- a/src/main/java/org/freeswitch/esl/client/inbound/IEslProtocolListener.java +++ b/src/main/java/org/freeswitch/esl/client/internal/IEslProtocolListener.java @@ -1,33 +1,36 @@ -/* - * 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.Context; -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. - *

- * Allow client implementations to observe events arriving from the server. - */ -interface IEslProtocolListener { - void authResponseReceived(CommandResponse response); - - void eventReceived(Context ctx, EslEvent event); - - void 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.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. + *

+ * Allow client implementations to observe events arriving from the server. + * + * @author david varnes + */ +public interface IEslProtocolListener +{ + void authResponseReceived(CommandResponse response); + + void eventReceived(EslEvent event); + + void disconnected(); +} diff --git a/src/main/java/org/freeswitch/esl/client/internal/IModEslApi.java b/src/main/java/org/freeswitch/esl/client/internal/IModEslApi.java deleted file mode 100644 index 31f8025..0000000 --- a/src/main/java/org/freeswitch/esl/client/internal/IModEslApi.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.freeswitch.esl.client.internal; - -import org.freeswitch.esl.client.transport.CommandResponse; -import org.freeswitch.esl.client.transport.SendMsg; -import org.freeswitch.esl.client.transport.event.EslEvent; -import org.freeswitch.esl.client.transport.message.EslMessage; - -import java.util.concurrent.CompletableFuture; - -public interface IModEslApi { - - enum EventFormat { - - PLAIN("plain"), - XML("xml"), - JSON("json"); - - private final String text; - - EventFormat(String txt) { - this.text = txt; - } - - @Override - public String toString() { - return text; - } - - } - - enum LoggingLevel { - - CONSOLE("console"), - DEBUG("debug"), - INFO("info"), - NOTICE("notice"), - WARNING("warning"), - ERR("err"), - CRIT("crit"), - ALERT("alert"); - - private final String text; - - LoggingLevel(String txt) { - this.text = txt; - } - - @Override - public String toString() { - return text; - } - - } - - boolean canSend(); - - EslMessage sendApiCommand(String command, String arg); - - CompletableFuture sendBackgroundApiCommand(String command, String arg); - - CommandResponse setEventSubscriptions(EventFormat format, String events); - - CommandResponse cancelEventSubscriptions(); - - CommandResponse addEventFilter(String eventHeader, String valueToFilter); - - CommandResponse deleteEventFilter(String eventHeader, String valueToFilter); - - CommandResponse sendMessage(SendMsg sendMsg); - - CommandResponse setLoggingLevel(LoggingLevel level); - - CommandResponse cancelLogging(); -} diff --git a/src/main/java/org/freeswitch/esl/client/internal/debug/ChannelEventRunnable.java b/src/main/java/org/freeswitch/esl/client/internal/debug/ChannelEventRunnable.java new file mode 100644 index 0000000..4c5fe4e --- /dev/null +++ b/src/main/java/org/freeswitch/esl/client/internal/debug/ChannelEventRunnable.java @@ -0,0 +1,89 @@ +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 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; + +import java.util.concurrent.Executor; + +/** + * 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. + */ + @Override + public void run() { +// log.info( "Sending [{}] upstream in [{}]", e, ctx ); + try + { + ctx.sendUpstream(e); + } + catch ( Throwable t ) + { + log.error( "Caught -->", t ); + } + } + + @Override + public Object unwrap() { + return e; + } +} diff --git a/src/main/java/org/freeswitch/esl/client/internal/debug/ExecutionHandler.java b/src/main/java/org/freeswitch/esl/client/internal/debug/ExecutionHandler.java new file mode 100644 index 0000000..9d6b7d4 --- /dev/null +++ b/src/main/java/org/freeswitch/esl/client/internal/debug/ExecutionHandler.java @@ -0,0 +1,113 @@ +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 org.jboss.netty.channel.*; +import org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor; +import org.jboss.netty.util.ExternalResourceReleasable; +import org.jboss.netty.util.internal.ExecutorUtil; + +import java.util.concurrent.Executor; + +/** + * Forwards an upstream {@link ChannelEvent} to an {@link Executor}. + *

+ * 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}: + *

+ * ChannelPipeline pipeline = ...;
+ * pipeline.addLast("decoder", new MyProtocolDecoder());
+ * pipeline.addLast("encoder", new MyProtocolEncoder());
+ *
+ * // HERE
+ * pipeline.addLast("executor", new {@link ExecutionHandler}(new {@link OrderedMemoryAwareThreadPoolExecutor}(16, 1048576, 1048576)));
+ *
+ * pipeline.addLast("handler", new MyBusinessLogicHandler());
+ * 
+ * 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 + */ +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. + */ + @Override + public void releaseExternalResources() { + ExecutorUtil.terminate(getExecutor()); + } + + @Override + public void handleUpstream( + ChannelHandlerContext context, ChannelEvent e) throws Exception { + executor.execute(new ChannelEventRunnable(context, e)); + } + + @Override + 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); + } +} diff --git a/src/main/java/org/freeswitch/esl/client/outbound/AbstractOutboundClientHandler.java b/src/main/java/org/freeswitch/esl/client/outbound/AbstractOutboundClientHandler.java new file mode 100644 index 0000000..82399ad --- /dev/null +++ b/src/main/java/org/freeswitch/esl/client/outbound/AbstractOutboundClientHandler.java @@ -0,0 +1,70 @@ +/* + * 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.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: + *
  • + * 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. + *
+ * 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 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" ); + // The message decoder for outbound, treats most of this incoming message as an 'event' in + // message body, so it 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" ); + } +} diff --git a/src/main/java/org/freeswitch/esl/client/outbound/AbstractOutboundPipelineFactory.java b/src/main/java/org/freeswitch/esl/client/outbound/AbstractOutboundPipelineFactory.java new file mode 100644 index 0000000..d4690b3 --- /dev/null +++ b/src/main/java/org/freeswitch/esl/client/outbound/AbstractOutboundPipelineFactory.java @@ -0,0 +1,52 @@ +/* + * 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.EslFrameDecoder; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineFactory; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.handler.codec.string.StringEncoder; +import org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor; + +/** + * An abstract factory to assemble a Netty processing pipeline for outbound clients. + * + * @author david varnes + */ +public abstract class AbstractOutboundPipelineFactory implements ChannelPipelineFactory +{ + @Override + public ChannelPipeline getPipeline() throws Exception + { + ChannelPipeline pipeline = Channels.pipeline(); + // Add the text line codec combination first + pipeline.addLast( "encoder", new StringEncoder() ); + // Note that outbound mode requires the decoder to treat many 'headers' as body lines + pipeline.addLast( "decoder", new EslFrameDecoder( 8092, 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(); +} diff --git a/src/main/java/org/freeswitch/esl/client/outbound/IClientHandler.java b/src/main/java/org/freeswitch/esl/client/outbound/IClientHandler.java deleted file mode 100644 index 7a2abcc..0000000 --- a/src/main/java/org/freeswitch/esl/client/outbound/IClientHandler.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.freeswitch.esl.client.outbound; - -import org.freeswitch.esl.client.inbound.IEslEventListener; -import org.freeswitch.esl.client.internal.Context; -import org.freeswitch.esl.client.transport.event.EslEvent; - -public interface IClientHandler extends IEslEventListener { - void onConnect(Context ctx, EslEvent event); -} diff --git a/src/main/java/org/freeswitch/esl/client/outbound/IClientHandlerFactory.java b/src/main/java/org/freeswitch/esl/client/outbound/IClientHandlerFactory.java deleted file mode 100644 index 4307961..0000000 --- a/src/main/java/org/freeswitch/esl/client/outbound/IClientHandlerFactory.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.freeswitch.esl.client.outbound; - -public interface IClientHandlerFactory { - IClientHandler createClientHandler(); -} diff --git a/src/main/java/org/freeswitch/esl/client/outbound/OutboundChannelInitializer.java b/src/main/java/org/freeswitch/esl/client/outbound/OutboundChannelInitializer.java deleted file mode 100644 index 197abca..0000000 --- a/src/main/java/org/freeswitch/esl/client/outbound/OutboundChannelInitializer.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.freeswitch.esl.client.outbound; - -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.string.StringEncoder; -import org.freeswitch.esl.client.transport.message.EslFrameDecoder; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class OutboundChannelInitializer extends ChannelInitializer { - - private final IClientHandlerFactory clientHandlerFactory; - private ExecutorService callbackExecutor = Executors.newSingleThreadExecutor(); - - public OutboundChannelInitializer(IClientHandlerFactory clientHandlerFactory) { - this.clientHandlerFactory = clientHandlerFactory; - } - - public OutboundChannelInitializer setCallbackExecutor(ExecutorService callbackExecutor) { - this.callbackExecutor = callbackExecutor; - return this; - } - - @Override - protected void initChannel(SocketChannel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - // Add the text line codec combination first - pipeline.addLast("encoder", new StringEncoder()); - // Note that outbound mode requires the decoder to treat many 'headers' as body lines - pipeline.addLast("decoder", new EslFrameDecoder(8092, true)); - - // now the outbound client logic - pipeline.addLast("clientHandler", - new OutboundClientHandler( - clientHandlerFactory.createClientHandler(), - callbackExecutor)); - } -} diff --git a/src/main/java/org/freeswitch/esl/client/outbound/OutboundClientHandler.java b/src/main/java/org/freeswitch/esl/client/outbound/OutboundClientHandler.java deleted file mode 100644 index f72be64..0000000 --- a/src/main/java/org/freeswitch/esl/client/outbound/OutboundClientHandler.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 io.netty.channel.ChannelHandlerContext; -import org.freeswitch.esl.client.internal.AbstractEslClientHandler; -import org.freeswitch.esl.client.internal.Context; -import org.freeswitch.esl.client.transport.event.EslEvent; -import org.freeswitch.esl.client.transport.message.EslMessage; - -import java.util.concurrent.ExecutorService; - -/** - * Specialised {@link AbstractEslClientHandler} that implements the base connecction logic for an - * 'Outbound' FreeSWITCH Event Socket connection. The responsibilities for this class are: - *
  • - * 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. - *
- * 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). - */ -class OutboundClientHandler extends AbstractEslClientHandler { - - private final IClientHandler clientHandler; - private final ExecutorService callbackExecutor; - - public OutboundClientHandler(IClientHandler clientHandler, ExecutorService callbackExecutor) { - this.clientHandler = clientHandler; - this.callbackExecutor = callbackExecutor; - } - - @Override - public void channelActive(final ChannelHandlerContext ctx) throws Exception { - super.channelActive(ctx); - - // Have received a connection from FreeSWITCH server, send connect response - log.debug("Received new connection from server, sending connect message"); - - sendApiSingleLineCommand(ctx.channel(), "connect") - .thenAccept(response -> clientHandler.onConnect( - new Context(ctx.channel(), OutboundClientHandler.this), - new EslEvent(response, true))) - .exceptionally(throwable -> { - ctx.channel().close(); - handleDisconnectionNotice(); - return null; - }); - } - - @Override - protected void handleEslEvent(final ChannelHandlerContext ctx, final EslEvent event) { - callbackExecutor.execute(() -> clientHandler.onEslEvent( - new Context(ctx.channel(), OutboundClientHandler.this), event)); - } - - @Override - protected void handleAuthRequest(io.netty.channel.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"); - } -} diff --git a/src/main/java/org/freeswitch/esl/client/outbound/SocketClient.java b/src/main/java/org/freeswitch/esl/client/outbound/SocketClient.java index 00f6154..e46a478 100644 --- a/src/main/java/org/freeswitch/esl/client/outbound/SocketClient.java +++ b/src/main/java/org/freeswitch/esl/client/outbound/SocketClient.java @@ -1,82 +1,89 @@ /* * 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. + * 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. + * 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 com.google.common.util.concurrent.AbstractService; -import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.Channel; -import io.netty.channel.ChannelOption; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.nio.NioServerSocketChannel; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +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; -import java.net.SocketAddress; +import java.net.InetSocketAddress; +import java.util.concurrent.*; /** * Entry point to run a socket client that a running FreeSWITCH Event Socket Library module can * make outbound connections to. - *

+ *

* 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. - *

+ *

* See http://wiki.freeswitch.org/wiki/Mod_event_socket + * + * @author david varnes */ -public class SocketClient extends AbstractService { +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, int bossMaxNumber, int workerMaxNumber) { + this.port = port; + this.pipelineFactory = pipelineFactory; + + final ThreadFactory bossThreadFactory = new ThreadFactoryBuilder().setNameFormat("boss-pool-%d").build(); + final ThreadFactory workerThreadFactory = new ThreadFactoryBuilder().setNameFormat("worker-pool-%d").build(); - private final Logger log = LoggerFactory.getLogger(this.getClass()); - private final EventLoopGroup bossGroup; - private final EventLoopGroup workerGroup; - private final IClientHandlerFactory clientHandlerFactory; - private final SocketAddress bindAddress; + final ExecutorService bossPool = new ThreadPoolExecutor(1, bossMaxNumber, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(1024), bossThreadFactory, new ThreadPoolExecutor.AbortPolicy()); - private Channel serverChannel; + final ExecutorService workerPool = new ThreadPoolExecutor(4, workerMaxNumber, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(2048), workerThreadFactory, new ThreadPoolExecutor.AbortPolicy()); - public SocketClient(SocketAddress bindAddress, IClientHandlerFactory clientHandlerFactory) { - this.bindAddress = bindAddress; - this.clientHandlerFactory = clientHandlerFactory; - this.bossGroup = new NioEventLoopGroup(); - this.workerGroup = new NioEventLoopGroup(); - } + this.channelFactory = new NioServerSocketChannelFactory(bossPool, workerPool); + } - @Override - protected void doStart() { - final ServerBootstrap bootstrap = new ServerBootstrap() - .group(bossGroup, workerGroup) - .channel(NioServerSocketChannel.class) - .childHandler(new OutboundChannelInitializer(clientHandlerFactory)) - .childOption(ChannelOption.TCP_NODELAY, true) - .childOption(ChannelOption.SO_KEEPALIVE, true); + public void start() { + ServerBootstrap bootstrap = new ServerBootstrap(channelFactory); - serverChannel = bootstrap.bind(bindAddress).syncUninterruptibly().channel(); - notifyStarted(); - log.info("SocketClient waiting for connections on [{}] ...", bindAddress); - } + bootstrap.setPipelineFactory(pipelineFactory); + bootstrap.setOption("child.tcpNoDelay", true); + bootstrap.setOption("child.keepAlive", true); - @Override - protected void doStop() { - if (null != serverChannel) { - serverChannel.close().awaitUninterruptibly(); - } - workerGroup.shutdownGracefully(); - bossGroup.shutdownGracefully(); - notifyStopped(); - log.info("SocketClient stopped"); - } + 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"); + } } diff --git a/src/main/java/org/freeswitch/esl/client/outbound/example/SimpleHangupOutboundHandler.java b/src/main/java/org/freeswitch/esl/client/outbound/example/SimpleHangupOutboundHandler.java new file mode 100644 index 0000000..3271d7b --- /dev/null +++ b/src/main/java/org/freeswitch/esl/client/outbound/example/SimpleHangupOutboundHandler.java @@ -0,0 +1,83 @@ +/* + * 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.EslHeaders.Name; +import org.freeswitch.esl.client.transport.message.EslMessage; +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 + */ +public class SimpleHangupOutboundHandler extends AbstractOutboundClientHandler +{ + + @Override + protected void handleConnectResponse( ChannelHandlerContext ctx, EslEvent event ) + { + log.info( "Received connect response [{}]", event ); + if ( "CHANNEL_DATA".equalsIgnoreCase(event.getEventName()) ) + { + // 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 + { + log.error( "Call hangup failed: [{}}", response.getHeaderValue( Name.REPLY_TEXT ) ); + } + } +} diff --git a/src/main/java/org/freeswitch/esl/client/outbound/example/SimpleHangupPipelineFactory.java b/src/main/java/org/freeswitch/esl/client/outbound/example/SimpleHangupPipelineFactory.java new file mode 100644 index 0000000..9b9738e --- /dev/null +++ b/src/main/java/org/freeswitch/esl/client/outbound/example/SimpleHangupPipelineFactory.java @@ -0,0 +1,35 @@ +/* + * 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 + */ +public class SimpleHangupPipelineFactory extends AbstractOutboundPipelineFactory +{ + + @Override + protected AbstractOutboundClientHandler makeHandler() + { + return new SimpleHangupOutboundHandler(); + } + +} diff --git a/src/main/java/org/freeswitch/esl/client/transport/CommandResponse.java b/src/main/java/org/freeswitch/esl/client/transport/CommandResponse.java index 4714fb2..2f66aca 100644 --- a/src/main/java/org/freeswitch/esl/client/transport/CommandResponse.java +++ b/src/main/java/org/freeswitch/esl/client/transport/CommandResponse.java @@ -20,45 +20,53 @@ /** * Result object to carry the results of a command sent to the FreeSWITCH Event Socket. + * + * @author david varnes */ -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; - } +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; + } } diff --git a/src/main/java/org/freeswitch/esl/client/transport/HeaderParser.java b/src/main/java/org/freeswitch/esl/client/transport/HeaderParser.java deleted file mode 100644 index a886f13..0000000 --- a/src/main/java/org/freeswitch/esl/client/transport/HeaderParser.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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; - -/** - * This parser provides a static helper method to split a standard Header field - * into the name and value parts. - *

- * This code was copied from the splitHeader() method in the HttpMessageDecoder class, which - * is licensed under the Apache License version 2. (Original author: Trustin Lee.) - */ -public class HeaderParser { - /** - * Split a header in the form - *

-	 *   Header-Name: Some_header-value
-	 * 
- * 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; - } -} diff --git a/src/main/java/org/freeswitch/esl/client/transport/SendMsg.java b/src/main/java/org/freeswitch/esl/client/transport/SendMsg.java index 249a00c..846d9c0 100644 --- a/src/main/java/org/freeswitch/esl/client/transport/SendMsg.java +++ b/src/main/java/org/freeswitch/esl/client/transport/SendMsg.java @@ -18,162 +18,169 @@ import java.util.ArrayList; import java.util.List; -public class SendMsg { - private final List msgLines = new ArrayList<>(); - private final boolean hasUuid; - - /** - * Constructor for use with outbound socket client only. This client mode does not need a call - * UUID for context. - */ - public SendMsg() { - msgLines.add("sendmsg"); - hasUuid = false; - } - - /** - * Constructor for use with the inbound client. - * - * @param uuid of the call to send message to (it should be in 'park' to be operated on). - */ - public SendMsg(String uuid) { - msgLines.add("sendmsg " + uuid); - hasUuid = true; - } - - /** - * Adds the following line to the message: - *
-	 *   call-command: command
-	 * 
- * - * @param command the string command [ execute | hangup ] - */ - public SendMsg addCallCommand(String command) { - msgLines.add("call-command: " + command); - return this; - } - - /** - * Adds the following line to the message: - *
-	 *   execute-app-name: appName
-	 * 
- * - * @param appName the string app name to execute - */ - public SendMsg addExecuteAppName(String appName) { - msgLines.add("execute-app-name: " + appName); - return this; - } - - /** - * Adds the following line to the message: - *
-	 *   execute-app-arg: arg
-	 * 
- * - * @param arg the string arg - */ - public SendMsg addExecuteAppArg(String arg) { - msgLines.add("execute-app-arg: " + arg); - return this; - } - - /** - * Adds the following line to the message: - *
-	 *   loops: count
-	 * 
- * - * @param count the int number of times to loop - */ - public SendMsg addLoops(int count) { - msgLines.add("loops: " + count); - return this; - } - - /** - * Adds the following line to the message: - *
-	 *   hangup-cause: cause
-	 * 
- * - * @param cause the string cause - */ - public SendMsg addHangupCause(String cause) { - msgLines.add("hangup-cause: " + cause); - return this; - } - - /** - * Adds the following line to the message: - *
-	 *   nomedia-uid: value
-	 * 
- * - * @param value the string value part of the line - */ - public SendMsg addNomediaUuid(String value) { - msgLines.add("nomedia-uuid: " + value); - return this; - } - - /** - * Adds the following line to the message: - *
-	 *    event-lock: true
-	 *  
- */ - public SendMsg addEventLock() { - msgLines.add("event-lock: true"); - return this; - } - - /** - * A generic method to add a message line. The constructed line in the sent message will be in the - * form: - *
-	 *   name: value
-	 * 
- * - * @param name part of line - * @param value part of line - */ - public SendMsg addGenericLine(String name, String value) { - msgLines.add(name + ": " + value); - return this; - } - - /** - * The list of strings that make up the message to send to FreeSWITCH. - * - * @return list of strings, as they were added to this message. - */ - public List getMsgLines() { - return msgLines; - } - - /** - * Indicate if message was constructed with a UUID. - * - * @return true if constructed with a UUID. - */ - public boolean hasUuid() { - return hasUuid; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder("SendMsg: "); - if (msgLines.size() > 1) { - sb.append(msgLines.get(1)); - } else if (msgLines.size() > 0) { - sb.append(0); - } - - return sb.toString(); - } - - +/** + * + * @author david varnes + */ +public class SendMsg +{ + private final List msgLines = new ArrayList(); + private final boolean hasUuid; + + /** + * Constructor for use with outbound socket client only. This client mode does not need a call + * UUID for context. + */ + public SendMsg() + { + msgLines.add( "sendmsg" ); + hasUuid = false; + } + + /** + * Constructor for use with the inbound client. + * + * @param uuid of the call to send message to (it should be in 'park' to be operated on). + */ + public SendMsg( String uuid ) + { + msgLines.add( "sendmsg " + uuid ); + hasUuid = true; + } + + /** + * Adds the following line to the message: + *
+     *   call-command: command
+     * 
+ * @param command the string command [ execute | hangup ] + */ + public void addCallCommand( String command ) + { + msgLines.add( "call-command: " + command ); + } + + /** + * Adds the following line to the message: + *
+     *   execute-app-name: appName
+     * 
+ * @param appName the string app name to execute + */ + public void addExecuteAppName( String appName ) + { + msgLines.add( "execute-app-name: " + appName ); + } + + /** + * Adds the following line to the message: + *
+     *   execute-app-arg: arg
+     * 
+ * @param arg the string arg + */ + public void addExecuteAppArg( String arg ) + { + msgLines.add( "execute-app-arg: " + arg ); + } + + /** + * Adds the following line to the message: + *
+     *   loops: count
+     * 
+ * @param count the int number of times to loop + */ + public void addLoops( int count ) + { + msgLines.add( "loops: " + count ); + } + + /** + * Adds the following line to the message: + *
+     *   hangup-cause: cause
+     * 
+ * @param cause the string cause + */ + public void addHangupCause( String cause ) + { + msgLines.add( "hangup-cause: " + cause ); + } + + /** + * Adds the following line to the message: + *
+     *   nomedia-uid: value
+     * 
+ * @param value the string value part of the line + */ + public void addNomediaUuid( String value ) + { + msgLines.add( "nomedia-uuid: " + value ); + } + + /** + * Adds the following line to the message: + *
+     *    event-lock: true
+     *  
+ */ + public void addEventLock() + { + msgLines.add( "event-lock: true" ); + } + + /** + * A generic method to add a message line. The constructed line in the sent message will be in the + * form: + *
+     *   name: value
+     * 
+ * + * @param name part of line + * @param value part of line + */ + public void addGenericLine( String name, String value ) + { + msgLines.add( name + ": " + value ); + } + + /** + * The list of strings that make up the message to send to FreeSWITCH. + * + * @return list of strings, as they were added to this message. + */ + public List getMsgLines() + { + return msgLines; + } + + /** + * Indicate if message was constructed with a UUID. + * + * @return true if constructed with a UUID. + */ + public boolean hasUuid() + { + return hasUuid; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder( "SendMsg: " ); + if ( msgLines.size() > 1 ) + { + sb.append( msgLines.get( 1 ) ); + } + else if ( msgLines.size() > 0 ) + { + sb.append( 0 ); + } + + return sb.toString(); + } + + } diff --git a/src/main/java/org/freeswitch/esl/client/transport/event/EslEvent.java b/src/main/java/org/freeswitch/esl/client/transport/event/EslEvent.java index f64e253..d5f76f1 100644 --- a/src/main/java/org/freeswitch/esl/client/transport/event/EslEvent.java +++ b/src/main/java/org/freeswitch/esl/client/transport/event/EslEvent.java @@ -15,8 +15,7 @@ */ package org.freeswitch.esl.client.transport.event; -import org.freeswitch.esl.client.transport.HeaderParser; -import org.freeswitch.esl.client.transport.message.EslHeaders; +import org.freeswitch.esl.client.internal.HeaderParser; import org.freeswitch.esl.client.transport.message.EslHeaders.Name; import org.freeswitch.esl.client.transport.message.EslHeaders.Value; import org.freeswitch.esl.client.transport.message.EslMessage; @@ -30,170 +29,206 @@ import java.util.List; import java.util.Map; -import static com.google.common.base.MoreObjects.toStringHelper; - /** * FreeSWITCH Event Socket events are decoded into this data object. - *

+ *

* 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. - *

+ * 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. + *

* 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} - *

+ *

* Any eventBody lines are cached in a list. - *

- * The messageHeader lines from the original message are cached in a map keyed by {@link EslHeaders.Name}. - * + *

+ * The messageHeader lines from the original message are cached in a map keyed by {@link Name}. + * + * @author david varnes * @see EslEventHeaderNames */ -public class EslEvent { - private final Logger log = LoggerFactory.getLogger(this.getClass()); - - private final Map messageHeaders; - private final Map eventHeaders; - private final List eventBody; - private boolean decodeEventHeaders = true; - - public EslEvent(EslMessage rawMessage) { - this(rawMessage, false); - } - - public EslEvent(EslMessage rawMessage, boolean parseCommandReply) { - messageHeaders = rawMessage.getHeaders(); - eventHeaders = new HashMap<>(rawMessage.getBodyLines().size()); - eventBody = new ArrayList<>(); - // 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 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 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 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 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() { - return toStringHelper(this) - .add("name", getEventName()) - .add("headers", messageHeaders.size()) - .add("eventHeaders", eventHeaders.size()) - .add("eventBody", eventBody.size() + " lines") - .toString(); - } +public class EslEvent +{ + private final Logger log = LoggerFactory.getLogger( this.getClass() ); + + private final Map messageHeaders; + private final Map eventHeaders; + private final List eventBody; + private boolean decodeEventHeaders = true; + + public EslEvent( EslMessage rawMessage ) + { + this( rawMessage, false ); + } + + public EslEvent( EslMessage rawMessage, boolean parseCommandReply ) + { + messageHeaders = rawMessage.getHeaders(); + eventHeaders = new HashMap( rawMessage.getBodyLines().size() ); + eventBody = new ArrayList(); + // 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 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 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 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 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 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(); + } } diff --git a/src/main/java/org/freeswitch/esl/client/transport/event/EslEventHeaderNames.java b/src/main/java/org/freeswitch/esl/client/transport/event/EslEventHeaderNames.java index 3e917af..409e5b3 100644 --- a/src/main/java/org/freeswitch/esl/client/transport/event/EslEventHeaderNames.java +++ b/src/main/java/org/freeswitch/esl/client/transport/event/EslEventHeaderNames.java @@ -17,69 +17,73 @@ /** * Convenience container class for some commonly used ESL event header names (note there are many more!). - *

+ *

* These names are stored as strings (rather than an Enum) so that there is no necessity to keep up to * date with changes or additions to event header names. + * + * @author david varnes */ -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"; +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 */ - } + private EslEventHeaderNames() + { + /* private class */ + } } diff --git a/src/main/java/org/freeswitch/esl/client/transport/message/EslFrameDecoder.java b/src/main/java/org/freeswitch/esl/client/transport/message/EslFrameDecoder.java index f1bb095..5d42ea6 100644 --- a/src/main/java/org/freeswitch/esl/client/transport/message/EslFrameDecoder.java +++ b/src/main/java/org/freeswitch/esl/client/transport/message/EslFrameDecoder.java @@ -15,185 +15,215 @@ */ package org.freeswitch.esl.client.transport.message; -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.ReplayingDecoder; -import io.netty.handler.codec.TooLongFrameException; -import org.freeswitch.esl.client.transport.HeaderParser; +import org.freeswitch.esl.client.internal.HeaderParser; import org.freeswitch.esl.client.transport.message.EslHeaders.Name; +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.frame.TooLongFrameException; +import org.jboss.netty.handler.codec.replay.ReplayingDecoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; - /** * Decoder used by the IO processing pipeline. Client consumers should never need to use * this class. - *

+ *

* Follows the following decode algorithm (from FreeSWITCH wiki) *

  *    Look for \n\n in your receive buffer
  *
  *    Examine data for existence of Content-Length
- *
+ * 
  *    If NOT present, process event and remove from receive buffer
- *
+ *  
  *    IF present, Shift buffer to remove 'header'
  *    Evaluate content-length value
- *
+ *    
  *    Loop until receive buffer size is >= Content-length
  *    Extract content-length bytes from buffer and process
  * 
+ * + * @author david varnes */ -public class EslFrameDecoder extends ReplayingDecoder { - /** - * Line feed character - */ - static final byte LF = 10; - - protected enum State { - READ_HEADER, - READ_BODY, - } - - private final Logger log = LoggerFactory.getLogger(this.getClass()); - private final int maxHeaderSize; - private EslMessage currentMessage; - private boolean treatUnknownHeadersAsBody = false; - - public EslFrameDecoder(int maxHeaderSize) { - super(State.READ_HEADER); - if (maxHeaderSize <= 0) { - throw new IllegalArgumentException( - "maxHeaderSize must be a positive integer: " + - maxHeaderSize); - } - this.maxHeaderSize = maxHeaderSize; - } - - public EslFrameDecoder(int maxHeaderSize, boolean treatUnknownHeadersAsBody) { - this(maxHeaderSize); - this.treatUnknownHeadersAsBody = treatUnknownHeadersAsBody; - } - - @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List out) throws Exception { - State state = state(); - - log.trace("decode() : state [{}]", state); - switch (state) { - case READ_HEADER: - if (currentMessage == null) { - currentMessage = new EslMessage(); - } - /* - * read '\n' terminated lines until reach a single '\n' - */ - boolean reachedDoubleLF = false; - while (!reachedDoubleLF) { - // this will read or fail - String headerLine = readToLineFeedOrFail(buffer, maxHeaderSize); - log.debug("read header line [{}]", headerLine); - if (!headerLine.isEmpty()) { - // split the header line - String[] headerParts = HeaderParser.splitHeader(headerLine); - Name headerName = Name.fromLiteral(headerParts[0]); - if (headerName == null) { - if (treatUnknownHeadersAsBody) { - // cache this 'header' as a body line <-- useful for Outbound client mode - currentMessage.addBodyLine(headerLine); - } else { - throw new IllegalStateException("Unhandled ESL header [" + headerParts[0] + ']'); - } - } - currentMessage.addHeader(headerName, headerParts[1]); - } else { - reachedDoubleLF = true; - } - // do not read in this line again - checkpoint(); - } - // have read all headers - check for content-length - if (currentMessage.hasContentLength()) { - checkpoint(State.READ_BODY); - log.debug("have content-length, decoding body .."); - // force the next section - - break; - } else { - // end of message - checkpoint(State.READ_HEADER); - // send message upstream - EslMessage decodedMessage = currentMessage; - currentMessage = null; - - out.add(decodedMessage); - break; - } - - case READ_BODY: - /* - * read the content-length specified - */ - int contentLength = currentMessage.getContentLength(); - ByteBuf bodyBytes = buffer.readBytes(contentLength); - log.debug("read [{}] body bytes", bodyBytes.writerIndex()); - // most bodies are line based, so split on LF - while (bodyBytes.isReadable()) { - String bodyLine = readLine(bodyBytes, contentLength); - log.debug("read body line [{}]", bodyLine); - currentMessage.addBodyLine(bodyLine); - } - - // end of message - checkpoint(State.READ_HEADER); - // send message upstream - EslMessage decodedMessage = currentMessage; - currentMessage = null; - - out.add(decodedMessage); - break; - - default: - throw new Error("Illegal state: [" + state + ']'); - } - } - - private String readToLineFeedOrFail(ByteBuf buffer, int maxLineLegth) throws TooLongFrameException { - StringBuilder sb = new StringBuilder(64); - while (true) { - // this read might fail - byte nextByte = buffer.readByte(); - if (nextByte == LF) { - return sb.toString(); - } else { - // Abort decoding if the decoded line is too large. - if (sb.length() >= maxLineLegth) { - throw new TooLongFrameException( - "ESL header line is longer than " + maxLineLegth + " bytes."); - } - sb.append((char) nextByte); - } - } - } - - private String readLine(ByteBuf buffer, int maxLineLength) throws TooLongFrameException { - StringBuilder sb = new StringBuilder(64); - while (buffer.isReadable()) { - // this read should always succeed - byte nextByte = buffer.readByte(); - if (nextByte == LF) { - return sb.toString(); - } else { - // Abort decoding if the decoded line is too large. - if (sb.length() >= maxLineLength) { - throw new TooLongFrameException( - "ESL message line is longer than " + maxLineLength + " bytes."); - } - sb.append((char) nextByte); - } - } - - return sb.toString(); - } +public class EslFrameDecoder extends ReplayingDecoder +{ + /** + * Line feed character + */ + static final byte LF = 10; + + protected enum State + { + READ_HEADER, + READ_BODY, + } + + private final Logger log = LoggerFactory.getLogger( this.getClass() ); + private final int maxHeaderSize; + private EslMessage currentMessage; + private boolean treatUnknownHeadersAsBody = false; + + public EslFrameDecoder( int maxHeaderSize ) + { + super( State.READ_HEADER ); + if (maxHeaderSize <= 0) + { + throw new IllegalArgumentException( + "maxHeaderSize must be a positive integer: " + + maxHeaderSize); + } + this.maxHeaderSize = maxHeaderSize; + } + + public EslFrameDecoder( int maxHeaderSize, boolean treatUnknownHeadersAsBody ) + { + this( maxHeaderSize ); + this.treatUnknownHeadersAsBody = treatUnknownHeadersAsBody; + } + + @Override + protected Object decode( ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, + State state ) throws Exception + { + log.trace( "decode() : state [{}]", state ); + switch ( state ) + { + case READ_HEADER: + if ( currentMessage == null ) + { + currentMessage = new EslMessage(); + } + /* + * read '\n' terminated lines until reach a single '\n' + */ + boolean reachedDoubleLF = false; + while ( ! reachedDoubleLF ) + { + // this will read or fail + String headerLine = readToLineFeedOrFail( buffer, maxHeaderSize ); + log.debug( "read header line [{}]", headerLine ); + if ( ! headerLine.isEmpty() ) + { + // split the header line + String[] headerParts = HeaderParser.splitHeader( headerLine ); + Name headerName = Name.fromLiteral( headerParts[0] ); + if ( headerName == null ) + { + if ( treatUnknownHeadersAsBody ) + { + // cache this 'header' as a body line <-- useful for Outbound client mode + currentMessage.addBodyLine( headerLine ); + } + else + { + throw new IllegalStateException( "Unhandled ESL header [" + headerParts[0] + ']' ); + } + } + currentMessage.addHeader( headerName, headerParts[1] ); + } + else + { + reachedDoubleLF = true; + } + // do not read in this line again + checkpoint(); + } + // have read all headers - check for content-length + if ( currentMessage.hasContentLength() ) + { + checkpoint( State.READ_BODY ); + log.debug( "have content-length, decoding body .." ); + // force the next section + + return null; + } + else + { + // end of message + checkpoint( State.READ_HEADER ); + // send message upstream + EslMessage decodedMessage = currentMessage; + currentMessage = null; + + return decodedMessage; + } + + case READ_BODY: + /* + * read the content-length specified + */ + int contentLength = currentMessage.getContentLength(); + ChannelBuffer bodyBytes = buffer.readBytes( contentLength ); + log.debug( "read [{}] body bytes", bodyBytes.writerIndex() ); + // most bodies are line based, so split on LF + while( bodyBytes.readable() ) + { + String bodyLine = readLine( bodyBytes, contentLength ); + log.debug( "read body line [{}]", bodyLine ); + currentMessage.addBodyLine( bodyLine ); + } + + // end of message + checkpoint( State.READ_HEADER ); + // send message upstream + EslMessage decodedMessage = currentMessage; + currentMessage = null; + + return decodedMessage; + + default: + throw new Error( "Illegal state: [" + state + ']' ); + } + } + + private String readToLineFeedOrFail( ChannelBuffer buffer, int maxLineLegth ) throws TooLongFrameException + { + StringBuilder sb = new StringBuilder(64); + while ( true ) + { + // this read might fail + byte nextByte = buffer.readByte(); + if ( nextByte == LF ) + { + return sb.toString(); + } + else + { + // Abort decoding if the decoded line is too large. + if ( sb.length() >= maxLineLegth ) + { + throw new TooLongFrameException( + "ESL header line is longer than " + maxLineLegth + " bytes."); + } + sb.append( (char) nextByte ); + } + } + } + + private String readLine( ChannelBuffer buffer, int maxLineLength ) throws TooLongFrameException + { + StringBuilder sb = new StringBuilder(64); + while ( buffer.readable() ) + { + // this read should always succeed + byte nextByte = buffer.readByte(); + if (nextByte == LF) + { + return sb.toString(); + } + else + { + // Abort decoding if the decoded line is too large. + if ( sb.length() >= maxLineLength ) + { + throw new TooLongFrameException( + "ESL message line is longer than " + maxLineLength + " bytes."); + } + sb.append( (char) nextByte ); + } + } + + return sb.toString(); + } } diff --git a/src/main/java/org/freeswitch/esl/client/transport/message/EslHeaders.java b/src/main/java/org/freeswitch/esl/client/transport/message/EslHeaders.java index 45bb3c9..fb944c8 100644 --- a/src/main/java/org/freeswitch/esl/client/transport/message/EslHeaders.java +++ b/src/main/java/org/freeswitch/esl/client/transport/message/EslHeaders.java @@ -16,106 +16,121 @@ package org.freeswitch.esl.client.transport.message; /** - * Container class for enumeration of ESL message header names, and some commonly used + * Container class for enumeration of ESL message header names, and some commonly used * header string values. + * + * @author david varnes */ -public class EslHeaders { - /** - * Standard ESL header names. - *

- * Note this enum will need to be kept in synch with any new headers introduced on the server side. - */ - 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; - - 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. - *

- * This values are not coded as an enum to allow for the very large range of possible values, - * since they are just Strings. - */ - 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"; - } - +public class EslHeaders +{ + /** + * Standard ESL header names. + *

+ * Note this enum will need to be kept in synch with any new headers introduced on the server side. + * + * @author david varnes + */ + 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. + *

+ * 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 + */ + 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"; + } + } diff --git a/src/main/java/org/freeswitch/esl/client/transport/message/EslMessage.java b/src/main/java/org/freeswitch/esl/client/transport/message/EslMessage.java index febe5f2..d0959c1 100644 --- a/src/main/java/org/freeswitch/esl/client/transport/message/EslMessage.java +++ b/src/main/java/org/freeswitch/esl/client/transport/message/EslMessage.java @@ -24,135 +24,148 @@ import java.util.List; import java.util.Map; -import static com.google.common.base.MoreObjects.toStringHelper; - /** * Basic FreeSWITCH Event Socket messages from the server are decoded into this data object. - *

- * An ESL message is modelled as text lines. A message always has one or more header lines, and + *

+ * An ESL message is modelled as text lines. A message always has one or more header lines, and * optionally may have some body lines. - *

- * Header lines are parsed and cached in a map keyed by the {@link EslHeaders.Name} enum. A message + *

+ * Header lines are parsed and cached in a map keyed by the {@link Name} enum. A message * is always expected to have a "Content-Type" header - *

+ *

* Any Body lines are cached in a list. - * - * @see EslHeaders.Name + * + * @author david varnes + * @see Name */ -public class EslMessage { - private final Logger log = LoggerFactory.getLogger(this.getClass()); - - private final Map headers = new HashMap<>(); - private final List body = new ArrayList<>(); - - private Integer contentLength = null; - - /** - * 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 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 getBodyLines() { - return body; - } - - /** - * Used by the {@link EslFrameDecoder}. - */ - void addHeader(Name name, String value) { - log.debug("adding header [{}] [{}]", name, value); - headers.put(name, value); - } - - /** - * Used by the {@link EslFrameDecoder} - */ - void addBodyLine(String line) { - if (line == null) { - return; - } - body.add(line); - } - - /** - * Did this message return Reply-Text: +OK - * - * @return true if reply equals +OK, false if not. - */ - public boolean isReplyOk() { - return getHeaderValue(Name.REPLY_TEXT).trim().equals("+OK"); - } - - @Override - public String toString() { - return toStringHelper(this) - .add("contentType", getContentType()) - .add("headers", headers.size()) - .add("body", body.size() + " lines") - .toString(); - } - +public class EslMessage +{ + private final Logger log = LoggerFactory.getLogger( this.getClass() ); + + private final Map headers = new HashMap(); + private final List body = new ArrayList(); + + private Integer contentLength = null; + + /** + * All the received message headers in a map keyed by {@link 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 getHeaders() + { + return headers; + } + + /** + * Convenience method + * + * @param headerName as a {@link 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 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 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; + } + body.add( line ); + } + + @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(); + } + } diff --git a/src/test/java/OutboundTest.java b/src/test/java/OutboundTest.java deleted file mode 100644 index a4a6d37..0000000 --- a/src/test/java/OutboundTest.java +++ /dev/null @@ -1,116 +0,0 @@ -import com.google.common.base.Throwables; - -import org.freeswitch.esl.client.dptools.Execute; -import org.freeswitch.esl.client.dptools.ExecuteException; -import org.freeswitch.esl.client.inbound.Client; -import org.freeswitch.esl.client.internal.Context; -import org.freeswitch.esl.client.outbound.IClientHandler; -import org.freeswitch.esl.client.outbound.SocketClient; -import org.freeswitch.esl.client.transport.event.EslEvent; -import org.freeswitch.esl.client.transport.message.EslHeaders.Name; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.InetSocketAddress; -import java.util.List; -import java.util.Map; - -public class OutboundTest { - private static Logger logger = LoggerFactory.getLogger(OutboundTest.class); - private static String sb = "/usr/local/freeswitch/sounds/en/us/callie/ivr/8000/"; - String prompt = sb + "ivr-please_enter_extension_followed_by_pound.wav"; - String failed = sb + "ivr-that_was_an_invalid_entry.wav"; - public static void main(String[] args) { - new OutboundTest(); - } - - public OutboundTest() { - try { - - final Client inboudClient = new Client(); - inboudClient.connect(new InetSocketAddress("localhost", 8021), "ClueCon", 10); - inboudClient.addEventListener((ctx, event) -> logger.info("INBOUND onEslEvent: {}", event.getEventName())); - - final SocketClient outboundServer = new SocketClient( - new InetSocketAddress("localhost", 8084), - () -> new IClientHandler() { - @Override - public void onConnect(Context context, - EslEvent eslEvent) { - - - logger.warn(nameMapToString(eslEvent - .getMessageHeaders(), eslEvent.getEventBodyLines())); - - String uuid = eslEvent.getEventHeaders() - .get("unique-id"); - - logger.warn( - "Creating execute app for uuid {}", - uuid); - - Execute exe = new Execute(context, uuid); - - try { - - exe.answer(); - - String digits = exe.playAndGetDigits(3, - 5, 10, 10 * 1000, "#", prompt, - failed, "^\\d+", 10 * 1000); - logger.warn("Digits collected: {}", - digits); - - - } catch (ExecuteException e) { - logger.error( - "Could not prompt for digits", - e); - - } finally { - try { - exe.hangup(null); - } catch (ExecuteException e) { - logger.error( - "Could not hangup",e); - } - } - - } - - @Override - public void onEslEvent(Context ctx, - EslEvent event) { - logger.info("OUTBOUND onEslEvent: {}", - event.getEventName()); - - } - }); - outboundServer.startAsync(); - - } catch (Throwable t) { - Throwables.propagate(t); - } - } - - public static String nameMapToString(Map map, - List lines) { - StringBuilder sb = new StringBuilder("\nHeaders:\n"); - for (Name key : map.keySet()) { - if(key == null) - continue; - sb.append(key.toString()); - sb.append("\n\t\t\t\t = \t "); - sb.append(map.get(key)); - sb.append("\n"); - } - if (lines != null) { - sb.append("Body Lines:\n"); - for (String line : lines) { - sb.append(line); - sb.append("\n"); - } - } - return sb.toString(); - } -} diff --git a/src/test/java/org/freeswitch/esl/client/ClientExample.java b/src/test/java/org/freeswitch/esl/client/ClientExample.java deleted file mode 100644 index 8e36373..0000000 --- a/src/test/java/org/freeswitch/esl/client/ClientExample.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.freeswitch.esl.client; - -import com.google.common.base.Throwables; -import org.freeswitch.esl.client.inbound.Client; -import org.freeswitch.esl.client.internal.IModEslApi.EventFormat; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.InetSocketAddress; - -public class ClientExample { - private static final Logger L = LoggerFactory.getLogger(ClientExample.class); - - public static void main(String[] args) { - try { - if (args.length < 1) { - System.out.println("Usage: java ClientExample PASSWORD"); - return; - } - - String password = args[0]; - - Client client = new Client(); - - client.addEventListener((ctx, event) -> L.info("Received event: {}", event.getEventName())); - - client.connect(new InetSocketAddress("localhost", 8021), password, 10); - client.setEventSubscriptions(EventFormat.PLAIN, "all"); - - } catch (Throwable t) { - Throwables.propagate(t); - } - } -} diff --git a/src/test/java/org/freeswitch/esl/client/ClientTest.java b/src/test/java/org/freeswitch/esl/client/ClientTest.java new file mode 100644 index 0000000..c3f459b --- /dev/null +++ b/src/test/java/org/freeswitch/esl/client/ClientTest.java @@ -0,0 +1,57 @@ +package org.freeswitch.esl.client; + +import org.freeswitch.esl.client.inbound.Client; +import org.freeswitch.esl.client.transport.event.EslEvent; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ClientTest { + + private static class DemoEventListener implements IEslEventListener { + + @Override + public void eventReceived(EslEvent event) { + System.out.println("eventReceived:" + event.getEventName()); + } + + @Override + public void backgroundJobResultReceived(EslEvent event) { + System.out.println("backgroundJobResultReceived:" + event.getEventName()); + } + } + + public static void main(String[] args) throws InterruptedException { + String host = "localhost"; + int port = 8021; + String password = "ClueCon"; + int timeoutSeconds = 10; + Client inboundClient = new Client(2, 8); + try { + inboundClient.connect(host, port, password, timeoutSeconds); + inboundClient.addEventListener(new DemoEventListener()); + inboundClient.setEventSubscriptions("plain", "all"); + } catch (Exception e) { + System.out.println("connect fail"); + } + + //单独起1个线程,定时检测连接状态 + ScheduledExecutorService service = new ScheduledThreadPoolExecutor(1); + service.scheduleAtFixedRate(() -> { + System.out.println(System.currentTimeMillis() + " " + inboundClient.canSend()); + if (!inboundClient.canSend()) { + try { + //重连 + inboundClient.connect(host, port, password, timeoutSeconds); + inboundClient.cancelEventSubscriptions(); + inboundClient.setEventSubscriptions("plain", "all"); + } catch (Exception e) { + System.out.println("connect fail"); + } + } + }, 1, 500, TimeUnit.MILLISECONDS); + + System.out.println("other process ..."); + } +} diff --git a/src/test/java/org/freeswitch/esl/client/SocketClientTest.java b/src/test/java/org/freeswitch/esl/client/SocketClientTest.java new file mode 100644 index 0000000..7c582fb --- /dev/null +++ b/src/test/java/org/freeswitch/esl/client/SocketClientTest.java @@ -0,0 +1,13 @@ +package org.freeswitch.esl.client; + +import org.freeswitch.esl.client.outbound.SocketClient; +import org.freeswitch.esl.client.outbound.example.SimpleHangupPipelineFactory; + +public class SocketClientTest { + + public static void main(String[] args) { + SocketClient client = new SocketClient(8086, new SimpleHangupPipelineFactory(), 2, 16); + client.start(); + System.out.println("started ..."); + } +} diff --git a/src/test/java/org/freeswitch/esl/client/transport/message/EslFrameDecoderTest.java b/src/test/java/org/freeswitch/esl/client/transport/message/EslFrameDecoderTest.java deleted file mode 100644 index d6a9036..0000000 --- a/src/test/java/org/freeswitch/esl/client/transport/message/EslFrameDecoderTest.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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 static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.embedded.EmbeddedChannel; -import org.junit.Before; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -public class EslFrameDecoderTest -{ - private final Logger log = LoggerFactory.getLogger( this.getClass() ); - - private EmbeddedChannel embedder; - - @Before - public void setupTest() - { - embedder = new EmbeddedChannel(new EslFrameDecoder(64)); - } - - @Test - public void simpleMessage() throws Exception - { - List inputLines = new ArrayList<>(); - inputLines.add( "Content-Type: command/reply" ); - inputLines.add( "Reply-Text: +OK event listener enabled plain" ); - inputLines.add( "" ); - - embedder.writeInbound(createInputBuffer(inputLines, true)); - embedder.finish(); - - EslMessage result = (EslMessage) embedder.readInbound(); - - assertNotNull( result ); - assertEquals( 2, result.getHeaders().size() ); - assertFalse( result.hasContentLength() ); - } - - @Test - public void simpleMessageWithContent() throws Exception - { - List inputLines = new ArrayList<>(); - inputLines.add( "Content-Type: api/response" ); - inputLines.add( "Content-Length: 694" ); - inputLines.add( "" ); - inputLines.add( "=================================================================================================" ); - inputLines.add( " Name Type Data State" ); - inputLines.add( " internal profile sip:mod_sofia@192.168.1.1:5060 RUNNING (0)" ); - inputLines.add( " external profile sip:mod_sofia@yyy.yyy.yyy.yyy:5080 RUNNING (0)" ); - inputLines.add( " iinet gateway sip:02xxxxxxxx@sip.nsw.iinet.net.au REGED" ); - inputLines.add( " clinic profile sip:mod_sofia@yyy.yyy.yyy.yyy:5070 RUNNING (0)" ); - inputLines.add( " 192.168.1.1 alias internal ALIASED" ); - inputLines.add( "=================================================================================================" ); - - embedder.writeInbound(createInputBuffer(inputLines, true)); - - EslMessage result = (EslMessage) embedder.readInbound(); - embedder.finish(); - - assertNotNull( result ); - assertEquals( 2, result.getHeaders().size() ); - assertTrue( result.hasContentLength() ); - assertEquals( 8, result.getBodyLines().size() ); - } - - @Test - public void eventWithSecondContentLength() - { - List inputLines = new ArrayList<>(); - inputLines.add( "Content-Length: 582" ); - inputLines.add( "Content-Type: text/event-plain" ); - inputLines.add( "" ); - inputLines.add( "Job-UUID: 7f4db78a-17d7-11dd-b7a0-db4edd065621" ); - inputLines.add( "Job-Command: originate" ); - inputLines.add( "Job-Command-Arg: sofia/default/1005%20'%26park'" ); - inputLines.add( "Event-Name: BACKGROUND_JOB" ); - inputLines.add( "Core-UUID: 42bdf272-16e6-11dd-b7a0-db4edd065621" ); - inputLines.add( "FreeSWITCH-Hostname: ser" ); - inputLines.add( "FreeSWITCH-IPv4: 192.168.1.104" ); - inputLines.add( "FreeSWITCH-IPv6: 127.0.0.1" ); - inputLines.add( "Event-Date-Local: 2008-05-02%2007%3A37%3A03" ); - inputLines.add( "Event-Date-GMT: Thu,%2001%20May%202008%2023%3A37%3A03%20GMT" ); - inputLines.add( "Event-Date-timestamp: 1209685023894968" ); - inputLines.add( "Event-Calling-File: mod_event_socket.c" ); - inputLines.add( "Event-Calling-Function: api_exec" ); - inputLines.add( "Event-Calling-Line-Number: 609" ); - inputLines.add( "Content-Length: 41" ); - inputLines.add( "" ); - inputLines.add( "+OK 7f4de4bc-17d7-11dd-b7a0-db4edd065621" ); - - embedder.writeInbound(createInputBuffer(inputLines, false)); - - /* - * NB .. there is no trailing '\n' in this event - */ - - EslMessage result = (EslMessage) embedder.readInbound(); - embedder.finish(); - - assertNotNull( result ); - assertEquals( 2, result.getHeaders().size() ); - assertTrue( result.hasContentLength() ); - assertEquals( 17, result.getBodyLines().size() ); - } - - - private ByteBuf createInputBuffer( List inputLines, boolean terminateLastLine ) - { - ByteBuf buffer = Unpooled.buffer(); - - Iterator it = inputLines.iterator(); - while ( it.hasNext() ) - { - buffer.writeBytes( it.next().getBytes() ); - // only terminate last line if asked - if ( it.hasNext() || terminateLastLine ) - { - buffer.writeByte( '\n' ); - } - } - - log.debug( "Created buffer with [{}] bytes", buffer.writerIndex() ); - - return buffer; - } -}