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 + + + + + + + + + + + + + + + + + http://api.voicerss.org + + + ca-es + zh-cn + zh-hk + zh-tw + da-dk + nl-nl + en-au + en-ca + en-gb + en-in + en-us + fi-fi + fr-ca + fr-fr + de-de + it-it + ja-jp + ko-kr + nb-no + pl-pl + pt-br + pt-pt + ru-ru + es-mx + es-es + sv-se + + + + + +
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> mapping; + private final String name; + + public MockedActor(String name, Map, List> mapping) { + super(); + this.name = name; + this.mapping = mapping; + } + + public MockedActor(String name) { + this(name, new HashMap, List>()); + } + + public void onReceive(ActorRef self, ActorRef sender, Object msg) throws Throwable { + List responses = this.mapping.get(msg.getClass()); + if (responses == null || responses.isEmpty()) { + System.err.println("MockedActor." + name + ": unhandled request - " + msg); + return; + } + for (Object response : responses) { + if (response instanceof Answer1) { + response = ((Answer1) response).answer(msg); + if (response instanceof Optional) { + Optional resp = ((Optional) response); + if (resp.isPresent()) { + response = resp.get(); + } else { + continue; + } + } + } + + sender.tell(response, self); + return; + } + } + + public MockedActor add(Class clazz, Object resp) { + List responses = mapping.get(clazz); + if (responses == null) { + mapping.put(clazz, responses = new ArrayList<>()); + } + responses.add(resp); + return this; + } + + public MockedActor add(final Class request, final Predicate matcher, final Object resp) { + this.add(request, new Answer1() { + @Override + public Optional answer(Object msg) throws Throwable { + if (matcher.apply(msg)) { + return Optional.fromNullable(resp); + } + return Optional.absent(); + } + }); + return this; + } + + private class MockedActorRef extends UntypedActor { + + public MockedActorRef() { + + } + + @Override + public void onReceive(Object o) throws Exception { + try { + MockedActor.this.onReceive(self(), sender(), o); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + } + + public ActorRef asRef(ActorSystem system) { + return system.actorOf(new Props(new UntypedActorFactory() { + private static final long serialVersionUID = 1L; + + @Override + public Actor create() throws Exception { + return new MockedActorRef(); + } + })); + } + + public static class SimplePropertyPredicate implements Predicate { + + private final P value; + private final Function extractor; + + public SimplePropertyPredicate(P value, Function extractor) { + this.value = value; + this.extractor = extractor; + } + + @Override + public boolean apply(Object t) { + return value.equals(extractor.apply((T) t)); + } + } +} diff --git a/restcomm/restcomm.interpreter/src/test/resources/restcomm.xml b/restcomm/restcomm.interpreter/src/test/resources/restcomm.xml new file mode 100644 index 0000000000..aea2b5f97d --- /dev/null +++ b/restcomm/restcomm.interpreter/src/test/resources/restcomm.xml @@ -0,0 +1,594 @@ + + + + + + 2012-04-24 + + + false + + + /restcomm/audio + + + beep.wav + alert.wav + + + + en-US + en-GB + es-ES + it-IT + fr-FR + pl-PL + pt-PT + + + + + google + + + + ${restcomm:home}/cache + /restcomm/cache + + + true + + + file://${restcomm:home}/recordings + /restcomm/recordings + + + /restcomm/errors + + + + + + true + + + false + + + true + + + + false + + + 5060 + WebRTCGW__1@ + WebRTCGW/1.0 + + + + + 60 + + + false + + true + + + + true + + + false + + + + false + + + + false + + + false + + + + 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 + + + + + + + + + + + + http://GMLC-IP:port/restcomm/gmlc/rest?msisdn= + + + + + + + + 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 + RestComm:*:EmailMessages + RestComm:*:Usage + RestComm:*:Geolocation + + + + + + + + + + + + + + + + + + + + + + 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 + 10 + true + us-east-1 + + secure + false + http://127.0.0.1:8090/s3 + + + + + rms + +
127.0.0.1
+ 5060 + udp + 5 +
+
+ + + + + 127.0.0.1 + 2727 + 127.0.0.1 + 2427 + 500 + + 60 + im + + + + + + + 6000 + + strict + + true + + + + + + + /restcomm-rvd/services + true + 5000 + 500 + + + + + + 127.0.0.1:5070 + + + + + + + + + + test + test + 127.0.0.1 + 2776 + TRANSCEIVER + + test + sms + + 0x34 + -1 + -1 + + + 1 + + 60000 + + 10000 + + 30000 + + 15000 + true + true + + 30000 + + + + + + + + + + + + + + + + + + + + http://vaas.acapela-group.com/Services/Synthesizer + + + + + justine8k + marcia8k + rachel8k graham8k + louise8k + eliska8k + mette8krasmus8k + laura8k ryan8k + sanna8k + claire8k bruno8k + sarah8k klaus8k + dimitris8k + chiara8k vittorio8k + jasmijn8k daan8k + kari8k olav8k + ania8k + celia8k + alyona8k + salma8k mehdi8k + laia8k + maria8k antonio8k + elin8k emil8k + ipek8k + lulu8k + sakura8k + + + + + http://api.voicerss.org + + + ca-es + zh-cn + zh-hk + zh-tw + da-dk + nl-nl + en-au + en-ca + en-gb + en-in + en-us + fi-fi + fr-ca + fr-fr + de-de + it-it + ja-jp + ko-kr + nb-no + pl-pl + pt-br + pt-pt + ru-ru + es-mx + es-es + sv-se + + + + + + + + + + Mizuki + Filiz + TatyanaMaxim + CarmenMaxim + InesCristiano + VitoriaRicardo + MajaJan + LotteRuben + Liv + CarlaGiorgio + DoraKarl + CelineMathieu + Chantal + PenelopeMiguel + ConchitaEnrique + Geraint + Gwyneth + JoannaJoey + Raveena + EmmaBrian + NicoleRussell + MarleneHans + NajaMads + + + + + +
diff --git a/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/AsrSignal.java b/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/AsrSignal.java new file mode 100644 index 0000000000..ae90a78725 --- /dev/null +++ b/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/AsrSignal.java @@ -0,0 +1,155 @@ +/* + * 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.mgcp; + +import jain.protocol.ip.mgcp.pkg.MgcpEvent; +import org.mobicents.protocols.mgcp.jain.pkg.AUMgcpEvent; +import org.restcomm.connect.commons.annotations.concurrency.Immutable; +import org.apache.commons.codec.binary.Hex; + +import java.net.URI; +import java.util.List; + +/** + * Created by gdubina on 6/6/17. + */ +@Immutable +public class AsrSignal { + + public static final MgcpEvent REQUEST_ASR = MgcpEvent.factory("asr", AUMgcpEvent.END_SIGNAL + 1); + + private static final String SPACE_CHARACTER = " "; + + private final String driver; + private final List initialPrompts; + private final String endInputKey; + private final int maximumRecTimer; + private final int waitingInputTimer; + private final int timeAfterSpeech; + private final String hotWords; + private final String lang; + private final String input; + private final int minNumber; + private final int maxNumber; + private final boolean partialResult; + + /** + * + * @param driver ASR driver + * @param lang speech language + * @param initialPrompts Initial prompt + * @param endInputKey end input key, if present stop ASR with dtmf signal + * @param maximumRecTimer maximum recognition time + * @param waitingInputTimer waiting time to detect user input (gather timeout) + * @param timeAfterSpeech amount of silence necessary after the end of speech (gather timeout) + * @param hotWords hints for speech analyzer tool + * @param input "dtmf", "speech", "dtmf speech" + * @param numberOfDigits number of digits system expects from User + * @param partialResult whether RC needs partial results + */ + + public AsrSignal(String driver, String lang, List initialPrompts, String endInputKey, int maximumRecTimer, int waitingInputTimer, + int timeAfterSpeech, String hotWords, String input, int numberOfDigits, boolean partialResult) { + this.driver = driver; + this.initialPrompts = initialPrompts; + this.endInputKey = endInputKey; + this.maximumRecTimer = maximumRecTimer; + this.waitingInputTimer = waitingInputTimer; + this.timeAfterSpeech = timeAfterSpeech; + this.hotWords = hotWords; + this.lang = lang; + this.input = input; + //RMS expects two parameters but Collect in RVD has only one + this.minNumber = numberOfDigits; + this.maxNumber = numberOfDigits; + this.partialResult = partialResult; + } + + @Override + public String toString() { + final StringBuilder buffer = new StringBuilder(); + if (!initialPrompts.isEmpty()) { + buffer.append("ip="); + for (int index = 0; index < initialPrompts.size(); index++) { + buffer.append(initialPrompts.get(index)); + if (index < initialPrompts.size() - 1) { + //https://github.com/RestComm/Restcomm-Connect/issues/1988 + buffer.append(","); + } + } + } + + if (buffer.length() > 0) + buffer.append(SPACE_CHARACTER); + buffer.append("dr=").append(driver); + + if (buffer.length() > 0) + buffer.append(SPACE_CHARACTER); + buffer.append("ln=").append(lang); + + if (endInputKey != null) { + if (buffer.length() > 0) + buffer.append(SPACE_CHARACTER); + buffer.append("eik=").append(endInputKey); + } + if (maximumRecTimer > 0) { + if (buffer.length() > 0) + buffer.append(SPACE_CHARACTER); + buffer.append("mrt=").append(maximumRecTimer * 10); + } + if (waitingInputTimer > 0) { + if (buffer.length() > 0) + buffer.append(SPACE_CHARACTER); + buffer.append("wit=").append(waitingInputTimer * 10); + } + if (timeAfterSpeech > 0) { + if (buffer.length() > 0) + buffer.append(SPACE_CHARACTER); + buffer.append("pst=").append(timeAfterSpeech * 10); + } + if (hotWords != null) { + if (buffer.length() > 0) + buffer.append(SPACE_CHARACTER); + buffer.append("hw=").append(Hex.encodeHexString(hotWords.getBytes())); + } + if (input != null) { + if (buffer.length() > 0) + buffer.append(SPACE_CHARACTER); + buffer.append("in=").append(input); + } + if (minNumber > 0) { + if (buffer.length() > 0) + buffer.append(SPACE_CHARACTER); + buffer.append("mn=").append(minNumber); + } + if (maxNumber > 0) { + if (buffer.length() > 0) + buffer.append(SPACE_CHARACTER); + buffer.append("mx=").append(maxNumber); + } + if (partialResult) { + if (buffer.length() > 0) + buffer.append(SPACE_CHARACTER); + buffer.append("pr=").append(partialResult); + } + return buffer.toString(); + } +} diff --git a/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/CollectedResult.java b/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/CollectedResult.java new file mode 100644 index 0000000000..ccef6e0c7f --- /dev/null +++ b/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/CollectedResult.java @@ -0,0 +1,58 @@ +/* + * 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.mgcp; + +/** + * Created by gdubina on 6/6/17. + */ +public class CollectedResult { + + private final String result; + private final boolean isAsr; + private final boolean isPartial; + + public CollectedResult(String result, boolean isAsr, boolean isPartial) { + this.result = result; + this.isAsr = isAsr; + this.isPartial = isPartial; + } + + public String getResult() { + return result; + } + + public boolean isAsr() { + return isAsr; + } + + public boolean isPartial() { + return isPartial; + } + + @Override + public String toString() { + return "CollectedResult{" + + "result='" + result + '\'' + + ", isAsr=" + isAsr + + ", isPartial=" + isPartial + + '}'; + } +} diff --git a/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/IvrEndpoint.java b/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/IvrEndpoint.java index 8077eb2a07..8d4198c408 100644 --- a/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/IvrEndpoint.java +++ b/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/IvrEndpoint.java @@ -19,6 +19,18 @@ */ package org.restcomm.connect.mgcp; +import static jain.protocol.ip.mgcp.message.parms.ReturnCode.Transaction_Executed_Normally; + +import java.util.Map; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.lang.StringUtils; +import org.mobicents.protocols.mgcp.jain.pkg.AUMgcpEvent; +import org.mobicents.protocols.mgcp.jain.pkg.AUPackage; +import org.restcomm.connect.commons.patterns.Observe; +import org.restcomm.connect.commons.patterns.StopObserving; + import akka.actor.ActorRef; import jain.protocol.ip.mgcp.JainIPMgcpException; import jain.protocol.ip.mgcp.JainMgcpResponseEvent; @@ -35,15 +47,6 @@ import jain.protocol.ip.mgcp.message.parms.ReturnCode; import jain.protocol.ip.mgcp.pkg.MgcpEvent; import jain.protocol.ip.mgcp.pkg.PackageName; -import org.mobicents.protocols.mgcp.jain.pkg.AUMgcpEvent; -import org.mobicents.protocols.mgcp.jain.pkg.AUPackage; -import org.restcomm.connect.commons.patterns.Observe; -import org.restcomm.connect.commons.patterns.StopObserving; - -import java.util.HashMap; -import java.util.Map; - -import static jain.protocol.ip.mgcp.message.parms.ReturnCode.Transaction_Executed_Normally; /** * @author quintana.thomas@gmail.com (Thomas Quintana) @@ -68,6 +71,11 @@ public IvrEndpoint(final ActorRef gateway, final MediaSession session, final Not this.agent = agent; } + private void sendAsr(final AsrSignal message) { + MgcpEvent event = AsrSignal.REQUEST_ASR.withParm(message.toString()); + sendRequest(new EventName(PACKAGE_NAME, event), REQUESTED_EVENTS); + } + private void send(final Object message) { final Class klass = message.getClass(); final String parameters = message.toString(); @@ -79,12 +87,16 @@ private void send(final Object message) { } else if (PlayRecord.class.equals(klass)) { event = AUMgcpEvent.aupr.withParm(parameters); } + sendRequest(new EventName(PACKAGE_NAME, event), REQUESTED_EVENTS); + } + + private void sendRequest(EventName reqSignal, RequestedEvent[] reqEvents) { final EventName[] signal = new EventName[1]; - signal[0] = new EventName(PACKAGE_NAME, event); + signal[0] = reqSignal; final RequestIdentifier requestId = new RequestIdentifier(DEFAULT_REQUEST_ID); final NotificationRequest request = new NotificationRequest(self(), id, requestId); request.setNotifiedEntity(agent); - request.setRequestedEvents(REQUESTED_EVENTS); + request.setRequestedEvents(reqEvents); request.setSignalRequests(signal); gateway.tell(request, self()); } @@ -129,6 +141,8 @@ public void onReceive(final Object message) throws Exception { onDestroyEndpoint((DestroyEndpoint) message, self, sender); } else if (Play.class.equals(klass) || PlayCollect.class.equals(klass) || PlayRecord.class.equals(klass)) { send(message); + } else if (AsrSignal.class.equals(klass)) { + sendAsr((AsrSignal) message); } else if (StopEndpoint.class.equals(klass)) { stop(message); } else if (Notify.class.equals(klass)) { @@ -146,7 +160,7 @@ private void fail(final int code) { final String error = Integer.toString(code); final String message = "The IVR request failed with the following error code " + error; final JainIPMgcpException exception = new JainIPMgcpException(message); - final IvrEndpointResponse response = new IvrEndpointResponse(exception); + final IvrEndpointResponse response = new IvrEndpointResponse(exception); for (final ActorRef observer : observers) { observer.tell(response, self); } @@ -161,6 +175,28 @@ private void response(final Object message) { } } + private void handleAsrr(final Map parameters, int code) { + if (parameters.containsKey("asrr")) { + String asrr = parameters.get("asrr"); + if (!StringUtils.isEmpty(asrr)) { + try { + asrr = new String(Hex.decodeHex(asrr.toCharArray())); + } catch (DecoderException e) { + logger.error("asrr parameter cannot be decoded.", e); + fail(code); + } + } + // Notify the observers that the event successfully completed. + final IvrEndpointResponse result = new IvrEndpointResponse(new CollectedResult(asrr, true, code == 101)); + for (final ActorRef observer : observers) { + observer.tell(result, self()); + } + } else { + logger.error("asrr parameter is missing"); + fail(code); + } + } + private void notification(final Object message) { final Notify notification = (Notify) message; final ActorRef self = self(); @@ -173,43 +209,36 @@ private void notification(final Object message) { final EventName[] observedEvents = notification.getObservedEvents(); if (observedEvents.length == 1) { final MgcpEvent event = observedEvents[0].getEventIdentifier(); - final Map parameters = parse(event.getParms()); + final Map parameters = MgcpUtil.parseParameters(event.getParms()); final int code = Integer.parseInt(parameters.get("rc")); switch (code) { + case 323: // provisioning error case 326: // No digits case 327: // No speech case 328: // Spoke too long case 329: // Digit pattern not matched - case 100: { // Success + case 331: // Speech pattern not detected + case 100: { // Success(final result) String digits = parameters.get("dc"); - if (digits == null) { - digits = EMPTY_STRING; - } - // Notify the observers that the event successfully completed. - final IvrEndpointResponse result = new IvrEndpointResponse(digits); - for (final ActorRef observer : observers) { - observer.tell(result, self); + if (digits == null && parameters.get("asrr") != null) { + handleAsrr(parameters, code); + } else { + final IvrEndpointResponse result = new IvrEndpointResponse( + new CollectedResult(digits == null ? EMPTY_STRING : digits, AsrSignal.REQUEST_ASR.getName().equals(event.getName()), false)); + for (final ActorRef observer : observers) { + observer.tell(result, self); + } } break; } + case 101: { // Success(partial result) + handleAsrr(parameters, code); + break; + } default: { fail(code); } } } } - - private Map parse(final String input) { - final Map parameters = new HashMap(); - final String[] tokens = input.split(" "); - for (final String token : tokens) { - final String[] values = token.split("="); - if (values.length == 1) { - parameters.put(values[0], null); - } else if (values.length == 2) { - parameters.put(values[0], values[1]); - } - } - return parameters; - } } diff --git a/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/MediaGateway.java b/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/MediaGateway.java index a892397fb9..2acd13bce5 100644 --- a/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/MediaGateway.java +++ b/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/MediaGateway.java @@ -18,7 +18,6 @@ * */ package org.restcomm.connect.mgcp; - import akka.actor.Actor; import akka.actor.ActorRef; import akka.actor.Props; @@ -38,6 +37,7 @@ import jain.protocol.ip.mgcp.message.NotificationRequest; import jain.protocol.ip.mgcp.message.Notify; import jain.protocol.ip.mgcp.message.parms.ConnectionIdentifier; +import jain.protocol.ip.mgcp.message.parms.EventName; import jain.protocol.ip.mgcp.message.parms.NotifiedEntity; import org.restcomm.connect.commons.faulttolerance.RestcommUntypedActor; import org.restcomm.connect.commons.util.RevolvingCounter; @@ -262,6 +262,11 @@ private void powerOn(final Object message) { transactionIdPool = new RevolvingCounter(1, Long.MAX_VALUE); } + private boolean isPartialNotify(final Notify notify) { + EventName[] events = notify.getObservedEvents(); + return events != null && events.length != 0 && MgcpUtil.isPartialNotify(events[events.length - 1]); + } + @Override public void processMgcpCommandEvent(final JainMgcpCommandEvent event) { final int value = event.getObjectIdentifier(); @@ -269,7 +274,13 @@ public void processMgcpCommandEvent(final JainMgcpCommandEvent event) { case Constants.CMD_NOTIFY: { final Notify notify = (Notify) event; final String id = notify.getRequestIdentifier().toString(); - final ActorRef listener = notificationListeners.remove(id); + + final ActorRef listener; + if (isPartialNotify(notify)) { + listener = notificationListeners.get(id); + } else { + listener = notificationListeners.remove(id); + } if (listener != null) { listener.tell(notify, self()); } diff --git a/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/MgcpUtil.java b/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/MgcpUtil.java new file mode 100644 index 0000000000..d3afc0feca --- /dev/null +++ b/restcomm/restcomm.mgcp/src/main/java/org/restcomm/connect/mgcp/MgcpUtil.java @@ -0,0 +1,58 @@ +/* + * 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.mgcp; + +import jain.protocol.ip.mgcp.message.parms.EventName; +import jain.protocol.ip.mgcp.pkg.MgcpEvent; +import org.apache.commons.lang.math.NumberUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by gdubina on 23.06.17. + */ +public final class MgcpUtil { + + public static final int RETURNCODE_PARTIAL = 101; + + private MgcpUtil(){} + + public static Map parseParameters(final String input) { + final Map parameters = new HashMap(); + final String[] tokens = input.split(" "); + for (final String token : tokens) { + final String[] values = token.split("="); + if (values.length == 1) { + parameters.put(values[0], null); + } else if (values.length == 2) { + parameters.put(values[0], values[1]); + } + } + return parameters; + } + + public static boolean isPartialNotify(EventName lastEvent){ + final MgcpEvent event = lastEvent.getEventIdentifier(); + final Map parameters = MgcpUtil.parseParameters(event.getParms()); + return NumberUtils.toInt(parameters.get("rc")) == RETURNCODE_PARTIAL; + } +} diff --git a/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/AbstractMockMediaGateway.java b/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/AbstractMockMediaGateway.java index 10179c443f..8f9eccfa1f 100644 --- a/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/AbstractMockMediaGateway.java +++ b/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/AbstractMockMediaGateway.java @@ -175,6 +175,8 @@ public void onReceive(final Object message) throws Exception { request(message, sender); } else if (message instanceof JainMgcpResponseEvent) { response(message, sender); + } else { + throw new IllegalArgumentException("Unsupported operation !!!"); } } diff --git a/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/AsrSignalTest.java b/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/AsrSignalTest.java new file mode 100644 index 0000000000..5710f310c6 --- /dev/null +++ b/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/AsrSignalTest.java @@ -0,0 +1,88 @@ +/* + * 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.mgcp; + +import org.junit.Before; +import org.junit.Test; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Dmitriy Nadolenko + */ +public class AsrSignalTest { + + public static final String DEFAULT_LANG = "en-US"; + + private String driver; + private List initialPrompts; + private String endInputKey; + private int maximumRecTimer; + private int waitingInputTimer; + private int timeAfterSpeech; + private String hotWords; + private String input; + private int numberOfDigits; + private boolean partialResult; + + @Before + public void init() { + driver = "no_name_driver"; + initialPrompts = Collections.singletonList(URI.create("hello.wav")); + endInputKey = "#"; + maximumRecTimer = 10; + waitingInputTimer = 10; + timeAfterSpeech = 5; + hotWords = "Wait"; + input = "dtmf_speech"; + numberOfDigits = 1; + partialResult = true; + } + + @Test + public void testFormatting() { + String expectedResult = "ip=hello.wav dr=no_name_driver ln=en-US eik=# mrt=100 wit=100 pst=50 hw=57616974 in=dtmf_speech mn=1 mx=1 pr=true"; + AsrSignal asrSignal = new AsrSignal(driver, DEFAULT_LANG, initialPrompts, endInputKey, maximumRecTimer, waitingInputTimer, + timeAfterSpeech, hotWords, input, numberOfDigits, partialResult); + String actualResult = asrSignal.toString(); + + assertEquals(expectedResult, actualResult); + } + + @Test + public void testFormattingWithMultiplePrompts() { + initialPrompts = new ArrayList() {{ + add(URI.create("hello.wav")); + add(URI.create("world.wav")); + }}; + String expectedResult = "ip=hello.wav,world.wav dr=no_name_driver ln=en-US eik=# mrt=100 wit=100 pst=50 hw=57616974 in=dtmf_speech mn=1 mx=1 pr=true"; + AsrSignal asrSignal = new AsrSignal(driver, DEFAULT_LANG, initialPrompts, endInputKey, maximumRecTimer, waitingInputTimer, + timeAfterSpeech, hotWords, input, numberOfDigits, partialResult); + String actualResult = asrSignal.toString(); + + assertEquals(expectedResult, actualResult); + } +} diff --git a/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/IvrAsrEndpointTest.java b/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/IvrAsrEndpointTest.java new file mode 100644 index 0000000000..3ca946ca10 --- /dev/null +++ b/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/IvrAsrEndpointTest.java @@ -0,0 +1,306 @@ +/* + * 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.mgcp; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.net.URI; +import java.util.Collections; + +import org.apache.commons.codec.binary.Hex; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mobicents.protocols.mgcp.jain.pkg.AUMgcpEvent; +import org.mobicents.protocols.mgcp.jain.pkg.AUPackage; +import org.restcomm.connect.commons.patterns.Observe; +import org.restcomm.connect.commons.patterns.Observing; +import org.restcomm.connect.commons.patterns.StopObserving; + +import akka.actor.ActorRef; +import akka.actor.ActorSystem; +import akka.actor.Props; +import akka.testkit.JavaTestKit; +import jain.protocol.ip.mgcp.JainMgcpResponseEvent; +import jain.protocol.ip.mgcp.message.NotificationRequest; +import jain.protocol.ip.mgcp.message.NotificationRequestResponse; +import jain.protocol.ip.mgcp.message.Notify; +import jain.protocol.ip.mgcp.message.parms.EventName; +import jain.protocol.ip.mgcp.message.parms.ReturnCode; +import jain.protocol.ip.mgcp.pkg.MgcpEvent; + +/** + * @author Dmitriy Nadolenko + * @version 1.0 + * @since 1.0 + */ +public class IvrAsrEndpointTest { + + private static final String ASR_RESULT_TEXT = "Super_text"; + private static final String ASR_RESULT_TEXT_HEX = Hex.encodeHexString(ASR_RESULT_TEXT.getBytes()); + private static final String HINTS = "hint 1, hint 2"; + + private static ActorSystem system; + + public IvrAsrEndpointTest() { + super(); + } + + @BeforeClass + public static void before() throws Exception { + system = ActorSystem.create(); + } + + @AfterClass + public static void after() throws Exception { + system.shutdown(); + } + + @Test + @SuppressWarnings("unchecked") + public void testSuccessfulAsrScenario() { + new JavaTestKit(system) { + { + final ActorRef observer = getRef(); + // Create a new mock media gateway to simulate the real thing. + final ActorRef gateway = system.actorOf(new Props(IvrAsrEndpointTest.MockAsrMediaGateway.class)); + // Create a media session. This is just an identifier that groups + // a set of end points, connections, and lists in to one call. + gateway.tell(new CreateMediaSession(), observer); + final MediaGatewayResponse mediaSessionResponse = expectMsgClass(MediaGatewayResponse.class); + assertTrue(mediaSessionResponse.succeeded()); + final MediaSession session = mediaSessionResponse.get(); + // Create an IVR end point. + gateway.tell(new CreateIvrEndpoint(session), observer); + final MediaGatewayResponse endpointResponse = expectMsgClass(MediaGatewayResponse.class); + assertTrue(endpointResponse.succeeded()); + final ActorRef endpoint = endpointResponse.get(); + // Start observing events from the IVR end point. + endpoint.tell(new Observe(observer), observer); + final Observing observingResponse = expectMsgClass(Observing.class); + assertTrue(observingResponse.succeeded()); + + AsrSignal asr = new AsrSignal("no_name_driver", "en-US", Collections.singletonList(URI.create("hello.wav")), "#", 10, 10, 10, HINTS, "dtmf_speech", 1, true); + endpoint.tell(asr, observer); + final IvrEndpointResponse ivrResponse = expectMsgClass(IvrEndpointResponse.class); + assertTrue(ivrResponse.succeeded()); + CollectedResult collectedResult = (CollectedResult)ivrResponse.get(); + assertTrue(ASR_RESULT_TEXT.equals(collectedResult.getResult())); + assertTrue(collectedResult.isAsr()); + + final IvrEndpointResponse ivrResponse2 = expectMsgClass(IvrEndpointResponse.class); + assertTrue(ivrResponse2.succeeded()); + collectedResult = (CollectedResult)ivrResponse2.get(); + assertTrue(ASR_RESULT_TEXT.equals(collectedResult.getResult())); + assertTrue(collectedResult.isAsr()); + + + final IvrEndpointResponse ivrResponse3 = expectMsgClass(IvrEndpointResponse.class); + collectedResult = (CollectedResult)ivrResponse3.get(); + assertTrue(ivrResponse3.succeeded()); + assertTrue(ASR_RESULT_TEXT.equals(collectedResult.getResult())); + assertTrue(collectedResult.isAsr()); + + // Stop observing events from the IVR end point. + endpoint.tell(new StopObserving(observer), observer); + } + }; + } + + @Test + @SuppressWarnings("unchecked") + public void testFailureScenario() { + new JavaTestKit(system) { + { + final ActorRef observer = getRef(); + // Create a new mock media gateway to simulate the real thing. + final ActorRef gateway = system.actorOf(new Props(IvrAsrEndpointTest.FailingMockAsrMediaGateway.class)); + // Create a media session. This is just an identifier that groups + // a set of end points, connections, and lists in to one call. + gateway.tell(new CreateMediaSession(), observer); + final MediaGatewayResponse mediaSessionResponse = expectMsgClass(MediaGatewayResponse.class); + assertTrue(mediaSessionResponse.succeeded()); + final MediaSession session = mediaSessionResponse.get(); + // Create an IVR end point. + gateway.tell(new CreateIvrEndpoint(session), observer); + final MediaGatewayResponse endpointResponse = expectMsgClass(MediaGatewayResponse.class); + assertTrue(endpointResponse.succeeded()); + final ActorRef endpoint = endpointResponse.get(); + // Start observing events from the IVR end point. + endpoint.tell(new Observe(observer), observer); + final Observing observingResponse = expectMsgClass(Observing.class); + assertTrue(observingResponse.succeeded()); + + AsrSignal asr = new AsrSignal("no_name_driver", "en-US", Collections.singletonList(URI.create("hello.wav")), "#", 10, 10, 10, HINTS, "dtmf_speech", 1, true); + endpoint.tell(asr, observer); + final IvrEndpointResponse ivrResponse = expectMsgClass(IvrEndpointResponse.class); + assertFalse(ivrResponse.succeeded()); + String errorMessage = "jain.protocol.ip.mgcp.JainIPMgcpException: The IVR request failed with the following error code 300"; + assertTrue(ivrResponse.cause().toString().equals(errorMessage)); + assertTrue(ivrResponse.get() == null); + } + }; + } + + @Test + @SuppressWarnings("unchecked") + public void testEndSignal() { + new JavaTestKit(system) { + { + final ActorRef observer = getRef(); + // Create a new mock media gateway to simulate the real thing. + final ActorRef gateway = system.actorOf(new Props(MockAsrWithEndSignal.class)); + // Create a media session. This is just an identifier that groups + // a set of end points, connections, and lists in to one call. + gateway.tell(new CreateMediaSession(), observer); + final MediaGatewayResponse mediaSessionResponse = expectMsgClass(MediaGatewayResponse.class); + assertTrue(mediaSessionResponse.succeeded()); + final MediaSession session = mediaSessionResponse.get(); + // Create an IVR end point. + gateway.tell(new CreateIvrEndpoint(session), observer); + final MediaGatewayResponse endpointResponse = expectMsgClass(MediaGatewayResponse.class); + assertTrue(endpointResponse.succeeded()); + final ActorRef endpoint = endpointResponse.get(); + // Start observing events from the IVR end point. + endpoint.tell(new Observe(observer), observer); + final Observing observingResponse = expectMsgClass(Observing.class); + assertTrue(observingResponse.succeeded()); + + AsrSignal asr = new AsrSignal("no_name_driver", "en-US", Collections.singletonList(URI.create("hello.wav")), "#", 10, 10, 10, ASR_RESULT_TEXT, "dtmf_speech", 1, true); + endpoint.tell(asr, observer); + final IvrEndpointResponse ivrResponse = expectMsgClass(IvrEndpointResponse.class); + assertTrue(ivrResponse.succeeded()); + CollectedResult collectedResult = (CollectedResult)ivrResponse.get(); + assertTrue(ASR_RESULT_TEXT.equals(collectedResult.getResult())); + assertTrue(collectedResult.isAsr()); + + // EndSignal to IVR + endpoint.tell(new StopEndpoint(AsrSignal.REQUEST_ASR), observer); + + final IvrEndpointResponse ivrResponse2 = expectMsgClass(IvrEndpointResponse.class); + assertTrue(ivrResponse2.succeeded()); + + // Stop observing events from the IVR end point. + endpoint.tell(new StopObserving(observer), observer); + } + }; + } + + + private static final class MockAsrMediaGateway extends AuAbstractMockMediaGateway { + @SuppressWarnings("unused") + public MockAsrMediaGateway() { + super(); + } + + @Override + protected void event(final Object message, final ActorRef sender) { + final ActorRef self = self(); + final Class klass = message.getClass(); + if (NotificationRequest.class.equals(klass)) { + // Send a successful response for this request. + final NotificationRequest request = (NotificationRequest) message; + final JainMgcpResponseEvent response = new NotificationRequestResponse(this, + ReturnCode.Transaction_Executed_Normally); + sender.tell(response, self); + + // Send the notification. + Notify notify = createNotify(request, (int) transactionIdPool.get(), AUMgcpEvent.auoc.withParm("rc=101 asrr=" + ASR_RESULT_TEXT_HEX)); + sender.tell(notify, self); + + notify = createNotify(request, (int) transactionIdPool.get(), AUMgcpEvent.auoc.withParm("rc=101 asrr=" + ASR_RESULT_TEXT_HEX)); + sender.tell(notify, self); + + notify = createNotify(request, (int) transactionIdPool.get(), AUMgcpEvent.auoc.withParm("rc=100 asrr=" + ASR_RESULT_TEXT_HEX)); + sender.tell(notify, self); + } + } + } + + private static final class FailingMockAsrMediaGateway extends AuAbstractMockMediaGateway { + @SuppressWarnings("unused") + public FailingMockAsrMediaGateway() { + super(); + } + + @Override + protected void event(final Object message, final ActorRef sender) { + final ActorRef self = self(); + final Class klass = message.getClass(); + if (NotificationRequest.class.equals(klass)) { + // Send a successful response for this request. + final NotificationRequest request = (NotificationRequest) message; + final JainMgcpResponseEvent response = new NotificationRequestResponse(this, + ReturnCode.Transaction_Executed_Normally); + response.setTransactionHandle(request.getTransactionHandle()); + sender.tell(response, self); + + // Send the notification. + final Notify notify = createNotify(request, (int) transactionIdPool.get(), AUMgcpEvent.auof.withParm("rc=300")); + sender.tell(notify, self); + } + } + } + + private static final class MockAsrWithEndSignal extends AuAbstractMockMediaGateway { + + @SuppressWarnings("unused") + public MockAsrWithEndSignal() { + super(); + } + + @Override + protected void event(final Object message, final ActorRef sender) { + final ActorRef self = self(); + final Class klass = message.getClass(); + if (NotificationRequest.class.equals(klass)) { + // Send a successful response for this request. + final NotificationRequest request = (NotificationRequest) message; + if ("AU/es(sg=asr)".equals(request.getSignalRequests()[0].toString())) { + //handle stop request + final JainMgcpResponseEvent response = new NotificationRequestResponse(this, ReturnCode.Transaction_Executed_Normally); + sender.tell(response, self); + + Notify notify = createNotify(request, (int) transactionIdPool.get(), AUMgcpEvent.auoc.withParm("rc=100")); + sender.tell(notify, self); + return; + } + final JainMgcpResponseEvent response = new NotificationRequestResponse(this, ReturnCode.Transaction_Executed_Normally); + sender.tell(response, self); + // Send the notification. + Notify notify = createNotify(request, (int) transactionIdPool.get(), AUMgcpEvent.auoc.withParm("rc=101 asrr=" + ASR_RESULT_TEXT_HEX)); + sender.tell(notify, self); + } + } + } + + private static abstract class AuAbstractMockMediaGateway extends AbstractMockMediaGateway { + + protected Notify createNotify(final NotificationRequest request, int transactionId, final MgcpEvent event) { + final EventName[] events = {new EventName(AUPackage.AU, event)}; + Notify notify = new Notify(this, request.getEndpointIdentifier(), request.getRequestIdentifier(), events); + notify.setTransactionHandle(transactionId); + return notify; + } + + } +} diff --git a/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/IvrEndpointTest.java b/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/IvrEndpointTest.java index c9c052979c..0bc5705a41 100644 --- a/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/IvrEndpointTest.java +++ b/restcomm/restcomm.mgcp/src/test/java/org/restcomm/connect/mgcp/IvrEndpointTest.java @@ -21,14 +21,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import jain.protocol.ip.mgcp.JainMgcpEvent; -import jain.protocol.ip.mgcp.JainMgcpResponseEvent; -import jain.protocol.ip.mgcp.message.NotificationRequest; -import jain.protocol.ip.mgcp.message.NotificationRequestResponse; -import jain.protocol.ip.mgcp.message.Notify; -import jain.protocol.ip.mgcp.message.parms.EventName; -import jain.protocol.ip.mgcp.message.parms.ReturnCode; -import jain.protocol.ip.mgcp.pkg.MgcpEvent; import java.net.URI; import java.util.ArrayList; @@ -47,6 +39,13 @@ import akka.actor.ActorSystem; import akka.actor.Props; import akka.testkit.JavaTestKit; +import jain.protocol.ip.mgcp.JainMgcpResponseEvent; +import jain.protocol.ip.mgcp.message.NotificationRequest; +import jain.protocol.ip.mgcp.message.NotificationRequestResponse; +import jain.protocol.ip.mgcp.message.Notify; +import jain.protocol.ip.mgcp.message.parms.EventName; +import jain.protocol.ip.mgcp.message.parms.ReturnCode; +import jain.protocol.ip.mgcp.pkg.MgcpEvent; /** * @author thomas.quintana@telestax.com (Thomas Quintana) @@ -96,7 +95,7 @@ public void testSuccessfulScenario() { announcements.add(URI.create("hello.wav")); final Play play = new Play(announcements, 1); endpoint.tell(play, observer); - final IvrEndpointResponse ivrResponse = expectMsgClass(IvrEndpointResponse.class); + final IvrEndpointResponse ivrResponse = expectMsgClass(IvrEndpointResponse.class); assertTrue(ivrResponse.succeeded()); // Stop observing events from the IVR end point. endpoint.tell(new StopObserving(observer), observer); @@ -132,9 +131,9 @@ public void testSuccessfulScenarioWithDigits() { builder.addPrompt(URI.create("hello.wav")); final PlayCollect playCollect = builder.build(); endpoint.tell(playCollect, observer); - final IvrEndpointResponse ivrResponse = expectMsgClass(IvrEndpointResponse.class); + final IvrEndpointResponse ivrResponse = expectMsgClass(IvrEndpointResponse.class); assertTrue(ivrResponse.succeeded()); - assertTrue("1".equals(ivrResponse.get())); + assertTrue("1".equals(((CollectedResult)ivrResponse.get()).getResult())); // Stop observing events from the IVR end point. endpoint.tell(new StopObserving(observer), observer); } @@ -169,7 +168,7 @@ public void testFailureScenario() { announcements.add(URI.create("hello.wav")); final Play play = new Play(announcements, 1); endpoint.tell(play, observer); - final IvrEndpointResponse ivrResponse = expectMsgClass(IvrEndpointResponse.class); + final IvrEndpointResponse ivrResponse = expectMsgClass(IvrEndpointResponse.class); assertFalse(ivrResponse.succeeded()); // Stop observing events from the IVR end point. endpoint.tell(new StopObserving(observer), observer); @@ -186,9 +185,6 @@ public MockMediaGateway() { @Override protected void event(final Object message, final ActorRef sender) { final ActorRef self = self(); - if (message instanceof JainMgcpEvent) { - System.out.println(message.toString()); - } final Class klass = message.getClass(); if (NotificationRequest.class.equals(klass)) { // Send a successful response for this request. @@ -196,14 +192,12 @@ protected void event(final Object message, final ActorRef sender) { final JainMgcpResponseEvent response = new NotificationRequestResponse(this, ReturnCode.Transaction_Executed_Normally); sender.tell(response, self); - System.out.println(response.toString()); // Send the notification. final MgcpEvent event = AUMgcpEvent.auoc.withParm("rc=100 dc=1"); final EventName[] events = { new EventName(AUPackage.AU, event) }; final Notify notify = new Notify(this, request.getEndpointIdentifier(), request.getRequestIdentifier(), events); notify.setTransactionHandle((int) transactionIdPool.get()); sender.tell(notify, self); - System.out.println(notify.toString()); } } } @@ -217,9 +211,6 @@ public FailingMockMediaGateway() { @Override protected void event(final Object message, final ActorRef sender) { final ActorRef self = self(); - if (message instanceof JainMgcpEvent) { - System.out.println(message.toString()); - } final Class klass = message.getClass(); if (NotificationRequest.class.equals(klass)) { // Send a successful response for this request. @@ -228,14 +219,12 @@ protected void event(final Object message, final ActorRef sender) { ReturnCode.Transaction_Executed_Normally); response.setTransactionHandle(request.getTransactionHandle()); sender.tell(response, self); - System.out.println(response.toString()); // Send the notification. final MgcpEvent event = AUMgcpEvent.auoc.withParm("rc=300"); final EventName[] events = { new EventName(AUPackage.AU, event) }; final Notify notify = new Notify(this, request.getEndpointIdentifier(), request.getRequestIdentifier(), events); notify.setTransactionHandle((int) transactionIdPool.get()); sender.tell(notify, self); - System.out.println(notify.toString()); } } } diff --git a/restcomm/restcomm.mscontrol.api/src/main/java/org/restcomm/connect/mscontrol/api/messages/Collect.java b/restcomm/restcomm.mscontrol.api/src/main/java/org/restcomm/connect/mscontrol/api/messages/Collect.java index 4a20b27a18..0111eb7cc6 100644 --- a/restcomm/restcomm.mscontrol.api/src/main/java/org/restcomm/connect/mscontrol/api/messages/Collect.java +++ b/restcomm/restcomm.mscontrol.api/src/main/java/org/restcomm/connect/mscontrol/api/messages/Collect.java @@ -19,30 +19,65 @@ */ package org.restcomm.connect.mscontrol.api.messages; +import org.restcomm.connect.commons.annotations.concurrency.Immutable; + import java.net.URI; import java.util.List; -import org.restcomm.connect.commons.annotations.concurrency.Immutable; - /** * @author quintana.thomas@gmail.com (Thomas Quintana) */ @Immutable public final class Collect { + + public enum Type { + DTMF, SPEECH, DTMF_SPEECH; + + public static Type parseOrDefault(String name, Type defaultValue){ + try { + return "DTMF SPEECH".equalsIgnoreCase(name) ? DTMF_SPEECH : Type.valueOf(name.toUpperCase()); + } catch (Exception e) { + return defaultValue; + } + } + } + + private final Type type; private final List prompts; private final String pattern; private final int timeout; private final String endInputKey; private final int numberOfDigits; + private final String lang; + private final String hints; + private final String driver; + private final boolean partialResult; - public Collect(final List prompts, final String pattern, final int timeout, final String endInputKey, - final int numberOfDigits) { + public Collect(String driver,final Type type, final List prompts, final String pattern, final int timeout, final String endInputKey, + final int numberOfDigits, final String lang, final String hints, final boolean partialResult) { super(); + this.driver = driver; + this.type = type; this.prompts = prompts; this.pattern = pattern; this.timeout = timeout; this.endInputKey = endInputKey; this.numberOfDigits = numberOfDigits; + this.lang = lang; + this.hints = hints; + this.partialResult = partialResult; + } + + public String getDriver() { + return driver; + } + + public Type type() { + return type; + } + + public String lang() { + return lang; } public List prompts() { @@ -77,4 +112,11 @@ public int numberOfDigits() { return numberOfDigits; } + public String getHints() { + return hints; + } + + public boolean needPartialResult() { + return partialResult; + } } diff --git a/restcomm/restcomm.mscontrol.api/src/main/java/org/restcomm/connect/mscontrol/api/messages/CollectedResult.java b/restcomm/restcomm.mscontrol.api/src/main/java/org/restcomm/connect/mscontrol/api/messages/CollectedResult.java new file mode 100644 index 0000000000..8b2df9a1aa --- /dev/null +++ b/restcomm/restcomm.mscontrol.api/src/main/java/org/restcomm/connect/mscontrol/api/messages/CollectedResult.java @@ -0,0 +1,58 @@ +/* + * 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.mscontrol.api.messages; + +/** + * Created by gdubina on 6/6/17. + */ +public class CollectedResult { + + private final String result; + private final boolean isAsr; + private final boolean isPartial; + + public CollectedResult(String result, boolean isAsr, boolean isPartial) { + this.result = result; + this.isAsr = isAsr; + this.isPartial = isPartial; + } + + public String getResult() { + return result; + } + + public boolean isAsr() { + return isAsr; + } + + public boolean isPartial() { + return isPartial; + } + + @Override + public String toString() { + return "CollectedResult{" + + "result='" + result + '\'' + + ", isAsr=" + isAsr + + ", isPartial=" + isPartial + + '}'; + } +} diff --git a/restcomm/restcomm.mscontrol.mms/src/main/java/org/restcomm/connect/mscontrol/mms/MgcpMediaGroup.java b/restcomm/restcomm.mscontrol.mms/src/main/java/org/restcomm/connect/mscontrol/mms/MgcpMediaGroup.java index 4832b37605..fd12cfe3fd 100644 --- a/restcomm/restcomm.mscontrol.mms/src/main/java/org/restcomm/connect/mscontrol/mms/MgcpMediaGroup.java +++ b/restcomm/restcomm.mscontrol.mms/src/main/java/org/restcomm/connect/mscontrol/mms/MgcpMediaGroup.java @@ -26,7 +26,9 @@ import jain.protocol.ip.mgcp.message.parms.ConnectionIdentifier; import jain.protocol.ip.mgcp.message.parms.ConnectionMode; import jain.protocol.ip.mgcp.pkg.MgcpEvent; +import org.apache.commons.configuration.Configuration; import org.mobicents.protocols.mgcp.jain.pkg.AUMgcpEvent; +import org.restcomm.connect.commons.configuration.RestcommConfiguration; import org.restcomm.connect.commons.fsm.Action; import org.restcomm.connect.commons.fsm.FiniteStateMachine; import org.restcomm.connect.commons.fsm.State; @@ -34,6 +36,7 @@ import org.restcomm.connect.commons.patterns.Observe; import org.restcomm.connect.commons.patterns.Observing; import org.restcomm.connect.commons.patterns.StopObserving; +import org.restcomm.connect.mgcp.AsrSignal; import org.restcomm.connect.mgcp.CreateIvrEndpoint; import org.restcomm.connect.mgcp.CreateLink; import org.restcomm.connect.mgcp.DestroyEndpoint; @@ -52,6 +55,7 @@ import org.restcomm.connect.mgcp.UpdateLink; import org.restcomm.connect.mscontrol.api.MediaGroup; import org.restcomm.connect.mscontrol.api.messages.Collect; +import org.restcomm.connect.mscontrol.api.messages.CollectedResult; import org.restcomm.connect.mscontrol.api.messages.Join; import org.restcomm.connect.mscontrol.api.messages.MediaGroupResponse; import org.restcomm.connect.mscontrol.api.messages.MediaGroupStateChanged; @@ -71,7 +75,6 @@ /** * @author quintana.thomas@gmail.com (Thomas Quintana) * @author maria.farooq@telestax.com (Maria Farooq) - * */ public class MgcpMediaGroup extends MediaGroup { private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this); @@ -95,6 +98,8 @@ public class MgcpMediaGroup extends MediaGroup { // FSM. protected FiniteStateMachine fsm; + // The user specific configuration. + Configuration configuration = null; // MGCP runtime stuff. protected final ActorRef gateway; @@ -196,20 +201,29 @@ public MgcpMediaGroup(final ActorRef gateway, final MediaSession session, final protected void collect(final Object message) { final ActorRef self = self(); final Collect request = (Collect) message; - final PlayCollect.Builder builder = PlayCollect.builder(); - for (final URI prompt : request.prompts()) { - builder.addPrompt(prompt); - } - builder.setClearDigitBuffer(true); - builder.setDigitPattern(request.pattern()); - builder.setFirstDigitTimer(request.timeout()); - builder.setInterDigitTimer(request.timeout()); - builder.setEndInputKey(request.endInputKey()); - builder.setMaxNumberOfDigits(request.numberOfDigits()); - this.lastEvent = AUMgcpEvent.aupc; stop(lastEvent); + + Object signal; + if (request.type() == Collect.Type.DTMF) { + final PlayCollect.Builder builder = PlayCollect.builder(); + for (final URI prompt : request.prompts()) { + builder.addPrompt(prompt); + } + builder.setClearDigitBuffer(true); + builder.setDigitPattern(request.pattern()); + builder.setFirstDigitTimer(request.timeout()); + builder.setInterDigitTimer(request.timeout()); + builder.setEndInputKey(request.endInputKey()); + builder.setMaxNumberOfDigits(request.numberOfDigits()); + signal = builder.build(); + this.lastEvent = AUMgcpEvent.aupc; + } else { + this.lastEvent = AsrSignal.REQUEST_ASR; + signal = new AsrSignal(request.getDriver(), request.lang(), request.prompts(), request.endInputKey(), RestcommConfiguration.getInstance().getMgAsr().getAsrMRT(), request.timeout(), + request.timeout(), request.getHints(), request.type().toString() ,request.numberOfDigits(), request.needPartialResult()); + } this.originator = sender(); - ivr.tell(builder.build(), self); + ivr.tell(signal, self); ivrInUse = true; } @@ -219,8 +233,8 @@ protected void play(final Object message) { final List uris = request.uris(); final int iterations = request.iterations(); final org.restcomm.connect.mgcp.Play play = new org.restcomm.connect.mgcp.Play(uris, iterations); - this.lastEvent = AUMgcpEvent.aupa; stop(lastEvent); + this.lastEvent = AUMgcpEvent.aupa; this.originator = sender(); ivr.tell(play, self); ivrInUse = true; @@ -228,20 +242,22 @@ protected void play(final Object message) { @SuppressWarnings("unchecked") protected void notification(final Object message) { - final IvrEndpointResponse response = (IvrEndpointResponse) message; + final IvrEndpointResponse response = (IvrEndpointResponse) message; + Object ivrResponse = response.get(); final ActorRef self = self(); - MediaGroupResponse event = null; + MediaGroupResponse event; + org.restcomm.connect.mgcp.CollectedResult mgcpCollectedResult = null; if (response.succeeded()) { - event = new MediaGroupResponse(response.get()); + mgcpCollectedResult = (org.restcomm.connect.mgcp.CollectedResult)ivrResponse; + event = new MediaGroupResponse<>(new CollectedResult(mgcpCollectedResult.getResult(), mgcpCollectedResult.isAsr(), mgcpCollectedResult.isPartial())); } else { - event = new MediaGroupResponse(response.cause(), response.error()); + event = new MediaGroupResponse<>(response.cause(), response.error()); } - // for (final ActorRef observer : observers) { - // observer.tell(event, self); - // } if (originator != null) this.originator.tell(event, self); - ivrInUse = false; + if (ivrResponse == null || (mgcpCollectedResult != null && !(mgcpCollectedResult.isPartial()))) { + ivrInUse = false; + } } protected void observe(final Object message) { @@ -383,8 +399,8 @@ protected void record(final Object message) { builder.setEndInputKey("null"); } builder.setRecordingId(request.destination()); - this.lastEvent = AUMgcpEvent.aupr; stop(lastEvent); + this.lastEvent = AUMgcpEvent.aupr; this.originator = sender(); ivr.tell(builder.build(), self); ivrInUse = true; diff --git a/restcomm/restcomm.ui/src/main/webapp/lib/jquery/README.md b/restcomm/restcomm.ui/src/main/webapp/lib/jquery/README.md index 328064b7b6..30b8381af3 100755 --- a/restcomm/restcomm.ui/src/main/webapp/lib/jquery/README.md +++ b/restcomm/restcomm.ui/src/main/webapp/lib/jquery/README.md @@ -1,3 +1,10 @@ +RestComm +======== +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FRestComm%2FRestcomm-Connect.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FRestComm%2FRestcomm-Connect?ref=badge_shield) + +RestComm is licensed under dual license policy. The default license is the Free Open Source GNU Affero GPL v3.0. Alternatively a commercial license can be obtained from Telestax ([contact form](http://www.telestax.com/contactus/#InquiryForm)) + +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FRestComm%2FRestcomm-Connect.svg?type=large)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FRestComm%2FRestcomm-Connect?ref=badge_large) jQuery Component ================