diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml
index c4c954319c..7fb8ed00de 100644
--- a/.idea/codeStyleSettings.xml
+++ b/.idea/codeStyleSettings.xml
@@ -2,7 +2,11 @@
diff --git a/restcomm/configuration/config-scripts/as7-config-scripts/restcomm/autoconfig.d/config-restcomm.sh b/restcomm/configuration/config-scripts/as7-config-scripts/restcomm/autoconfig.d/config-restcomm.sh
index 2fe3b75be5..5aeb1570df 100755
--- a/restcomm/configuration/config-scripts/as7-config-scripts/restcomm/autoconfig.d/config-restcomm.sh
+++ b/restcomm/configuration/config-scripts/as7-config-scripts/restcomm/autoconfig.d/config-restcomm.sh
@@ -634,6 +634,20 @@ configRMSNetworking() {
fi
}
+configAsrDriver() {
+ if [ ! -z "$MG_ASR_DRIVERS" ] && [ ! -z "$MG_ASR_DRIVER_DEFAULT" ]; then
+ FILE=$RESTCOMM_DEPLOY/WEB-INF/conf/restcomm.xml
+ xmlstarlet ed --inplace -d "/restcomm/runtime-settings/mg-asr-drivers" \
+ -s "/restcomm/runtime-settings" -t elem -n mg-asr-drivers \
+ -i "/restcomm/runtime-settings/mg-asr-drivers" -t attr -n default -v "$MG_ASR_DRIVER_DEFAULT" \
+ $FILE
+ for driverName in ${MG_ASR_DRIVERS//,/ }; do
+ xmlstarlet ed --inplace -s "/restcomm/runtime-settings/mg-asr-drivers" -t elem -n "driver" -v "$driverName" \
+ $FILE
+ done
+ fi
+}
+
# MAIN
echo 'Configuring RestComm...'
configRCJavaOpts
@@ -674,4 +688,5 @@ otherRestCommConf
confRcmlserver
confRVD
configRMSNetworking
+configAsrDriver
echo 'Configured RestComm!'
diff --git a/restcomm/configuration/config-scripts/as7-config-scripts/restcomm/restcomm.conf b/restcomm/configuration/config-scripts/as7-config-scripts/restcomm/restcomm.conf
index baef3d71df..e15aeb1369 100755
--- a/restcomm/configuration/config-scripts/as7-config-scripts/restcomm/restcomm.conf
+++ b/restcomm/configuration/config-scripts/as7-config-scripts/restcomm/restcomm.conf
@@ -111,4 +111,10 @@ LOG_LEVEL_COMPONENT_SIPRESTCOMM='INFO' #Log level for "org.mobicents.servlet.sip
LOG_LEVEL_COMPONENT_RESTCOMM='INFO' #Log level for "org.restcomm.connect" module
#AKKA log level. Set the Log level for the AKKA actor system.
-AKKA_LOG_LEVEL='INFO'
\ No newline at end of file
+AKKA_LOG_LEVEL='INFO'
+
+#ASR drivers
+#list of drivers divided with comma, for example: "driver1,driver2,driver3"
+MG_ASR_DRIVERS=""
+#default asr driver to use. It has to be included to MG_ASR_DRIVERS
+MG_ASR_DRIVER_DEFAULT=""
\ No newline at end of file
diff --git a/restcomm/restcomm.application/src/main/webapp/WEB-INF/conf/restcomm.xml b/restcomm/restcomm.application/src/main/webapp/WEB-INF/conf/restcomm.xml
index 42876554e9..e808bcdaec 100644
--- a/restcomm/restcomm.application/src/main/webapp/WEB-INF/conf/restcomm.xml
+++ b/restcomm/restcomm.application/src/main/webapp/WEB-INF/conf/restcomm.xml
@@ -27,6 +27,124 @@
beep.wav
alert.wav
+
+
+ af-ZA
+ id-ID
+ ms-MY
+ ca-ES
+ cs-CZ
+ da-DK
+ de-DE
+ en-AU
+ en-CA
+ en-GB
+ en-IN
+ en-IE
+ en-NZ
+ en-PH
+ en-ZA
+ en-US
+ es-AR
+ es-BO
+ es-CL
+ es-CO
+ es-CR
+ es-EC
+ es-SV
+ es-ES
+ es-US
+ es-GT
+ es-HN
+ es-MX
+ es-NI
+ es-PA
+ es-PY
+ es-PE
+ es-PR
+ es-DO
+ es-UY
+ es-VE
+ eu-ES
+ fil-PH
+ fr-CA
+ fr-FR
+ gl-ES
+ hr-HR
+ zu-ZA
+ is-IS
+ it-IT
+ lt-LT
+ hu-HU
+ nl-NL
+ nb-NO
+ pl-PL
+ pt-BR
+ pt-PT
+ ro-RO
+ sk-SK
+ sl-SI
+ fi-FI
+ sv-SE
+ vi-VN
+ tr-TR
+ el-GR
+ bg-BG
+ ru-RU
+ sr-RS
+ uk-UA
+ he-IL
+ ar-IL
+ ar-JO
+ ar-AE
+ ar-BH
+ ar-DZ
+ ar-SA
+ ar-IQ
+ ar-KW
+ ar-MA
+ ar-TN
+ ar-OM
+ ar-PS
+ ar-QA
+ ar-LB
+ ar-EG
+ fa-IR
+ hi-IN
+ th-TH
+ ko-KR
+ cmn-Hant-TW
+ yue-Hant-HK
+ ja-JP
+ cmn-Hans-HK
+ cmn-Hans-CN
+
+
+
+
+ 5
+
+ 60
+
+
+
+
${restcomm:home}/cache
/restcomm/cache
diff --git a/restcomm/restcomm.commons/src/main/java/org/restcomm/connect/commons/configuration/RestcommConfiguration.java b/restcomm/restcomm.commons/src/main/java/org/restcomm/connect/commons/configuration/RestcommConfiguration.java
index 0c7ba036d5..a5746eb4f1 100644
--- a/restcomm/restcomm.commons/src/main/java/org/restcomm/connect/commons/configuration/RestcommConfiguration.java
+++ b/restcomm/restcomm.commons/src/main/java/org/restcomm/connect/commons/configuration/RestcommConfiguration.java
@@ -26,10 +26,11 @@
import org.apache.commons.configuration.Configuration;
import org.restcomm.connect.commons.configuration.sets.CacheConfigurationSet;
import org.restcomm.connect.commons.configuration.sets.RcmlserverConfigurationSet;
+import org.restcomm.connect.commons.configuration.sets.MainConfigurationSet;
import org.restcomm.connect.commons.configuration.sets.impl.CacheConfigurationSetImpl;
import org.restcomm.connect.commons.configuration.sets.impl.ConfigurationSet;
-import org.restcomm.connect.commons.configuration.sets.MainConfigurationSet;
import org.restcomm.connect.commons.configuration.sets.impl.MainConfigurationSetImpl;
+import org.restcomm.connect.commons.configuration.sets.impl.MgAsrConfigurationSet;
import org.restcomm.connect.commons.configuration.sets.impl.RcmlserverConfigurationSetImpl;
import org.restcomm.connect.commons.configuration.sources.ApacheConfigurationSource;
@@ -55,6 +56,7 @@ public RestcommConfiguration(Configuration apacheConf) {
addConfigurationSet("main", new MainConfigurationSetImpl(apacheCfgSrc));
addConfigurationSet("cache", new CacheConfigurationSetImpl(apacheCfgSrc));
addConfigurationSet("rcmlserver", new RcmlserverConfigurationSetImpl(apacheCfgSrc));
+ addConfigurationSet("mg-asr", new MgAsrConfigurationSet(apacheCfgSrc, apacheConf));
// addConfigurationSet("identity", new IdentityConfigurationSet( new DbConfigurationSource(dbConf)));
// ...
@@ -86,6 +88,10 @@ public CacheConfigurationSet getCache() {
public RcmlserverConfigurationSet getRcmlserver() { return (RcmlserverConfigurationSet) sets.get("rcmlserver"); }
+ public MgAsrConfigurationSet getMgAsr() {
+ return (MgAsrConfigurationSet) sets.get("mg-asr");
+ }
+
// singleton stuff
private static RestcommConfiguration instance;
public static RestcommConfiguration createOnce(Configuration apacheConf) {
diff --git a/restcomm/restcomm.commons/src/main/java/org/restcomm/connect/commons/configuration/sets/impl/MgAsrConfigurationSet.java b/restcomm/restcomm.commons/src/main/java/org/restcomm/connect/commons/configuration/sets/impl/MgAsrConfigurationSet.java
new file mode 100644
index 0000000000..8f7b50fea2
--- /dev/null
+++ b/restcomm/restcomm.commons/src/main/java/org/restcomm/connect/commons/configuration/sets/impl/MgAsrConfigurationSet.java
@@ -0,0 +1,74 @@
+/*
+ * TeleStax, Open Source Cloud Communications
+ * Copyright 2011-2014, Telestax Inc and individual contributors
+ * by the @authors tag.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see
+ *
+ */
+
+package org.restcomm.connect.commons.configuration.sets.impl;
+
+import org.apache.commons.configuration.Configuration;
+import org.restcomm.connect.commons.configuration.sources.ConfigurationSource;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by gdubina on 26.06.17.
+ */
+public class MgAsrConfigurationSet extends ConfigurationSet {
+
+ private final List drivers;
+ private final String defaultDriver;
+ private final List languages;
+ private final String defaultLanguage;
+ private final int asrMRT;
+ private final int defaultGatheringTimeout;
+
+ public MgAsrConfigurationSet(ConfigurationSource source, Configuration config) {
+ super(source);
+ drivers = Collections.unmodifiableList(config.getList("runtime-settings.mg-asr-drivers.driver"));
+ defaultDriver = config.getString("runtime-settings.mg-asr-drivers[@default]");
+ languages = Collections.unmodifiableList(config.getList("runtime-settings.asr-languages.language"));
+ defaultLanguage = config.getString("runtime-settings.asr-languages[@default]");
+ asrMRT = config.containsKey("runtime-settings.asr-mrt-timeout") ? config.getInt("runtime-settings.asr-mrt-timeout") : 60;
+ defaultGatheringTimeout = config.containsKey("runtime-settings.default-gathering-timeout") ? config.getInt("runtime-settings.default-gathering-timeout") : 5;
+ }
+
+ public List getDrivers() {
+ return drivers;
+ }
+
+ public String getDefaultDriver() {
+ return defaultDriver;
+ }
+
+ public List getLanguages() {
+ return languages;
+ }
+
+ public String getDefaultLanguage() {
+ return defaultLanguage;
+ }
+
+ public int getAsrMRT() {
+ return asrMRT;
+ }
+
+ public int getDefaultGatheringTimeout() {
+ return defaultGatheringTimeout;
+ }
+}
diff --git a/restcomm/restcomm.commons/src/test/java/org/restcomm/connect/commons/configuration/MgAsrConfigurationTest.java b/restcomm/restcomm.commons/src/test/java/org/restcomm/connect/commons/configuration/MgAsrConfigurationTest.java
new file mode 100644
index 0000000000..609eabe5aa
--- /dev/null
+++ b/restcomm/restcomm.commons/src/test/java/org/restcomm/connect/commons/configuration/MgAsrConfigurationTest.java
@@ -0,0 +1,92 @@
+/*
+ * TeleStax, Open Source Cloud Communications
+ * Copyright 2011-2014, Telestax Inc and individual contributors
+ * by the @authors tag.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see
+ *
+ */
+
+package org.restcomm.connect.commons.configuration;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.XMLConfiguration;
+import org.junit.Test;
+import org.restcomm.connect.commons.configuration.sets.impl.MgAsrConfigurationSet;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+public class MgAsrConfigurationTest {
+
+ private List expectedDrivers = Arrays.asList("driver1", "driver2");
+
+ private List expectedLanguages = Arrays.asList("en-US", "en-GB", "es-ES", "it-IT", "fr-FR", "pl-PL", "pt-PT");
+
+ private RestcommConfiguration createCfg(final String cfgFileName) throws ConfigurationException,
+ MalformedURLException {
+ URL url = this.getClass().getResource(cfgFileName);
+
+ Configuration xml = new XMLConfiguration(url);
+ return new RestcommConfiguration(xml);
+ }
+
+ public MgAsrConfigurationTest() {
+ super();
+ }
+
+ @Test
+ public void checkMgAsrSection() throws ConfigurationException, MalformedURLException {
+ MgAsrConfigurationSet conf = createCfg("/restcomm.xml").getMgAsr();
+
+ assertTrue(CollectionUtils.isEqualCollection(expectedDrivers, conf.getDrivers()));
+ assertEquals("driver1", conf.getDefaultDriver());
+ }
+
+ @Test
+ public void checkNoMgAsrSection() throws ConfigurationException, MalformedURLException {
+ MgAsrConfigurationSet conf = createCfg("/restcomm-no-mg-asr.xml").getMgAsr();
+
+ assertTrue(CollectionUtils.isEqualCollection(Collections.emptyList(), conf.getDrivers()));
+ assertNull(conf.getDefaultDriver());
+ }
+
+ @Test
+ public void checkAsrLanguagesSection() throws ConfigurationException, MalformedURLException {
+ MgAsrConfigurationSet conf = createCfg("/restcomm.xml").getMgAsr();
+
+ assertTrue(CollectionUtils.isEqualCollection(expectedLanguages, conf.getLanguages()));
+ assertEquals("en-US", conf.getDefaultLanguage());
+ }
+
+ @Test
+ public void checkAsrMRT() throws ConfigurationException, MalformedURLException {
+ MgAsrConfigurationSet conf = createCfg("/restcomm.xml").getMgAsr();
+ assertSame(60, conf.getAsrMRT());
+ }
+
+ @Test
+ public void checkDefaultGatheringTimeout() throws ConfigurationException, MalformedURLException {
+ MgAsrConfigurationSet conf = createCfg("/restcomm.xml").getMgAsr();
+ assertSame(5, conf.getDefaultGatheringTimeout());
+ }
+
+}
diff --git a/restcomm/restcomm.commons/src/test/resources/restcomm-no-mg-asr.xml b/restcomm/restcomm.commons/src/test/resources/restcomm-no-mg-asr.xml
new file mode 100644
index 0000000000..14728d1641
--- /dev/null
+++ b/restcomm/restcomm.commons/src/test/resources/restcomm-no-mg-asr.xml
@@ -0,0 +1,381 @@
+
+
+
+
+
+ 2012-04-24
+
+
+ true
+
+
+ http://127.0.0.1:8080/restcomm/audio
+
+
+ ${restcomm:home}/cache
+ http://127.0.0.1:8080/restcomm/cache
+
+
+ file://${restcomm:home}/recordings
+ http://127.0.0.1:8080/restcomm/recordings
+
+
+ http://127.0.0.1:8080/restcomm/errors
+
+
+
+
+
+ true
+
+
+ false
+
+
+ true
+
+
+ false
+
+ true
+
+
+
+ true
+
+
+ false
+
+
+
+
+
+ 127.0.0.1:5070
+
+
+
+
+ 127.0.0.1:5090
+
+
+
+
+ false
+
+
+ true
+
+
+ false
+
+ 20
+
+ true
+
+
+
+ false
+ restcomm
+ restcomm
+ restcomm_instance_id
+ site_id
+ http://127.0.0.1:2080
+
+
+
+
+
+
+
+
+
+
+
+
+ RestComm:*:Accounts
+ RestComm:*:Applications
+ RestComm:*:Announcements
+ RestComm:Read:AvailablePhoneNumbers
+ RestComm:*:Calls
+ RestComm:*:Clients
+ RestComm:*:Conferences
+ RestComm:Create,Delete,Read:Faxes
+ RestComm:*:IncomingPhoneNumbers
+ RestComm:Read:Notifications
+ RestComm:*:OutgoingCallerIds
+ RestComm:Delete,Read:Recordings
+ RestComm:Read,Modify:SandBoxes
+ RestComm:*:ShortCodes
+ RestComm:Read:SmsMessages
+ RestComm:Read:Transcriptions
+ RestComm:*:OutboundProxies
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ https://backoffice.voipinnovations.com/api2.pl
+
+
+
+
+
+
+ https://api.inetwork.com/v1.0
+
+
+
+
+ https://rest.nexmo.com/
+
+
+
+
+ https://api.voxbone.com/ws-voxbone/services/rest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ restcomm-recordings
+
+
+
+ false
+ 7
+ true
+
+
+
+
+ rms
+
+ 127.0.0.1
+ 5060
+ udp
+ 5
+
+
+
+
+
+
+ 127.0.0.1
+ 2727
+ 127.0.0.1
+ 2427
+ 500
+
+
+
+
+
+
+
+
+ 5000
+
+ strict
+
+ true
+
+
+
+
+
+
+
+ 127.0.0.1:5070
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/restcomm/restcomm.commons/src/test/resources/restcomm.xml b/restcomm/restcomm.commons/src/test/resources/restcomm.xml
index 553cd4f88c..ec766ba743 100644
--- a/restcomm/restcomm.commons/src/test/resources/restcomm.xml
+++ b/restcomm/restcomm.commons/src/test/resources/restcomm.xml
@@ -14,6 +14,29 @@
2012-04-24
+
+
+ en-US
+ en-GB
+ es-ES
+ it-IT
+ fr-FR
+ pl-PL
+ pt-PT
+
+
+
+
+ driver1
+ driver2
+
+
+
+ 60
+
+
+ 5
+
- Sorry, I didn't get your response.
- handle-incoming-call.xml
-
-----
\ No newline at end of file
+= Restcomm RCML – Gather
+
+[[gather]]
+== Gather
+The ** verb supports two modes: *DTMF* and *SPEECH*. In DTMF mode it "gathers" digits that a caller enters into his or her telephone keypad. When the caller is done entering digits, RestComm submits that digits to the provided 'action' URL in an HTTP GET or POST request. In SPEECH mode it "gathers" recognized speech that a caller said. If no input is received before timeout, ** falls through to the next verb in the RestComm document. You may optionally nest **, **, and ** verbs within a ** verb while waiting for input. This allows you to read menu options to the caller while letting her enter a menu selection at any time. After the first digit is received the audio will stop playing.
+
+=== Gather Attributes
+
+[cols=",,",options="header",]
+|======================================================
+|Name |Allowed Values |Default Value
+|action |relative or absolute URL |current document URL
+|method |GET, POST |POST
+|timeout |positive integer |5 seconds
+|finishOnKey |any digit, #, * |#
+|numDigits |integer >= 1 |unlimited
+|input |dtmf, speech |dtmf
+|partialResultCallback |relative or absolute url |none
+|partialResultCallbackMethod |GET, POST |POST
+|language |en-US, en-GB, es-ES, it-IT, fr-FR, pl-PL, pt-PT |en-US
+|hints |"words, phrases that have many words" |none
+|======================================================
+
+* *action.* The 'action' attribute takes an absolute or relative URL as a value. When the caller has finished entering digits RestComm will make a GET or POST request to this URL including the parameters below. If no 'action' is provided, RestComm will by default make a POST request to the current document's URL.
+
+=== Request Parameters
+
+[cols=",",options="header",]
+|=======================================================================
+|Parameter |Description
+|Digits |The digits the caller pressed, excluding the finishOnKey digit.
+|SpeechResult |The transcribed result of the speech.
+|=======================================================================
+
+
+* *method.* The 'method' attribute takes the value 'GET' or 'POST'. This tells RestComm whether to request the 'action' URL via HTTP GET or POST.
+* *timeout.* The 'timeout' attribute sets the limit in seconds that RestComm will wait for the caller to press another digit before moving on and making a request to the 'action' URL. For example, if 'timeout' is '10', RestComm will wait ten seconds for the caller to press another key before submitting the previously entered digits to the 'action' URL. RestComm waits until completing the execution of all nested verbs before beginning the timeout period.
+* *finishOnKey.* The 'finishOnKey' attribute lets you choose one value that submits the received data when entered. For example, if you set 'finishOnKey' to '\#' and the user enters '1234#', RestComm will immediately stop waiting for more input when the '\#' is received and will submit "Digits=1234" to the 'action' URL. Note that the 'finishOnKey' value is not sent. The allowed values are the digits 0-9, '#', '*' and the empty string (set 'finishOnKey' to ''). If the empty string is used, captures all input and no key will end the when pressed. In this case RestComm will submit the entered digits to the 'action' URL only after the timeout has been reached. The value can only be a single character.
+* *numDigits.* The 'numDigits' attribute lets you set the number of digits you are expecting, and submits the data to the 'action' URL once the caller enters that number of digits.
+* *input* A list of inputs that RestComm should accept for . Can be "dtmf" or "speech". Defaults to "dtmf".
+* *partialResultCallback* A relative or fully qualified URL. Is a mandatory attribute for “speech” mode. RestComm will make requests to your partialResultCallback in real-time as speech is recognized.
+
+=== Request Parameters
+
+[cols=",",options="header",]
+|=======================================================================
+|Parameter |Description
+|UnstableSpeechResult |Partially recognized speech the caller said.
+|=======================================================================
+
+
+* *language* The language RestComm should recognize. Defaults to en-US
+* *hints* A list of words or phrases that RestComm should expect during recognition. These are very useful for improving recognition of single words or phrases. Entries into hints should be separated by a comma.
+
+=== Nesting
+You can nest the following verbs within : , ,
+
+=== Examples
+For an example of how to use the ** verb see below.
+
+----
+
+
+ Welcome to TPS.
+ For store hours, press 1.
+ To speak to an agent, press 2.
+ To check your package status, press 3.
+
+
+ Sorry, I didn't get your response.
+ handle-incoming-call.xml
+
+----
diff --git a/restcomm/restcomm.interpreter/pom.xml b/restcomm/restcomm.interpreter/pom.xml
index 8b9c257921..8aa4e7f408 100644
--- a/restcomm/restcomm.interpreter/pom.xml
+++ b/restcomm/restcomm.interpreter/pom.xml
@@ -171,6 +171,11 @@
akka-testkit_2.10
test
-
+
+ org.mockito
+ mockito-core
+ 2.8.9
+ test
+
\ No newline at end of file
diff --git a/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/BaseVoiceInterpreter.java b/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/BaseVoiceInterpreter.java
index ad94618348..6b37b6f1ab 100644
--- a/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/BaseVoiceInterpreter.java
+++ b/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/BaseVoiceInterpreter.java
@@ -45,6 +45,7 @@
import org.restcomm.connect.commons.cache.DiskCacheRequest;
import org.restcomm.connect.commons.cache.DiskCacheResponse;
import org.restcomm.connect.commons.cache.HashGenerator;
+import org.restcomm.connect.commons.configuration.RestcommConfiguration;
import org.restcomm.connect.commons.dao.Sid;
import org.restcomm.connect.commons.faulttolerance.RestcommUntypedActor;
import org.restcomm.connect.commons.fsm.Action;
@@ -83,7 +84,9 @@
import org.restcomm.connect.interpreter.rcml.ParserFailed;
import org.restcomm.connect.interpreter.rcml.Tag;
import org.restcomm.connect.interpreter.rcml.Verbs;
+import org.restcomm.connect.interpreter.rcml.domain.GatherAttributes;
import org.restcomm.connect.mscontrol.api.messages.Collect;
+import org.restcomm.connect.mscontrol.api.messages.CollectedResult;
import org.restcomm.connect.mscontrol.api.messages.MediaGroupResponse;
import org.restcomm.connect.mscontrol.api.messages.Play;
import org.restcomm.connect.mscontrol.api.messages.Record;
@@ -124,6 +127,7 @@
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
+import org.apache.commons.lang.StringUtils;
import static akka.pattern.Patterns.ask;
@@ -165,6 +169,7 @@ public abstract class BaseVoiceInterpreter extends RestcommUntypedActor {
final State sendingSms;
final State hangingUp;
final State sendingEmail;
+ final State continuousGathering;
// final State finished;
// FSM.
@@ -251,14 +256,18 @@ public abstract class BaseVoiceInterpreter extends RestcommUntypedActor {
String finishOnKey;
int numberOfDigits = Short.MAX_VALUE;
StringBuffer collectedDigits;
+ String speechResult;
//Monitoring service
ActorRef monitoring;
final Set transitions = new HashSet();
int recordingDuration = -1;
+ protected RestcommConfiguration restcommConfiguration;
+
public BaseVoiceInterpreter() {
super();
+ restcommConfiguration = RestcommConfiguration.getInstance();
final ActorRef source = self();
// 20 States in common
uninitialized = new State("uninitialized", null, null);
@@ -282,6 +291,7 @@ public BaseVoiceInterpreter() {
sendingSms = new State("sending sms", new SendingSms(source), null);
hangingUp = new State("hanging up", new HangingUp(source), null);
sendingEmail = new State("sending Email", new SendingEmail(source), null);
+ continuousGathering = new State("push partial result", new PartialGathering(source), null);
// Initialize the transitions for the FSM.
transitions.add(new Transition(uninitialized, acquiringAsrInfo));
@@ -361,8 +371,14 @@ public BaseVoiceInterpreter() {
transitions.add(new Transition(processingGatherChildren, gathering));
transitions.add(new Transition(processingGatherChildren, synthesizing));
transitions.add(new Transition(processingGatherChildren, hangingUp));
+
transitions.add(new Transition(gathering, finishGathering));
transitions.add(new Transition(gathering, hangingUp));
+ transitions.add(new Transition(gathering, continuousGathering));
+
+ transitions.add(new Transition(continuousGathering, continuousGathering));
+ transitions.add(new Transition(continuousGathering, finishGathering));
+
transitions.add(new Transition(finishGathering, faxing));
transitions.add(new Transition(finishGathering, sendingEmail));
transitions.add(new Transition(finishGathering, pausing));
@@ -535,7 +551,7 @@ public ActorRef getCache() {
return cache;
}
- ActorRef cache(final String path, final String uri) {
+ protected ActorRef cache(final String path, final String uri) {
final Props props = new Props(new UntypedActorFactory() {
private static final long serialVersionUID = 1L;
@Override
@@ -546,7 +562,7 @@ public UntypedActor create() throws Exception {
return getContext().actorOf(props);
}
- ActorRef downloader() {
+ protected ActorRef downloader() {
final Props props = new Props(new UntypedActorFactory() {
private static final long serialVersionUID = 1L;
@@ -813,7 +829,6 @@ public ActorRef getSynthesizer() {
ActorRef tts(final Configuration ttsConf) {
final String classpath = ttsConf.getString("[@class]");
-
final Props props = new Props(new UntypedActorFactory() {
private static final long serialVersionUID = 1L;
@@ -1191,7 +1206,7 @@ Map getSynthesizeDetails(final Object message) {
}
// Parse the language attribute.
String language = "en";
- attribute = verb.attribute("language");
+ attribute = verb.attribute(GatherAttributes.ATTRIBUTE_LANGUAGE);
if (attribute != null) {
language = attribute.value();
if (language != null && !language.isEmpty()) {
@@ -1284,7 +1299,7 @@ public void execute(final Object message) throws Exception {
}
final NotificationsDao notifications = storage.getNotificationsDao();
String method = "POST";
- Attribute attribute = verb.attribute("method");
+ Attribute attribute = verb.attribute(GatherAttributes.ATTRIBUTE_METHOD);
if (attribute != null) {
method = attribute.value();
if (method != null && !method.isEmpty()) {
@@ -1332,7 +1347,7 @@ public AbstractGatherAction(final ActorRef source) {
protected String finishOnKey(final Tag container) {
String finishOnKey = "#";
- Attribute attribute = container.attribute("finishOnKey");
+ Attribute attribute = container.attribute(GatherAttributes.ATTRIBUTE_FINISH_ON_KEY);
if (attribute != null) {
finishOnKey = attribute.value();
if (finishOnKey != null && !finishOnKey.isEmpty()) {
@@ -1524,14 +1539,40 @@ public Gathering(final ActorRef source) {
@Override
public void execute(final Object message) throws Exception {
+
+ // check mandatory attribute partialResultCallback
+ Attribute partialResultCallbackAttr = verb.attribute(GatherAttributes.ATTRIBUTE_PARTIAL_RESULT_CALLBACK);
final NotificationsDao notifications = storage.getNotificationsDao();
+ // parse attribute "input"
+ Attribute typeAttr = verb.attribute(GatherAttributes.ATTRIBUTE_INPUT);
+ Collect.Type inputType = null;
+ if (typeAttr == null) {
+ inputType = Collect.Type.DTMF;
+ } else {
+ inputType = Collect.Type.parseOrDefault(typeAttr.value(), Collect.Type.DTMF);
+ }
+
+ // parse attribute "language"
+ Attribute langAttr = verb.attribute(GatherAttributes.ATTRIBUTE_LANGUAGE);
+ String defaultLang = restcommConfiguration.getMgAsr().getDefaultLanguage();
+ String lang = parseAttrLanguage(langAttr, defaultLang);
+
+ // parse attribute "hints"
+ String hints = null;
+ Attribute hintsAttr = verb.attribute(GatherAttributes.ATTRIBUTE_HINTS);
+ if (hintsAttr != null && !StringUtils.isEmpty(hintsAttr.value())) {
+ hints = hintsAttr.value();
+ } else if (inputType != Collect.Type.DTMF) {
+ logger.warning("'{}' attribute is null or empty", GatherAttributes.ATTRIBUTE_HINTS);
+ }
+
// Parse finish on key.
finishOnKey = finishOnKey(verb);
// Parse the number of digits.
- Attribute attribute = verb.attribute("numDigits");
+ Attribute attribute = verb.attribute(GatherAttributes.ATTRIBUTE_NUM_DIGITS);
if (attribute != null) {
final String value = attribute.value();
- if (value != null && !value.isEmpty()) {
+ if (!StringUtils.isEmpty(value)) {
try {
numberOfDigits = Integer.parseInt(value);
} catch (final NumberFormatException exception) {
@@ -1542,11 +1583,11 @@ public void execute(final Object message) throws Exception {
}
}
// Parse timeout.
- int timeout = 5;
- attribute = verb.attribute("timeout");
+ int timeout = restcommConfiguration.getMgAsr().getDefaultGatheringTimeout();
+ attribute = verb.attribute(GatherAttributes.ATTRIBUTE_TIME_OUT);
if (attribute != null) {
final String value = attribute.value();
- if (value != null && !value.isEmpty()) {
+ if (!StringUtils.isEmpty(value)) {
try {
timeout = Integer.parseInt(value);
} catch (final NumberFormatException exception) {
@@ -1557,7 +1598,8 @@ public void execute(final Object message) throws Exception {
}
}
// Start gathering.
- final Collect collect = new Collect(gatherPrompts, null, timeout, finishOnKey, numberOfDigits);
+ final Collect collect = new Collect(restcommConfiguration.getMgAsr().getDefaultDriver(), inputType, gatherPrompts,
+ null, timeout, finishOnKey, numberOfDigits, lang, hints, partialResultCallbackAttr != null && !StringUtils.isEmpty(partialResultCallbackAttr.value()));
call.tell(collect, source);
// Some clean up.
gatherChildren = null;
@@ -1565,10 +1607,79 @@ public void execute(final Object message) throws Exception {
dtmfReceived = false;
collectedDigits = new StringBuffer("");
}
+
+ private String parseAttrLanguage(Attribute langAttr, String defaultLang) {
+ if (langAttr != null && !StringUtils.isEmpty(langAttr.value())) {
+ return restcommConfiguration.getMgAsr().getLanguages().contains(langAttr.value()) ? langAttr.value() : defaultLang;
+ } else {
+ logger.warning("Illegal or unsupported attribute value: '{}'. Will be use default value '{}'", langAttr,
+ defaultLang);
+ return defaultLang;
+ }
+ }
}
- final class FinishGathering extends AbstractGatherAction {
-// StringBuffer collectedDigits = new StringBuffer("");
+ private abstract class CallbackGatherAction extends AbstractGatherAction {
+
+ public CallbackGatherAction(ActorRef source) {
+ super(source);
+ }
+
+ protected void execHttpRequest(final NotificationsDao notifications,
+ final Attribute callbackAttr, final Attribute methodAttr,
+ final List parameters) {
+ if (callbackAttr == null) {
+ final Notification notification = notification(ERROR_NOTIFICATION, 11101, "Callback attribute is null");
+ notifications.addNotification(notification);
+ sendMail(notification);
+ final StopInterpreter stop = new StopInterpreter();
+ source.tell(stop, source);
+ logger.error("CallbackAttribute is null, CallbackGatherAction failed");
+ }
+ String action = callbackAttr.value();
+ if (StringUtils.isEmpty(action)) {
+ final Notification notification = notification(ERROR_NOTIFICATION, 11101, "Callback attribute value is null or empty");
+ notifications.addNotification(notification);
+ sendMail(notification);
+ final StopInterpreter stop = new StopInterpreter();
+ source.tell(stop, source);
+ logger.error("Action url is null or empty");
+ }
+ URI target;
+ try {
+ target = URI.create(action);
+ } catch (final Exception exception) {
+ final Notification notification = notification(ERROR_NOTIFICATION, 11100, action
+ + " is an invalid URI.");
+ notifications.addNotification(notification);
+ sendMail(notification);
+ final StopInterpreter stop = new StopInterpreter();
+ source.tell(stop, source);
+ return;
+ }
+ String method = "POST";
+ if (methodAttr != null) {
+ method = methodAttr.value();
+ if (!StringUtils.isEmpty(method)) {
+ if (!"GET".equalsIgnoreCase(method) && !"POST".equalsIgnoreCase(method)) {
+ final Notification notification = notification(WARNING_NOTIFICATION, 14104, method
+ + " is not a valid HTTP method for ");
+ notifications.addNotification(notification);
+ method = "POST";
+ }
+ } else {
+ method = "POST";
+ }
+ }
+ final URI base = request.getUri();
+ final URI uri = UriUtils.resolve(base, target);
+ request = new HttpRequestDescriptor(uri, method, parameters);
+ downloader.tell(request, source);
+ }
+ }
+
+ final class FinishGathering extends CallbackGatherAction {
+
public FinishGathering(final ActorRef source) {
super(source);
}
@@ -1576,16 +1687,16 @@ public FinishGathering(final ActorRef source) {
@Override
public void execute(final Object message) throws Exception {
final NotificationsDao notifications = storage.getNotificationsDao();
- Attribute attribute = verb.attribute("action");
+
String digits = collectedDigits.toString();
collectedDigits = new StringBuffer();
- if(logger.isInfoEnabled()){
- logger.info("Digits collected: "+digits);
+ if (logger.isInfoEnabled()) {
+ logger.info("Digits collected: " + digits);
}
- if (digits.equals(finishOnKey)){
+ if (digits.equals(finishOnKey)) {
digits = "";
}
- if (logger.isDebugEnabled()){
+ if (logger.isDebugEnabled()) {
logger.debug("Digits collected : " + digits);
}
// https://bitbucket.org/telestax/telscale-restcomm/issue/150/verb-is-looping-by-default-and-never
@@ -1593,60 +1704,53 @@ public void execute(final Object message) throws Exception {
// before entering any other digits, Twilio will not make a request to the 'action' URL but instead continue
// processing
// the current TwiML document with the verb immediately following the
- if (attribute != null && (digits != null && !digits.trim().isEmpty())) {
- String action = attribute.value();
- if (action != null && !action.isEmpty()) {
- URI target = null;
- try {
- target = URI.create(action);
- } catch (final Exception exception) {
- final Notification notification = notification(ERROR_NOTIFICATION, 11100, action
- + " is an invalid URI.");
- notifications.addNotification(notification);
- sendMail(notification);
- final StopInterpreter stop = new StopInterpreter();
- source.tell(stop, source);
- return;
- }
- final URI base = request.getUri();
- final URI uri = UriUtils.resolve(base, target);
- // Parse "method".
- String method = "POST";
- attribute = verb.attribute("method");
- if (attribute != null) {
- method = attribute.value();
- if (method != null && !method.isEmpty()) {
- if (!"GET".equalsIgnoreCase(method) && !"POST".equalsIgnoreCase(method)) {
- final Notification notification = notification(WARNING_NOTIFICATION, 14104, method
- + " is not a valid HTTP method for ");
- notifications.addNotification(notification);
- method = "POST";
- }
- } else {
- method = "POST";
- }
- }
- // Redirect to the action url.
- if (digits.endsWith(finishOnKey)) {
- final int finishOnKeyIndex = digits.lastIndexOf(finishOnKey);
- digits = digits.substring(0, finishOnKeyIndex);
- }
- final List parameters = parameters();
- parameters.add(new BasicNameValuePair("Digits", digits));
- request = new HttpRequestDescriptor(uri, method, parameters);
- downloader.tell(request, source);
- return;
+ Attribute action = verb.attribute(GatherAttributes.ATTRIBUTE_ACTION);
+ if (action != null && (!digits.trim().isEmpty() || !StringUtils.isEmpty(speechResult))) {
+ // Redirect to the action url.
+ if (digits.endsWith(finishOnKey)) {
+ final int finishOnKeyIndex = digits.lastIndexOf(finishOnKey);
+ digits = digits.substring(0, finishOnKeyIndex);
}
+ final List parameters = parameters();
+ parameters.add(new BasicNameValuePair("Digits", digits));
+ parameters.add(new BasicNameValuePair("SpeechResult", speechResult));
+
+ execHttpRequest(notifications, action, verb.attribute(GatherAttributes.ATTRIBUTE_METHOD), parameters);
+ speechResult = null;
+ return;
}
- if(logger.isInfoEnabled()){
+ if (logger.isInfoEnabled()) {
logger.info("Attribute, Action or Digits is null, FinishGathering failed, moving to the next available verb");
}
+ speechResult = null;
// Ask the parser for the next action to take.
final GetNextVerb next = new GetNextVerb();
parser.tell(next, source);
}
}
+ final class PartialGathering extends CallbackGatherAction {
+
+ public PartialGathering(final ActorRef source) {
+ super(source);
+ }
+
+ @Override
+ public void execute(final Object message) throws Exception {
+ if (verb.attribute(GatherAttributes.ATTRIBUTE_PARTIAL_RESULT_CALLBACK) != null
+ && !StringUtils.isEmpty(verb.attribute(GatherAttributes.ATTRIBUTE_PARTIAL_RESULT_CALLBACK).value())) {
+ final MediaGroupResponse asrResponse = (MediaGroupResponse) message;
+
+ final List parameters = parameters();
+ parameters.add(new BasicNameValuePair("UnstableSpeechResult", asrResponse.get().getResult()));
+
+ final NotificationsDao notifications = storage.getNotificationsDao();
+ execHttpRequest(notifications, verb.attribute(GatherAttributes.ATTRIBUTE_PARTIAL_RESULT_CALLBACK),
+ verb.attribute(GatherAttributes.ATTRIBUTE_PARTIAL_RESULT_CALLBACK_METHOD), parameters);
+ }
+ }
+ }
+
final class CreatingRecording extends AbstractAction {
public CreatingRecording(final ActorRef source) {
super(source);
@@ -1660,7 +1764,7 @@ public void execute(final Object message) throws Exception {
}
final NotificationsDao notifications = storage.getNotificationsDao();
String finishOnKey = "1234567890*#";
- Attribute attribute = verb.attribute("finishOnKey");
+ Attribute attribute = verb.attribute(GatherAttributes.ATTRIBUTE_FINISH_ON_KEY);
if (attribute != null) {
finishOnKey = attribute.value();
if (finishOnKey != null && !finishOnKey.isEmpty()) {
@@ -1702,7 +1806,7 @@ public void execute(final Object message) throws Exception {
}
}
int timeout = 5;
- attribute = verb.attribute("timeout");
+ attribute = verb.attribute(GatherAttributes.ATTRIBUTE_TIME_OUT);
if (attribute != null) {
final String value = attribute.value();
if (value != null && !value.isEmpty()) {
@@ -1888,7 +1992,7 @@ public void execute(final Object message) throws Exception {
// If action is present redirect to the action URI.
String action = null;
- attribute = verb.attribute("action");
+ attribute = verb.attribute(GatherAttributes.ATTRIBUTE_ACTION);
if (attribute != null) {
action = attribute.value();
if (action != null && !action.isEmpty()) {
@@ -1908,7 +2012,7 @@ public void execute(final Object message) throws Exception {
final URI uri = UriUtils.resolve(base, target);
// Parse "method".
String method = "POST";
- attribute = verb.attribute("method");
+ attribute = verb.attribute(GatherAttributes.ATTRIBUTE_METHOD);
if (attribute != null) {
method = attribute.value();
if (method != null && !method.isEmpty()) {
@@ -1940,8 +2044,15 @@ public void execute(final Object message) throws Exception {
}
parameters.add(new BasicNameValuePair("RecordingDuration", Double.toString(duration)));
if (MediaGroupResponse.class.equals(klass)) {
- final MediaGroupResponse response = (MediaGroupResponse) message;
- parameters.add(new BasicNameValuePair("Digits", response.get()));
+ final MediaGroupResponse response = (MediaGroupResponse) message;
+ Object data = response.get();
+ if (data instanceof CollectedResult) {
+ parameters.add(new BasicNameValuePair("Digits", ((CollectedResult)data).getResult()));
+ } else if(data instanceof String) {
+ parameters.add(new BasicNameValuePair("Digits", (String)data));
+ } else {
+ logger.error("unidentified response recived in MediaGroupResponse: "+response);
+ }
request = new HttpRequestDescriptor(uri, method, parameters);
if (logger.isInfoEnabled()){
logger.info("About to execute Record action to: "+uri);
diff --git a/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/VoiceInterpreter.java b/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/VoiceInterpreter.java
index a8a5995228..0c749ec823 100644
--- a/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/VoiceInterpreter.java
+++ b/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/VoiceInterpreter.java
@@ -38,7 +38,6 @@
import org.joda.time.Interval;
import org.restcomm.connect.asr.AsrResponse;
import org.restcomm.connect.commons.cache.DiskCacheResponse;
-import org.restcomm.connect.commons.configuration.RestcommConfiguration;
import org.restcomm.connect.commons.dao.Sid;
import org.restcomm.connect.commons.fsm.Action;
import org.restcomm.connect.commons.fsm.FiniteStateMachine;
@@ -66,6 +65,8 @@
import org.restcomm.connect.interpreter.rcml.ParserFailed;
import org.restcomm.connect.interpreter.rcml.Tag;
import org.restcomm.connect.interpreter.rcml.Verbs;
+import org.restcomm.connect.interpreter.rcml.domain.GatherAttributes;
+import org.restcomm.connect.mscontrol.api.messages.CollectedResult;
import org.restcomm.connect.mscontrol.api.messages.JoinComplete;
import org.restcomm.connect.mscontrol.api.messages.Left;
import org.restcomm.connect.mscontrol.api.messages.MediaGroupResponse;
@@ -142,7 +143,7 @@
* @author pavel.slegr@telestax.com
* @author maria.farooq@telestax.com
*/
-public final class VoiceInterpreter extends BaseVoiceInterpreter {
+public class VoiceInterpreter extends BaseVoiceInterpreter {
// Logger.
private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
@@ -222,7 +223,7 @@ public final class VoiceInterpreter extends BaseVoiceInterpreter {
private String conferenceNameWithAccountAndFriendlyName;
private Sid callSid;
- private VoiceInterpreter(VoiceInterpreterParams params) {
+ public VoiceInterpreter(VoiceInterpreterParams params) {
super();
final ActorRef source = self();
downloadingRcml = new State("downloading rcml", new DownloadingRcml(source), null);
@@ -313,6 +314,9 @@ private VoiceInterpreter(VoiceInterpreterParams params) {
transitions.add(new Transition(finishGathering, ready));
transitions.add(new Transition(finishGathering, finishGathering));
transitions.add(new Transition(finishGathering, finished));
+ transitions.add(new Transition(continuousGathering, ready));
+ transitions.add(new Transition(continuousGathering, finishGathering));
+ transitions.add(new Transition(continuousGathering, finished));
transitions.add(new Transition(creatingSmsSession, finished));
transitions.add(new Transition(sendingSms, ready));
transitions.add(new Transition(sendingSms, startDialing));
@@ -665,7 +669,7 @@ private void onConferenceResponse(Object message) throws TransitionFailedExcepti
Attribute attribute = null;
if (verb != null) {
- attribute = verb.attribute("action");
+ attribute = verb.attribute(GatherAttributes.ATTRIBUTE_ACTION);
}
if (attribute == null) {
@@ -693,8 +697,6 @@ private void onConferenceResponse(Object message) throws TransitionFailedExcepti
}
}
-
-
private void onConferenceStateChanged(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
final ConferenceStateChanged event = (ConferenceStateChanged) message;
if(logger.isInfoEnabled()) {
@@ -793,7 +795,7 @@ private void onSmsServiceResponse(Object message) throws TransitionFailedExcepti
}
private void onMediaGroupResponse(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
- final MediaGroupResponse response = (MediaGroupResponse) message;
+ final MediaGroupResponse response = (MediaGroupResponse) message;
if(logger.isInfoEnabled()) {
logger.info("MediaGroupResponse, succeeded: " + response.succeeded() + " " + response.cause());
}
@@ -806,35 +808,43 @@ private void onMediaGroupResponse(Object message) throws TransitionFailedExcepti
fsm.transition(message, finishRecording);
} // This is either MMS collected digits or SIP INFO DTMF. If the DTMF is from SIP INFO, then more DTMF might
// come later
- else if (is(gathering) || (is(finishGathering) && !super.dtmfReceived)) {
- final MediaGroupResponse dtmfResponse = (MediaGroupResponse) message;
- if (sender == call) {
- // DTMF using SIP INFO, check if all digits collected here
- collectedDigits.append(dtmfResponse.get());
- // Collected digits == requested num of digits the complete the collect digits
- //Zendesk_34592: if collected digits smaller than numDigits in gather verb
- // when timeout on gather occur, garthering cannot move to finishGathering
- // If collected digits have finish on key at the end then complete the collect digits
- if (collectedDigits.toString().endsWith(finishOnKey)) {
- dtmfReceived = true;
- fsm.transition(message, finishGathering);
- } else {
- if (numberOfDigits != Short.MAX_VALUE) {
- if (collectedDigits.length() == numberOfDigits) {
- dtmfReceived = true;
- fsm.transition(message, finishGathering);
+ else if (is(gathering) || is(continuousGathering) || (is(finishGathering) && !super.dtmfReceived)) {
+ final MediaGroupResponse dtmfResponse = (MediaGroupResponse) message;
+ Object data = dtmfResponse.get();
+ if (data instanceof CollectedResult && ((CollectedResult)data).isAsr() && ((CollectedResult)data).isPartial()) {
+ fsm.transition(message, continuousGathering);
+ } else if (data instanceof CollectedResult && ((CollectedResult)data).isAsr() && !((CollectedResult)data).isPartial() && collectedDigits.length() == 0) {
+ speechResult = ((CollectedResult)data).getResult();
+ fsm.transition(message, finishGathering);
+ } else {
+ if (sender == call) {
+ // DTMF using SIP INFO, check if all digits collected here
+ collectedDigits.append(dtmfResponse.get());
+ // Collected digits == requested num of digits the complete the collect digits
+ //Zendesk_34592: if collected digits smaller than numDigits in gather verb
+ // when timeout on gather occur, garthering cannot move to finishGathering
+ // If collected digits have finish on key at the end then complete the collect digits
+ if (collectedDigits.toString().endsWith(finishOnKey)) {
+ dtmfReceived = true;
+ fsm.transition(message, finishGathering);
+ } else {
+ if (numberOfDigits != Short.MAX_VALUE) {
+ if (collectedDigits.length() == numberOfDigits) {
+ dtmfReceived = true;
+ fsm.transition(message, finishGathering);
+ } else {
+ dtmfReceived = false;
+ return;
+ }
} else {
dtmfReceived = false;
return;
}
- } else {
- dtmfReceived = false;
- return;
}
+ } else {
+ collectedDigits.append(((CollectedResult)data).getResult());
+ fsm.transition(message, finishGathering);
}
- } else {
- collectedDigits.append(dtmfResponse.get());
- fsm.transition(message, finishGathering);
}
} else if (is(bridging)) {
// Finally proceed with call bridging
@@ -904,7 +914,7 @@ private void onTagMessage(Object message) throws TransitionFailedException, Tran
fsm.transition(message, initializingCall);
}
} else if (Verbs.dial.equals(verb.name())) {
- action = verb.attribute("action");
+ action = verb.attribute(GatherAttributes.ATTRIBUTE_ACTION);
if (action != null && dialActionExecuted) {
//We have a new Dial verb that contains Dial Action URL again.
//We set dialActionExecuted to false in order to execute Dial Action again
@@ -1086,6 +1096,10 @@ private void onDownloaderResponse(Object message, State state) throws IOExceptio
logger.debug("statusCode " + response.get().getStatusCode());
}
if (response.succeeded() && HttpStatus.SC_OK == response.get().getStatusCode()) {
+ if (continuousGathering.equals(state)) {
+ //no need change state
+ return;
+ }
if (conferencing.equals(state)) {
//This is the downloader response for Conferencing waitUrl
if (parser != null) {
@@ -1483,7 +1497,7 @@ List parameters() {
final List parameters = new ArrayList();
final String callSid = callInfo.sid().toString();
parameters.add(new BasicNameValuePair("CallSid", callSid));
- parameters.add(new BasicNameValuePair("InstanceId", RestcommConfiguration.getInstance().getMain().getInstanceId()));
+ parameters.add(new BasicNameValuePair("InstanceId", restcommConfiguration.getMain().getInstanceId()));
if (outboundCallInfo != null) {
final String outboundCallSid = outboundCallInfo.sid().toString();
parameters.add(new BasicNameValuePair("OutboundCallSid", outboundCallSid));
@@ -1688,7 +1702,7 @@ private void createInitialCallRecord(CallResponse message) {
// Create a call detail record for the call.
final CallDetailRecord.Builder builder = CallDetailRecord.builder();
builder.setSid(callInfo.sid());
- builder.setInstanceId(RestcommConfiguration.getInstance().getMain().getInstanceId());
+ builder.setInstanceId(restcommConfiguration.getInstance().getMain().getInstanceId());
builder.setDateCreated(callInfo.dateCreated());
builder.setAccountSid(accountId);
builder.setTo(callInfo.to());
@@ -1796,7 +1810,7 @@ public void execute(final Object message) throws IOException {
source.tell(verb, source);
return;
} else if (downloadingRcml.equals(state) || downloadingFallbackRcml.equals(state) || redirecting.equals(state)
- || finishGathering.equals(state) || finishRecording.equals(state) || sendingSms.equals(state)
+ || continuousGathering.equals(state) || finishGathering.equals(state) || finishRecording.equals(state) || sendingSms.equals(state)
|| finishDialing.equals(state) || finishConferencing.equals(state) || is(forking)) {
response = ((DownloaderResponse) message).get();
if (parser != null) {
diff --git a/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/rcml/Attribute.java b/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/rcml/Attribute.java
index f1acfe1c3c..ba6fed3bfa 100644
--- a/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/rcml/Attribute.java
+++ b/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/rcml/Attribute.java
@@ -42,4 +42,12 @@ public String name() {
public String value() {
return value;
}
+
+ @Override
+ public String toString() {
+ return "Attribute{" +
+ "name='" + name + '\'' +
+ ", value='" + value + '\'' +
+ '}';
+ }
}
diff --git a/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/rcml/Parser.java b/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/rcml/Parser.java
index a49b3a3270..b83d0679f4 100644
--- a/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/rcml/Parser.java
+++ b/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/rcml/Parser.java
@@ -20,9 +20,12 @@
package org.restcomm.connect.interpreter.rcml;
import akka.actor.ActorRef;
+import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.restcomm.connect.commons.faulttolerance.RestcommUntypedActor;
+import org.restcomm.connect.interpreter.rcml.domain.GatherAttributes;
+import javax.naming.LimitExceededException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
@@ -31,6 +34,7 @@
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
+import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
@@ -113,7 +117,7 @@ private void start(final Stack builders, final XMLStreamReader stre
builders.push(builder);
}
- private Tag next() {
+ private Tag next() throws LimitExceededException{
if (iterator != null) {
while (iterator.hasNext()) {
final Tag tag = iterator.next();
@@ -124,6 +128,18 @@ private Tag next() {
continue;
}
}
+ if (tag.name().equals(Verbs.gather) && tag.hasAttribute(GatherAttributes.ATTRIBUTE_HINTS) && !StringUtils.isEmpty(tag.attribute(GatherAttributes.ATTRIBUTE_HINTS).value())) {
+ String hotWords = tag.attribute(GatherAttributes.ATTRIBUTE_HINTS).value();
+ List hintList = Arrays.asList(hotWords.split(","));
+ if (hintList.size() > 50) {
+ throw new LimitExceededException("HotWords limit exceeded. There are more than 50 phrases");
+ }
+ for (String hint : hintList) {
+ if (hint.length() > 100) {
+ throw new LimitExceededException("HotWords limit exceeded. Hint with more than 100 characters found");
+ }
+ }
+ }
current = tag;
return current;
}
@@ -168,18 +184,23 @@ public void onReceive(final Object message) throws Exception {
final ActorRef self = self();
final ActorRef sender = sender();
if (GetNextVerb.class.equals(klass)) {
- final Tag verb = next();
- if (verb != null) {
- sender.tell(verb, self);
- if(logger.isDebugEnabled()){
- logger.debug("Parser, next verb: "+verb.toString());
- }
- } else {
- final End end = new End();
- sender.tell(end, sender);
- if(logger.isDebugEnabled()) {
- logger.debug("Parser, next verb: "+end.toString());
+ try {
+ final Tag verb = next();
+ if (verb != null) {
+ sender.tell(verb, self);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Parser, next verb: " + verb.toString());
+ }
+ } else {
+ final End end = new End();
+ sender.tell(end, sender);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Parser, next verb: " + end.toString());
+ }
}
+ } catch (LimitExceededException e) {
+ logger.warn(e.getMessage());
+ sender.tell(new ParserFailed(e, xml), null);
}
}
}
diff --git a/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/rcml/domain/GatherAttributes.java b/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/rcml/domain/GatherAttributes.java
new file mode 100644
index 0000000000..248bee57a2
--- /dev/null
+++ b/restcomm/restcomm.interpreter/src/main/java/org/restcomm/connect/interpreter/rcml/domain/GatherAttributes.java
@@ -0,0 +1,41 @@
+/*
+ * TeleStax, Open Source Cloud Communications
+ * Copyright 2011-2014, Telestax Inc and individual contributors
+ * by the @authors tag.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see
+ *
+ */
+
+package org.restcomm.connect.interpreter.rcml.domain;
+
+/**
+ * Library class with all Gather attributes names.
+ *
+ * @author Dmitriy Nadolenko
+ */
+public class GatherAttributes {
+
+ public static final String ATTRIBUTE_INPUT = "input";
+ public static final String ATTRIBUTE_ACTION = "action";
+ public static final String ATTRIBUTE_METHOD = "method";
+ public static final String ATTRIBUTE_FINISH_ON_KEY = "finishOnKey";
+ public static final String ATTRIBUTE_NUM_DIGITS = "numDigits";
+ public static final String ATTRIBUTE_TIME_OUT = "timeout";
+ public static final String ATTRIBUTE_PARTIAL_RESULT_CALLBACK = "partialResultCallback";
+ public static final String ATTRIBUTE_PARTIAL_RESULT_CALLBACK_METHOD = "partialResultCallbackMethod";
+ public static final String ATTRIBUTE_LANGUAGE = "language";
+ public static final String ATTRIBUTE_HINTS = "hints";
+
+}
diff --git a/restcomm/restcomm.interpreter/src/test/java/org/restcomm/connect/interpreter/GatherSpeechTest.java b/restcomm/restcomm.interpreter/src/test/java/org/restcomm/connect/interpreter/GatherSpeechTest.java
new file mode 100644
index 0000000000..5c08f01d16
--- /dev/null
+++ b/restcomm/restcomm.interpreter/src/test/java/org/restcomm/connect/interpreter/GatherSpeechTest.java
@@ -0,0 +1,625 @@
+/*
+ * TeleStax, Open Source Cloud Communications
+ * Copyright 2011-2014, Telestax Inc and individual contributors
+ * by the @authors tag.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see
+ *
+ */
+
+package org.restcomm.connect.interpreter;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.net.URI;
+import java.util.List;
+
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.XMLConfiguration;
+import org.apache.http.NameValuePair;
+import org.joda.time.DateTime;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.restcomm.connect.commons.cache.DiskCacheRequest;
+import org.restcomm.connect.commons.cache.DiskCacheResponse;
+import org.restcomm.connect.commons.configuration.RestcommConfiguration;
+import org.restcomm.connect.commons.dao.Sid;
+import org.restcomm.connect.commons.fsm.FiniteStateMachine;
+import org.restcomm.connect.commons.fsm.State;
+import org.restcomm.connect.commons.patterns.Observe;
+import org.restcomm.connect.commons.telephony.CreateCallType;
+import org.restcomm.connect.dao.CallDetailRecordsDao;
+import org.restcomm.connect.dao.DaoManager;
+import org.restcomm.connect.http.client.DownloaderResponse;
+import org.restcomm.connect.http.client.HttpRequestDescriptor;
+import org.restcomm.connect.http.client.HttpResponseDescriptor;
+import org.restcomm.connect.interpreter.rcml.MockedActor;
+import org.restcomm.connect.interpreter.rcml.domain.GatherAttributes;
+import org.restcomm.connect.mscontrol.api.messages.Collect;
+import org.restcomm.connect.mscontrol.api.messages.CollectedResult;
+import org.restcomm.connect.mscontrol.api.messages.MediaGroupResponse;
+import org.restcomm.connect.mscontrol.api.messages.Play;
+import org.restcomm.connect.telephony.api.CallInfo;
+import org.restcomm.connect.telephony.api.CallResponse;
+import org.restcomm.connect.telephony.api.CallStateChanged;
+import org.restcomm.connect.telephony.api.GetCallInfo;
+import org.restcomm.connect.telephony.api.Hangup;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
+import akka.actor.Actor;
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import akka.actor.Props;
+import akka.actor.UntypedActorFactory;
+import akka.testkit.JavaTestKit;
+import akka.testkit.TestActorRef;
+
+/**
+ * Created by gdubina on 6/24/17.
+ */
+public class GatherSpeechTest {
+
+ private static ActorSystem system;
+
+ private Configuration configuration;
+
+ private URI requestUri = URI.create("http://127.0.0.1/gather.xml");
+ private URI playUri = URI.create("http://127.0.0.1/play.wav");
+ private URI actionCallbackUri = URI.create("http://127.0.0.1/gather-action.xml");
+ private URI partialCallbackUri = URI.create("http://127.0.0.1/gather-partial.xml");
+
+ private String endRcml = "";
+ private String playRcml = "" + playUri + "";
+ private String gatherRcmlPartial = "" +
+ "";
+ private String gatherRcmlNoPartial = "" +
+ "";
+ private String gatherEmpty = "" +
+ "";
+ private String gatherRcmlWithHints = "" +
+ "";
+
+ public GatherSpeechTest() {
+ super();
+ }
+
+ @BeforeClass
+ public static void before() throws Exception {
+ system = ActorSystem.create();
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ system.shutdown();
+ }
+
+ @Before
+ public void init() {
+ String restcommXmlPath = this.getClass().getResource("/restcomm.xml").getFile();
+ try {
+ configuration = getConfiguration(restcommXmlPath);
+ RestcommConfiguration.createOnce(configuration);
+ } catch (ConfigurationException e) {
+ throw new RuntimeException();
+ }
+ }
+
+ private Configuration getConfiguration(String path) throws ConfigurationException {
+ XMLConfiguration xmlConfiguration = new XMLConfiguration();
+ xmlConfiguration.setDelimiterParsingDisabled(true);
+ xmlConfiguration.setAttributeSplittingDisabled(true);
+ xmlConfiguration.load(path);
+
+ return xmlConfiguration;
+ }
+
+ private HttpResponseDescriptor getOkRcml(URI uri, String rcml) {
+ HttpResponseDescriptor.Builder builder = HttpResponseDescriptor.builder();
+ builder.setURI(uri);
+ builder.setStatusCode(200);
+ builder.setStatusDescription("OK");
+ builder.setContent(rcml);
+ builder.setContentLength(rcml.length());
+ builder.setContentType("text/xml");
+ return builder.build();
+ }
+
+ private TestActorRef createVoiceInterpreter(final ActorRef observer) {
+ //dao
+ final CallDetailRecordsDao recordsDao = mock(CallDetailRecordsDao.class);
+ when(recordsDao.getCallDetailRecord(any(Sid.class))).thenReturn(null);
+
+ final DaoManager storage = mock(DaoManager.class);
+ when(storage.getCallDetailRecordsDao()).thenReturn(recordsDao);
+
+ //actors
+ final ActorRef downloader = new MockedActor("downloader")
+ .add(DiskCacheRequest.class, new DiskCacheRequestProperty(playUri), new DiskCacheResponse(playUri))
+ .asRef(system);
+
+ final ActorRef callManager = new MockedActor("callManager").asRef(system);
+
+ final VoiceInterpreterParams.Builder builder = new VoiceInterpreterParams.Builder();
+ builder.setConfiguration(configuration);
+ builder.setStorage(storage);
+ builder.setCallManager(callManager);
+ builder.setAccount(new Sid("ACae6e420f425248d6a26948c17a9e2acf"));
+ builder.setVersion("2012-04-24");
+ builder.setUrl(requestUri);
+ builder.setMethod("GET");
+ builder.setAsImsUa(false);
+
+ final Props props = new Props(new UntypedActorFactory() {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Actor create() throws Exception {
+ return new VoiceInterpreter(builder.build()) {
+ @Override
+ protected ActorRef downloader() {
+ return observer;
+ }
+
+ @Override
+ protected ActorRef cache(String path, String uri) {
+ return downloader;
+ }
+
+ @Override
+ public ActorRef getCache() {
+ return downloader;
+ }
+ };
+ }
+ });
+ return TestActorRef.create(system, props, "VoiceInterpreter" + System.currentTimeMillis());//system.actorOf(props);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testPartialHangupScenario() throws Exception {
+ new JavaTestKit(system) {
+ {
+ final ActorRef observer = getRef();
+ final ActorRef interpreter = createVoiceInterpreter(observer);
+ interpreter.tell(new StartInterpreter(observer), observer);
+
+ expectMsgClass(GetCallInfo.class);
+ interpreter.tell(new CallResponse(new CallInfo(
+ new Sid("ACae6e420f425248d6a26948c17a9e2acf"),
+ CallStateChanged.State.IN_PROGRESS,
+ CreateCallType.SIP,
+ "inbound",
+ new DateTime(),
+ null,
+ "test", "test",
+ "testTo",
+ null,
+ null,
+ false,
+ false,
+ false,
+ new DateTime())), observer);
+
+ expectMsgClass(Observe.class);
+
+ //wait for rcml downloading
+ HttpRequestDescriptor callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), requestUri);
+
+ interpreter.tell(new DownloaderResponse(getOkRcml(requestUri, gatherRcmlPartial)), observer);
+
+ expectMsgClass(Collect.class);
+
+ //generate partial response1
+ interpreter.tell(new MediaGroupResponse(new CollectedResult("1", true, true)), observer);
+
+ callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), partialCallbackUri);
+ assertEquals(findParam(callback.getParameters(), "UnstableSpeechResult").getValue(), "1");
+
+ interpreter.tell(new DownloaderResponse(getOkRcml(partialCallbackUri, "")), observer);
+
+ //generate partial response2
+ interpreter.tell(new MediaGroupResponse(new CollectedResult("12", true, true)), observer);
+
+ callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), partialCallbackUri);
+ assertEquals(findParam(callback.getParameters(), "UnstableSpeechResult").getValue(), "12");
+
+ //generate final response
+ interpreter.tell(new MediaGroupResponse(new CollectedResult("Hello. World.", true, false)), observer);
+
+ callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), actionCallbackUri);
+ assertEquals(findParam(callback.getParameters(), "SpeechResult").getValue(), "Hello. World.");
+
+ interpreter.tell(new DownloaderResponse(getOkRcml(actionCallbackUri, endRcml)), observer);
+
+ expectMsgClass(Hangup.class);
+ }
+ };
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testFinalResultAndHangupScenario() throws Exception {
+ new JavaTestKit(system) {
+ {
+ final ActorRef observer = getRef();
+ final ActorRef interpreter = createVoiceInterpreter(observer);
+ interpreter.tell(new StartInterpreter(observer), observer);
+
+ expectMsgClass(GetCallInfo.class);
+ interpreter.tell(new CallResponse(new CallInfo(
+ new Sid("ACae6e420f425248d6a26948c17a9e2acf"),
+ CallStateChanged.State.IN_PROGRESS,
+ CreateCallType.SIP,
+ "inbound",
+ new DateTime(),
+ null,
+ "test", "test",
+ "testTo",
+ null,
+ null,
+ false,
+ false,
+ false,
+ new DateTime())), observer);
+
+ expectMsgClass(Observe.class);
+
+ //wait for rcml downloading
+ HttpRequestDescriptor callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), requestUri);
+
+ interpreter.tell(new DownloaderResponse(getOkRcml(requestUri, gatherRcmlNoPartial)), observer);
+
+ expectMsgClass(Collect.class);
+
+ //generate final response
+ interpreter.tell(new MediaGroupResponse(new CollectedResult("Hello. World.", true, false)), observer);
+
+ callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), actionCallbackUri);
+ assertEquals(findParam(callback.getParameters(), "SpeechResult").getValue(), "Hello. World.");
+
+ interpreter.tell(new DownloaderResponse(getOkRcml(actionCallbackUri, endRcml)), observer);
+
+ expectMsgClass(Hangup.class);
+ }
+ };
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testPartialAndPlayScenario() throws Exception {
+ new JavaTestKit(system) {
+ {
+ final ActorRef observer = getRef();
+ final ActorRef interpreter = createVoiceInterpreter(observer);
+ interpreter.tell(new StartInterpreter(observer), observer);
+
+ expectMsgClass(GetCallInfo.class);
+ interpreter.tell(new CallResponse(new CallInfo(
+ new Sid("ACae6e420f425248d6a26948c17a9e2acf"),
+ CallStateChanged.State.IN_PROGRESS,
+ CreateCallType.SIP,
+ "inbound",
+ new DateTime(),
+ null,
+ "test", "test",
+ "testTo",
+ null,
+ null,
+ false,
+ false,
+ false,
+ new DateTime())), observer);
+
+ expectMsgClass(Observe.class);
+
+ //wait for rcml downloading
+ HttpRequestDescriptor callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), requestUri);
+
+ interpreter.tell(new DownloaderResponse(getOkRcml(requestUri, gatherRcmlPartial)), observer);
+
+ expectMsgClass(Collect.class);
+
+ //generate partial response1
+ interpreter.tell(new MediaGroupResponse(new CollectedResult("1", true, true)), observer);
+
+ callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), partialCallbackUri);
+ assertEquals(findParam(callback.getParameters(), "UnstableSpeechResult").getValue(), "1");
+
+ interpreter.tell(new DownloaderResponse(getOkRcml(partialCallbackUri, "")), observer);
+
+ //generate partial response2
+ interpreter.tell(new MediaGroupResponse(new CollectedResult("12", true, true)), observer);
+
+ callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), partialCallbackUri);
+ assertEquals(findParam(callback.getParameters(), "UnstableSpeechResult").getValue(), "12");
+
+ //generate final response
+ interpreter.tell(new MediaGroupResponse(new CollectedResult("Hello. World.", true, false)), observer);
+
+ callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), actionCallbackUri);
+ assertEquals(findParam(callback.getParameters(), "SpeechResult").getValue(), "Hello. World.");
+
+ interpreter.tell(new DownloaderResponse(getOkRcml(actionCallbackUri, playRcml)), observer);
+
+ //wait for new tag: Play
+ DiskCacheRequest diskCacheRequest = expectMsgClass(DiskCacheRequest.class);
+
+ interpreter.tell(new DiskCacheResponse(diskCacheRequest.uri()), observer);
+
+ expectMsgClass(Play.class);
+
+ //simulate play is finished
+ interpreter.tell(new MediaGroupResponse(new CollectedResult("", false, false)), observer);
+
+ expectMsgClass(Hangup.class);
+ }
+ };
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testFinalResultAndPlayScenario() throws Exception {
+ new JavaTestKit(system) {
+ {
+ final ActorRef observer = getRef();
+ final ActorRef interpreter = createVoiceInterpreter(observer);
+ interpreter.tell(new StartInterpreter(observer), observer);
+
+ expectMsgClass(GetCallInfo.class);
+ interpreter.tell(new CallResponse(new CallInfo(
+ new Sid("ACae6e420f425248d6a26948c17a9e2acf"),
+ CallStateChanged.State.IN_PROGRESS,
+ CreateCallType.SIP,
+ "inbound",
+ new DateTime(),
+ null,
+ "test", "test",
+ "testTo",
+ null,
+ null,
+ false,
+ false,
+ false,
+ new DateTime())), observer);
+
+ expectMsgClass(Observe.class);
+
+ //wait for rcml downloading
+ HttpRequestDescriptor callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), requestUri);
+
+ interpreter.tell(new DownloaderResponse(getOkRcml(requestUri, gatherRcmlNoPartial)), observer);
+
+ expectMsgClass(Collect.class);
+
+ //generate final response
+ interpreter.tell(new MediaGroupResponse(new CollectedResult("Hello. World.", true, false)), observer);
+
+ callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), actionCallbackUri);
+ assertEquals(findParam(callback.getParameters(), "SpeechResult").getValue(), "Hello. World.");
+
+ interpreter.tell(new DownloaderResponse(getOkRcml(actionCallbackUri, playRcml)), observer);
+
+ //wait for new tag: Play
+ DiskCacheRequest diskCacheRequest = expectMsgClass(DiskCacheRequest.class);
+
+ interpreter.tell(new DiskCacheResponse(diskCacheRequest.uri()), observer);
+
+ expectMsgClass(Play.class);
+
+ //simulate play is finished
+ interpreter.tell(new MediaGroupResponse(new CollectedResult("", false, false)), observer);
+
+ expectMsgClass(Hangup.class);
+ }
+ };
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testValidateDefaultAttributeValues() throws Exception {
+ new JavaTestKit(system) {
+ {
+ final ActorRef observer = getRef();
+ final ActorRef interpreter = createVoiceInterpreter(observer);
+ interpreter.tell(new StartInterpreter(observer), observer);
+
+ expectMsgClass(GetCallInfo.class);
+ interpreter.tell(new CallResponse(new CallInfo(
+ new Sid("ACae6e420f425248d6a26948c17a9e2acf"),
+ CallStateChanged.State.IN_PROGRESS,
+ CreateCallType.SIP,
+ "inbound",
+ new DateTime(),
+ null,
+ "test", "test",
+ "testTo",
+ null,
+ null,
+ false,
+ false,
+ false,
+ new DateTime())), observer);
+
+ expectMsgClass(Observe.class);
+
+ //wait for rcml downloading
+ HttpRequestDescriptor callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), requestUri);
+
+ interpreter.tell(new DownloaderResponse(getOkRcml(requestUri, gatherEmpty)), observer);
+ Collect collect = expectMsgClass(Collect.class);
+ assertEquals(Collect.Type.DTMF, collect.type());
+ assertEquals("en-US", collect.lang());
+ assertEquals("#", collect.endInputKey());
+ assertSame(5, collect.timeout());
+ assertEquals("google", collect.getDriver());
+ }
+ };
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testOnMediaGroupResponseEmptyCollectedResult() throws Exception {
+ new JavaTestKit(system) {
+ {
+ final ActorRef observer = getRef();
+ final TestActorRef interpreterRef = createVoiceInterpreter(observer);
+ VoiceInterpreter interpreter = interpreterRef.underlyingActor();
+ interpreter.fsm = spy(new FiniteStateMachine(interpreter.continuousGathering, interpreter.transitions));
+ doNothing().when(interpreter.fsm).transition(any(), eq(interpreter.finishGathering));
+ interpreter.collectedDigits = new StringBuffer();
+ interpreterRef.tell(new MediaGroupResponse(new CollectedResult("", false, false)), observer);
+ assertSame(0, interpreter.collectedDigits.length());
+ }
+ };
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testIgnoreRcmlFromPartialCallback() throws Exception {
+ new JavaTestKit(system) {
+ {
+ final ActorRef observer = getRef();
+ final TestActorRef interpreterRef = createVoiceInterpreter(observer);
+ VoiceInterpreter interpreter = interpreterRef.underlyingActor();
+ interpreter.fsm = spy(new FiniteStateMachine(interpreter.continuousGathering, interpreter.transitions));
+ interpreterRef.tell(new DownloaderResponse(getOkRcml(partialCallbackUri, playRcml)), observer);
+ verify(interpreter.fsm, never()).transition(any(), any(State.class));
+ }
+ };
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testDtmfFromMediaServer() throws Exception {
+ new JavaTestKit(system) {
+ {
+ final ActorRef observer = getRef();
+ final TestActorRef interpreterRef = createVoiceInterpreter(observer);
+ VoiceInterpreter interpreter = interpreterRef.underlyingActor();
+ interpreter.fsm = spy(new FiniteStateMachine(interpreter.continuousGathering, interpreter.transitions));
+ doNothing().when(interpreter.fsm).transition(any(), eq(interpreter.finishGathering));
+ interpreter.collectedDigits = new StringBuffer();
+ interpreter.finishOnKey="#";
+ interpreterRef.tell(new MediaGroupResponse(new CollectedResult("5", false, false)), observer);
+ assertEquals("5", interpreter.collectedDigits.toString());
+ }
+ };
+ }
+
+ @Test
+ public void testHintsLimits() {
+ new JavaTestKit(system) {
+ {
+ final ActorRef observer = getRef();
+ final ActorRef interpreter = createVoiceInterpreter(observer);
+ interpreter.tell(new StartInterpreter(observer), observer);
+
+ expectMsgClass(GetCallInfo.class);
+ interpreter.tell(new CallResponse(new CallInfo(
+ new Sid("ACae6e420f425248d6a26948c17a9e2acf"),
+ CallStateChanged.State.IN_PROGRESS,
+ CreateCallType.SIP,
+ "inbound",
+ new DateTime(),
+ null,
+ "test", "test",
+ "testTo",
+ null,
+ null,
+ false,
+ false,
+ false,
+ new DateTime())), observer);
+
+ expectMsgClass(Observe.class);
+
+ //wait for rcml downloading
+ HttpRequestDescriptor callback = expectMsgClass(HttpRequestDescriptor.class);
+ assertEquals(callback.getUri(), requestUri);
+
+ interpreter.tell(new DownloaderResponse(getOkRcml(requestUri, gatherRcmlWithHints)), observer);
+
+ expectMsgClass(Hangup.class);
+ }
+ };
+ }
+
+ private NameValuePair findParam(final List params, final String key) {
+ return Iterables.find(params, new Predicate() {
+ public boolean apply(NameValuePair p) {
+ return key.equals(p.getName());
+ }
+ });
+ }
+
+ public static class DiskCacheRequestProperty extends MockedActor.SimplePropertyPredicate {
+
+ static Function extractor = new Function() {
+ @Override
+ public URI apply(DiskCacheRequest diskCacheRequest) {
+ return diskCacheRequest.uri();
+ }
+ };
+
+ public DiskCacheRequestProperty(URI value) {
+ super(value, extractor);
+ }
+ }
+}
diff --git a/restcomm/restcomm.interpreter/src/test/java/org/restcomm/connect/interpreter/rcml/MockedActor.java b/restcomm/restcomm.interpreter/src/test/java/org/restcomm/connect/interpreter/rcml/MockedActor.java
new file mode 100644
index 0000000000..d3d3018869
--- /dev/null
+++ b/restcomm/restcomm.interpreter/src/test/java/org/restcomm/connect/interpreter/rcml/MockedActor.java
@@ -0,0 +1,140 @@
+/*
+ * TeleStax, Open Source Cloud Communications
+ * Copyright 2011-2014, Telestax Inc and individual contributors
+ * by the @authors tag.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see
+ *
+ */
+
+package org.restcomm.connect.interpreter.rcml;
+
+import akka.actor.*;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import org.mockito.stubbing.Answer1;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by gdubina on 6/24/17.
+ */
+public class MockedActor {
+
+ private final Map, List