All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.restcomm.connect.interpreter.VoiceInterpreter Maven / Gradle / Ivy

There is a newer version: 8.4.0-227
Show newest version
/*
 * 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 akka.actor.Actor;
import akka.actor.ActorRef;
import akka.actor.Props;
import akka.actor.ReceiveTimeout;
import akka.actor.UntypedActorContext;
import akka.actor.UntypedActorFactory;
import akka.event.Logging;
import akka.event.LoggingAdapter;
import akka.pattern.AskTimeoutException;
import akka.util.Timeout;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.message.BasicNameValuePair;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.restcomm.connect.asr.AsrResponse;
import org.restcomm.connect.commons.cache.DiskCacheResponse;
import org.restcomm.connect.commons.dao.Sid;
import org.restcomm.connect.commons.fsm.Action;
import org.restcomm.connect.commons.fsm.FiniteStateMachine;
import org.restcomm.connect.commons.fsm.State;
import org.restcomm.connect.commons.fsm.Transition;
import org.restcomm.connect.commons.fsm.TransitionFailedException;
import org.restcomm.connect.commons.fsm.TransitionNotFoundException;
import org.restcomm.connect.commons.fsm.TransitionRollbackException;
import org.restcomm.connect.commons.patterns.Observe;
import org.restcomm.connect.commons.patterns.StopObserving;
import org.restcomm.connect.commons.telephony.CreateCallType;
import org.restcomm.connect.commons.util.UriUtils;
import org.restcomm.connect.dao.CallDetailRecordsDao;
import org.restcomm.connect.dao.NotificationsDao;
import org.restcomm.connect.dao.entities.CallDetailRecord;
import org.restcomm.connect.dao.entities.MediaAttributes;
import org.restcomm.connect.dao.entities.Notification;
import org.restcomm.connect.email.api.EmailResponse;
import org.restcomm.connect.extension.api.ExtensionResponse;
import org.restcomm.connect.extension.api.IExtensionFeatureAccessRequest;
import org.restcomm.connect.extension.controller.ExtensionController;
import org.restcomm.connect.fax.FaxResponse;
import org.restcomm.connect.http.client.DownloaderResponse;
import org.restcomm.connect.http.client.HttpRequestDescriptor;
import org.restcomm.connect.interpreter.rcml.Attribute;
import org.restcomm.connect.interpreter.rcml.End;
import org.restcomm.connect.interpreter.rcml.GetNextVerb;
import org.restcomm.connect.interpreter.rcml.Nouns;
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;
import org.restcomm.connect.mscontrol.api.messages.Mute;
import org.restcomm.connect.mscontrol.api.messages.Play;
import org.restcomm.connect.mscontrol.api.messages.StartRecording;
import org.restcomm.connect.mscontrol.api.messages.StopMediaGroup;
import org.restcomm.connect.mscontrol.api.messages.Unmute;
import org.restcomm.connect.sms.api.SmsServiceResponse;
import org.restcomm.connect.sms.api.SmsSessionResponse;
import org.restcomm.connect.telephony.api.AddParticipant;
import org.restcomm.connect.telephony.api.Answer;
import org.restcomm.connect.telephony.api.BridgeManagerResponse;
import org.restcomm.connect.telephony.api.BridgeStateChanged;
import org.restcomm.connect.telephony.api.CallFail;
import org.restcomm.connect.telephony.api.CallHoldStateChange;
import org.restcomm.connect.telephony.api.CallInfo;
import org.restcomm.connect.telephony.api.CallManagerResponse;
import org.restcomm.connect.telephony.api.CallResponse;
import org.restcomm.connect.telephony.api.CallStateChanged;
import org.restcomm.connect.telephony.api.Cancel;
import org.restcomm.connect.telephony.api.ConferenceCenterResponse;
import org.restcomm.connect.telephony.api.ConferenceInfo;
import org.restcomm.connect.telephony.api.ConferenceModeratorPresent;
import org.restcomm.connect.telephony.api.ConferenceResponse;
import org.restcomm.connect.telephony.api.ConferenceStateChanged;
import org.restcomm.connect.telephony.api.CreateBridge;
import org.restcomm.connect.telephony.api.CreateCall;
import org.restcomm.connect.telephony.api.CreateConference;
import org.restcomm.connect.telephony.api.DestroyCall;
import org.restcomm.connect.telephony.api.DestroyConference;
import org.restcomm.connect.telephony.api.Dial;
import org.restcomm.connect.telephony.api.FeatureAccessRequest;
import org.restcomm.connect.telephony.api.GetCallInfo;
import org.restcomm.connect.telephony.api.GetConferenceInfo;
import org.restcomm.connect.telephony.api.GetRelatedCall;
import org.restcomm.connect.telephony.api.Hangup;
import org.restcomm.connect.telephony.api.JoinCalls;
import org.restcomm.connect.telephony.api.Reject;
import org.restcomm.connect.telephony.api.RemoveParticipant;
import org.restcomm.connect.telephony.api.StartBridge;
import org.restcomm.connect.telephony.api.StopBridge;
import org.restcomm.connect.telephony.api.StopConference;
import org.restcomm.connect.telephony.api.StopWaiting;
import org.restcomm.connect.tts.api.SpeechSynthesizerResponse;
import scala.concurrent.Await;
import scala.concurrent.Future;
import scala.concurrent.duration.Duration;

import javax.servlet.sip.SipServletMessage;
import javax.servlet.sip.SipServletRequest;
import javax.servlet.sip.SipServletResponse;
import javax.servlet.sip.SipSession;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Currency;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static akka.pattern.Patterns.ask;

/**
 * @author [email protected] (Thomas Quintana)
 * @author [email protected]
 * @author [email protected]
 * @author [email protected] (Amit Bhayani)
 * @author [email protected]
 * @author [email protected]
 */
public class VoiceInterpreter extends BaseVoiceInterpreter {
    // Logger.
    private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);

    // States for the FSM.
    private final State startDialing;
    private final State processingDialChildren;
    private final State acquiringOutboundCallInfo;
    private final State forking;
    // private final State joiningCalls;
    private final State creatingBridge;
    private final State initializingBridge;
    private final State bridging;
    private final State bridged;
    private final State finishDialing;
    private final State acquiringConferenceInfo;
    private final State joiningConference;
    private final State conferencing;
    private final State finishConferencing;
    private final State downloadingRcml;
    private final State downloadingFallbackRcml;
    private final State initializingCall;
    // private final State initializedCall;
    private final State ready;
    private final State notFound;
    private final State rejecting;
    private final State finished;

    // FSM.
    // The conference Ceneter.
    private final ActorRef conferenceCenter;

    // State for outbound calls.
    private boolean isForking;
    private List dialBranches;
    private List dialChildren;
    private Map dialChildrenWithAttributes;

    // The conferencing stuff
    private int maxParticipantLimit = 40;
    private ActorRef conference;
    private Sid conferenceSid;
    private ConferenceInfo conferenceInfo;
    private ConferenceStateChanged.State conferenceState;
    private boolean muteCall;
    private boolean startConferenceOnEnter = true;
    private boolean endConferenceOnExit = false;
    private boolean confModeratorPresent = false;
    private ActorRef confSubVoiceInterpreter;
    private Attribute dialRecordAttribute;
    private boolean dialActionExecuted = false;
    private ActorRef sender;
    private boolean liveCallModification = false;
    private boolean recordingCall = true;
    protected boolean isParserFailed = false;
    protected boolean playWaitUrlPending = false;
    Tag conferenceVerb;
    List conferenceWaitUris;
    private boolean playMusicForConference = false;
    private MediaAttributes mediaAttributes;
    //Used for system apps, such as when WebRTC client is dialing out.
    //The rcml will be used instead of download the RCML
    private String rcml;

    // Call bridging
    private final ActorRef bridgeManager;
    private ActorRef bridge;
    private boolean beep;

    private boolean enable200OkDelay;

    // IMS authentication
    private boolean asImsUa;
    private String imsUaLogin;
    private String imsUaPassword;
    private String forwardedFrom;
    private Attribute action;

    private String conferenceNameWithAccountAndFriendlyName;
    private Sid callSid;

    // Controls if VI will wait MS response to move to the next verb
    protected boolean msResponsePending;

    private boolean callWaitingForAnswer = false;

    private Tag callWaitingForAnswerPendingTag;

    private long timeout;

    public VoiceInterpreter(VoiceInterpreterParams params) {
        super();
        final ActorRef source = self();
        downloadingRcml = new State("downloading rcml", new DownloadingRcml(source), null);
        downloadingFallbackRcml = new State("downloading fallback rcml", new DownloadingFallbackRcml(source), null);
        initializingCall = new State("initializing call", new InitializingCall(source), null);
        // initializedCall = new State("initialized call", new InitializedCall(source), new PostInitializedCall(source));
        ready = new State("ready", new Ready(source), null);
        notFound = new State("notFound", new NotFound(source), null);
        rejecting = new State("rejecting", new Rejecting(source), null);
        startDialing = new State("start dialing", new StartDialing(source), null);
        processingDialChildren = new State("processing dial children", new ProcessingDialChildren(source), null);
        acquiringOutboundCallInfo = new State("acquiring outbound call info", new AcquiringOutboundCallInfo(source), null);
        forking = new State("forking", new Forking(source), null);
        // joiningCalls = new State("joining calls", new JoiningCalls(source), null);
        this.creatingBridge = new State("creating bridge", new CreatingBridge(source), null);
        this.initializingBridge = new State("initializing bridge", new InitializingBridge(source), null);
        this.bridging = new State("bridging", new Bridging(source), null);
        bridged = new State("bridged", new Bridged(source), null);
        finishDialing = new State("finish dialing", new FinishDialing(source), null);
        acquiringConferenceInfo = new State("acquiring conference info", new AcquiringConferenceInfo(source), null);
        joiningConference = new State("joining conference", new JoiningConference(source), null);
        conferencing = new State("conferencing", new Conferencing(source), null);
        finishConferencing = new State("finish conferencing", new FinishConferencing(source), null);
        finished = new State("finished", new Finished(source), null);
        /*
         * dialing = new State("dialing", null, null); bridging = new State("bridging", null, null); conferencing = new
         * State("conferencing", null, null);
         */
        transitions.add(new Transition(acquiringAsrInfo, finished));
        transitions.add(new Transition(acquiringSynthesizerInfo, finished));
        transitions.add(new Transition(acquiringCallInfo, initializingCall));
        transitions.add(new Transition(acquiringCallInfo, downloadingRcml));
        transitions.add(new Transition(acquiringCallInfo, finished));
        transitions.add(new Transition(acquiringCallInfo, ready));
        transitions.add(new Transition(initializingCall, downloadingRcml));
        transitions.add(new Transition(initializingCall, ready));
        transitions.add(new Transition(initializingCall, finishDialing));
        transitions.add(new Transition(initializingCall, hangingUp));
        transitions.add(new Transition(initializingCall, finished));
        transitions.add(new Transition(downloadingRcml, ready));
        transitions.add(new Transition(downloadingRcml, notFound));
        transitions.add(new Transition(downloadingRcml, downloadingFallbackRcml));
        transitions.add(new Transition(downloadingRcml, hangingUp));
        transitions.add(new Transition(downloadingRcml, finished));
        transitions.add(new Transition(downloadingFallbackRcml, ready));
        transitions.add(new Transition(downloadingFallbackRcml, hangingUp));
        transitions.add(new Transition(downloadingFallbackRcml, finished));
        transitions.add(new Transition(downloadingFallbackRcml, notFound));
        transitions.add(new Transition(ready, initializingCall));
        transitions.add(new Transition(ready, faxing));
        transitions.add(new Transition(ready, sendingEmail));
        transitions.add(new Transition(ready, pausing));
        transitions.add(new Transition(ready, checkingCache));
        transitions.add(new Transition(ready, caching));
        transitions.add(new Transition(ready, synthesizing));
        transitions.add(new Transition(ready, rejecting));
        transitions.add(new Transition(ready, redirecting));
        transitions.add(new Transition(ready, processingGatherChildren));
        transitions.add(new Transition(ready, creatingRecording));
        transitions.add(new Transition(ready, creatingSmsSession));
        transitions.add(new Transition(ready, startDialing));
        transitions.add(new Transition(ready, hangingUp));
        transitions.add(new Transition(ready, finished));
        transitions.add(new Transition(pausing, ready));
        transitions.add(new Transition(pausing, finished));
        transitions.add(new Transition(rejecting, finished));
        transitions.add(new Transition(faxing, ready));
        transitions.add(new Transition(faxing, finished));
        transitions.add(new Transition(sendingEmail, ready));
        transitions.add(new Transition(sendingEmail, finished));
        transitions.add(new Transition(sendingEmail, finishDialing));
        transitions.add(new Transition(checkingCache, caching));
        transitions.add(new Transition(checkingCache, conferencing));
        transitions.add(new Transition(caching, finished));
        transitions.add(new Transition(caching, conferencing));
        transitions.add(new Transition(caching, finishConferencing));
        transitions.add(new Transition(playing, ready));
        transitions.add(new Transition(playing, finishConferencing));
        transitions.add(new Transition(playing, finished));
        transitions.add(new Transition(synthesizing, finished));
        transitions.add(new Transition(redirecting, ready));
        transitions.add(new Transition(redirecting, finished));
        transitions.add(new Transition(creatingRecording, finished));
        transitions.add(new Transition(finishRecording, ready));
        transitions.add(new Transition(finishRecording, finished));
        transitions.add(new Transition(processingGatherChildren, finished));
        transitions.add(new Transition(gathering, finished));
        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));
        transitions.add(new Transition(sendingSms, finished));
        transitions.add(new Transition(startDialing, processingDialChildren));
        transitions.add(new Transition(startDialing, acquiringConferenceInfo));
        transitions.add(new Transition(startDialing, faxing));
        transitions.add(new Transition(startDialing, sendingEmail));
        transitions.add(new Transition(startDialing, pausing));
        transitions.add(new Transition(startDialing, checkingCache));
        transitions.add(new Transition(startDialing, caching));
        transitions.add(new Transition(startDialing, synthesizing));
        transitions.add(new Transition(startDialing, redirecting));
        transitions.add(new Transition(startDialing, processingGatherChildren));
        transitions.add(new Transition(startDialing, creatingRecording));
        transitions.add(new Transition(startDialing, creatingSmsSession));
        transitions.add(new Transition(startDialing, startDialing));
        transitions.add(new Transition(startDialing, hangingUp));
        transitions.add(new Transition(startDialing, finished));
        transitions.add(new Transition(processingDialChildren, processingDialChildren));
        transitions.add(new Transition(processingDialChildren, forking));
        transitions.add(new Transition(processingDialChildren, startDialing));
        transitions.add(new Transition(processingDialChildren, checkingCache));
        transitions.add(new Transition(processingDialChildren, sendingEmail));
        transitions.add(new Transition(processingDialChildren, faxing));
        transitions.add(new Transition(processingDialChildren, sendingSms));
        transitions.add(new Transition(processingDialChildren, playing));
        transitions.add(new Transition(processingDialChildren, pausing));
        transitions.add(new Transition(processingDialChildren, ready));
        transitions.add(new Transition(processingDialChildren, hangingUp));
        transitions.add(new Transition(processingDialChildren, finished));
        transitions.add(new Transition(forking, acquiringOutboundCallInfo));
        transitions.add(new Transition(forking, finishDialing));
        transitions.add(new Transition(forking, hangingUp));
        transitions.add(new Transition(forking, finished));
        transitions.add(new Transition(forking, ready));
        transitions.add(new Transition(forking, checkingCache));
        transitions.add(new Transition(forking, caching));
        transitions.add(new Transition(forking, faxing));
        transitions.add(new Transition(forking, sendingEmail));
        transitions.add(new Transition(forking, pausing));
        transitions.add(new Transition(forking, synthesizing));
        transitions.add(new Transition(forking, redirecting));
        transitions.add(new Transition(forking, processingGatherChildren));
        transitions.add(new Transition(forking, creatingRecording));
        transitions.add(new Transition(forking, creatingSmsSession));
        // transitions.add(new Transition(acquiringOutboundCallInfo, joiningCalls));
        transitions.add(new Transition(acquiringOutboundCallInfo, hangingUp));
        transitions.add(new Transition(acquiringOutboundCallInfo, finished));
        transitions.add(new Transition(acquiringOutboundCallInfo, creatingBridge));
        transitions.add(new Transition(creatingBridge, initializingBridge));
        transitions.add(new Transition(creatingBridge, finishDialing));
        transitions.add(new Transition(initializingBridge, bridging));
        transitions.add(new Transition(initializingBridge, hangingUp));
        transitions.add(new Transition(initializingBridge, finished));
        transitions.add(new Transition(bridging, bridged));
        transitions.add(new Transition(bridging, finishDialing));
        transitions.add(new Transition(bridged, finishDialing));
        transitions.add(new Transition(bridged, finished));
        transitions.add(new Transition(finishDialing, ready));
        transitions.add(new Transition(finishDialing, faxing));
        transitions.add(new Transition(finishDialing, sendingEmail));
        transitions.add(new Transition(finishDialing, pausing));
        transitions.add(new Transition(finishDialing, checkingCache));
        transitions.add(new Transition(finishDialing, caching));
        transitions.add(new Transition(finishDialing, synthesizing));
        transitions.add(new Transition(finishDialing, redirecting));
        transitions.add(new Transition(finishDialing, processingGatherChildren));
        transitions.add(new Transition(finishDialing, creatingRecording));
        transitions.add(new Transition(finishDialing, creatingSmsSession));
        transitions.add(new Transition(finishDialing, startDialing));
        transitions.add(new Transition(finishDialing, hangingUp));
        transitions.add(new Transition(finishDialing, finished));
        transitions.add(new Transition(finishDialing, initializingCall));
        transitions.add(new Transition(acquiringConferenceInfo, joiningConference));
        transitions.add(new Transition(acquiringConferenceInfo, hangingUp));
        transitions.add(new Transition(acquiringConferenceInfo, finished));
        transitions.add(new Transition(joiningConference, conferencing));
        transitions.add(new Transition(joiningConference, acquiringConferenceInfo));
        transitions.add(new Transition(joiningConference, hangingUp));
        transitions.add(new Transition(joiningConference, finished));
        transitions.add(new Transition(conferencing, finishConferencing));
        transitions.add(new Transition(conferencing, hangingUp));
        transitions.add(new Transition(conferencing, finished));
        transitions.add(new Transition(conferencing, checkingCache));
        transitions.add(new Transition(conferencing, caching));
        transitions.add(new Transition(conferencing, playing));
        transitions.add(new Transition(conferencing, startDialing));
        transitions.add(new Transition(conferencing, creatingSmsSession));
        transitions.add(new Transition(conferencing, sendingEmail));
        transitions.add(new Transition(finishConferencing, ready));
        transitions.add(new Transition(finishConferencing, faxing));
        transitions.add(new Transition(finishConferencing, sendingEmail));
        transitions.add(new Transition(finishConferencing, pausing));
        transitions.add(new Transition(finishConferencing, checkingCache));
        transitions.add(new Transition(finishConferencing, caching));
        transitions.add(new Transition(finishConferencing, synthesizing));
        transitions.add(new Transition(finishConferencing, redirecting));
        transitions.add(new Transition(finishConferencing, processingGatherChildren));
        transitions.add(new Transition(finishConferencing, creatingRecording));
        transitions.add(new Transition(finishConferencing, creatingSmsSession));
        transitions.add(new Transition(finishConferencing, startDialing));
        transitions.add(new Transition(finishConferencing, hangingUp));
        transitions.add(new Transition(finishConferencing, finished));
        transitions.add(new Transition(hangingUp, finished));
        transitions.add(new Transition(hangingUp, finishConferencing));
        transitions.add(new Transition(hangingUp, finishDialing));
        transitions.add(new Transition(hangingUp, ready));
        transitions.add(new Transition(uninitialized, finished));
        transitions.add(new Transition(notFound, finished));
        // Initialize the FSM.
        this.fsm = new FiniteStateMachine(uninitialized, transitions);
        // Initialize the runtime stuff.
        this.accountId = params.getAccount();
        this.phoneId = params.getPhone();
        this.version = params.getVersion();
        this.url = params.getUrl();
        this.method = params.getMethod();
        this.fallbackUrl = params.getFallbackUrl();
        this.fallbackMethod = params.getFallbackMethod();
        this.viStatusCallback = params.getStatusCallback();
        this.viStatusCallbackMethod = params.getStatusCallbackMethod();
        this.referTarget = params.getReferTarget();
        this.transferor = params.getTransferor();
        this.transferee = params.getTransferee();
        this.emailAddress = params.getEmailAddress();
        this.configuration = params.getConfiguration();
        this.callManager = params.getCallManager();
        this.conferenceCenter = params.getConferenceCenter();
        this.bridgeManager = params.getBridgeManager();
        this.smsService = params.getSmsService();
        this.smsSessions = new HashMap();
        this.storage = params.getStorage();
        final Configuration runtime = configuration.subset("runtime-settings");
        playMusicForConference = Boolean.parseBoolean(runtime.getString("play-music-for-conference","false"));
        this.enable200OkDelay = this.configuration.subset("runtime-settings").getBoolean("enable-200-ok-delay",false);
        this.downloader = downloader();
        this.monitoring = params.getMonitoring();
        this.sdr = params.getSdr();
        this.rcml = params.getRcml();
        this.asImsUa = params.isAsImsUa();
        this.imsUaLogin = params.getImsUaLogin();
        this.imsUaPassword = params.getImsUaPassword();
        this.timeout = params.getTimeout();
        this.msResponsePending = false;
        this.mediaAttributes = new MediaAttributes();
    }

    public static Props props(final VoiceInterpreterParams params) {
        return new Props(new UntypedActorFactory() {
            @Override
            public Actor create() throws Exception {
                return new VoiceInterpreter(params);
            }
        });
    }

    private Notification notification(final int log, final int error, final String message) {
        final Notification.Builder builder = Notification.builder();
        final Sid sid = Sid.generate(Sid.Type.NOTIFICATION);
        builder.setSid(sid);
        builder.setAccountSid(accountId);
        builder.setCallSid(callInfo.sid());
        builder.setApiVersion(version);
        builder.setLog(log);
        builder.setErrorCode(error);
        String base = configuration.subset("runtime-settings").getString("error-dictionary-uri");
        try {
            base = UriUtils.resolve(new URI(base)).toString();
        } catch (URISyntaxException e) {
            logger.error("URISyntaxException when trying to resolve Error-Dictionary URI: " + e);
        }
        StringBuilder buffer = new StringBuilder();
        buffer.append(base);
        if (!base.endsWith("/")) {
            buffer.append("/");
        }
        buffer.append(error).append(".html");
        final URI info = URI.create(buffer.toString());
        builder.setMoreInfo(info);
        builder.setMessageText(message);
        final DateTime now = DateTime.now();
        builder.setMessageDate(now);
        if (request != null) {
            builder.setRequestUrl(request.getUri());
            builder.setRequestMethod(request.getMethod());
            builder.setRequestVariables(request.getParametersAsString());
        }
        if (response != null) {
            builder.setResponseHeaders(response.getHeadersAsString());
            final String type = response.getContentType();
            if (type.contains("text/xml") || type.contains("application/xml") || type.contains("text/html")) {
                try {
                    builder.setResponseBody(response.getContentAsString());
                } catch (final IOException exception) {
                    logger.error(
                            "There was an error while reading the contents of the resource " + "located @ " + url.toString(),
                            exception);
                }
            }
        }
        buffer = new StringBuilder();
        buffer.append("/").append(version).append("/Accounts/");
        buffer.append(accountId.toString()).append("/Notifications/");
        buffer.append(sid.toString());
        final URI uri = URI.create(buffer.toString());
        builder.setUri(uri);
        return builder.build();
    }

    @SuppressWarnings("unchecked")
    @Override
    public void onReceive(final Object message) throws Exception {
        final Class klass = message.getClass();
        final State state = fsm.state();
        sender = sender();
        ActorRef self = self();

        if (logger.isInfoEnabled()) {
            logger.info(" ********** VoiceInterpreter's " + self().path() + " Current State: " + state.toString() + "\n"
            + ", Processing Message: " + klass.getName() + " Sender is: "+sender.path());
        }

        if (StartInterpreter.class.equals(klass)) {
            final StartInterpreter request = (StartInterpreter) message;
            call = request.resource();
            fsm.transition(message, acquiringAsrInfo);
        } else if (AsrResponse.class.equals(klass)) {
            onAsrResponse(message);
        } else if (SpeechSynthesizerResponse.class.equals(klass)) {
            onSpeechSynthesizerResponse(message);
        } else if (CallResponse.class.equals(klass)) {
            onCallResponse(message, state);
        } else if (CallStateChanged.class.equals(klass)) {
            onCallStateChanged(message, sender);
        } else if (CallManagerResponse.class.equals(klass)) {
            onCallManagerResponse(message);
        } else if (StartForking.class.equals(klass)) {
            fsm.transition(message, processingDialChildren);
        } else if (ConferenceCenterResponse.class.equals(klass)) {
            onConferenceCenterResponse(message);
        } else if (Fork.class.equals(klass)) {
            onForkMessage(message);
        } else if (ConferenceResponse.class.equals(klass)) {
            onConferenceResponse(message);
        } else if (ConferenceStateChanged.class.equals(klass)) {
            onConferenceStateChanged(message);
        } else if (DownloaderResponse.class.equals(klass)) {
            onDownloaderResponse(message, state);
        } else if (DiskCacheResponse.class.equals(klass)) {
            onDiskCacheResponse(message);
        } else if (ParserFailed.class.equals(klass)) {
            onParserFailed(message);
        } else if (Tag.class.equals(klass)) {
            onTagMessage(message);
        } else if (End.class.equals(klass)) {
            onEndMessage(message);
        } else if (StartGathering.class.equals(klass)) {
            fsm.transition(message, gathering);
        } else if (MediaGroupResponse.class.equals(klass)) {
            onMediaGroupResponse(message);
        } else if (SmsServiceResponse.class.equals(klass)) {
            onSmsServiceResponse(message);
        } else if (SmsSessionResponse.class.equals(klass)) {
            smsResponse(message);
        } else if (FaxResponse.class.equals(klass)) {
            fsm.transition(message, ready);
        } else if (EmailResponse.class.equals(klass)) {
            onEmailResponse(message);
        } else if (StopInterpreter.class.equals(klass)) {
            onStopInterpreter(message);
        } else if (message instanceof ReceiveTimeout) {
            onReceiveTimeout(message);
        } else if (BridgeManagerResponse.class.equals(klass)) {
            onBridgeManagerResponse((BridgeManagerResponse) message, self, sender);
        } else if (BridgeStateChanged.class.equals(klass)) {
            onBridgeStateChanged((BridgeStateChanged) message, self, sender);
        } else if (GetRelatedCall.class.equals(klass)) {
            onGetRelatedCall((GetRelatedCall) message, self, sender);
        } else if (JoinComplete.class.equals(klass)) {
            onJoinComplete((JoinComplete)message);
        } else if (CallHoldStateChange.class.equals(klass)) {
            onCallHoldStateChange((CallHoldStateChange)message, sender);
        }
    }

    private void onJoinComplete(JoinComplete message) throws TransitionNotFoundException, TransitionFailedException, TransitionRollbackException {
        if (logger.isInfoEnabled()) {
            logger.info("JoinComplete received, sender: " + sender().path() + ", VI state: " + fsm.state());
        }
        if (is(joiningConference)) {
            fsm.transition(message, conferencing);
        }
    }

    private void onAsrResponse(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        if (outstandingAsrRequests > 0) {
            asrResponse(message);
        } else {
            fsm.transition(message, acquiringSynthesizerInfo);
        }
    }

    private void onForkMessage(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        if (is(processingDialChildren)) {
            fsm.transition(message, forking);
        }
    }

    private void onConferenceCenterResponse(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        if (is(startDialing) || is(joiningConference)) {
            ConferenceCenterResponse ccReponse = (ConferenceCenterResponse)message;
            if(ccReponse.succeeded()){
                fsm.transition(message, acquiringConferenceInfo);
            }else{
                fsm.transition(message, hangingUp);
            }
        }
    }

    private void onConferenceResponse(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        final ConferenceResponse response = (ConferenceResponse) message;
        final Class klass = ((ConferenceResponse)message).get().getClass();
        if (logger.isDebugEnabled()) {
            logger.debug("New ConferenceResponse received with message: "+klass.getName());
        }
        if (Left.class.equals(klass)) {
            Left left = (Left) ((ConferenceResponse)message).get();
            ActorRef leftCall = left.get();
            if (leftCall.equals(call) && conference != null) {
                if(conferenceInfo.globalParticipants() !=0 ){
                    String path = configuration.subset("runtime-settings").getString("prompts-uri");
                    if (!path.endsWith("/")) {
                        path += "/";
                    }
                    String exitAudio = configuration.subset("runtime-settings").getString("conference-exit-audio");
                    path += exitAudio == null || exitAudio.equals("") ? "alert.wav" : exitAudio;
                    URI uri = null;
                    try {
                        uri = UriUtils.resolve(new URI(path));
                    } catch (final Exception exception) {
                        final Notification notification = notification(ERROR_NOTIFICATION, 12400, exception.getMessage());
                        final NotificationsDao notifications = storage.getNotificationsDao();
                        notifications.addNotification(notification);
                        sendMail(notification);
                        final StopInterpreter stop = new StopInterpreter();
                        self().tell(stop, self());
                        return;
                    }
                    if (logger.isInfoEnabled()) {
                        logger.info("going to play conference-exit-audio beep");
                    }
                    final Play play = new Play(uri, 1);
                    conference.tell(play, self());
                }

                if (endConferenceOnExit) {
                    // Stop the conference if endConferenceOnExit is true
                    final StopConference stop = new StopConference();
                    conference.tell(stop, self());
                }

                Attribute attribute = null;
                if (verb != null) {
                    attribute = verb.attribute(GatherAttributes.ATTRIBUTE_ACTION);
                }

                if (attribute == null) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Attribute is null, will ask for the next verb from parser");
                    }
                    final GetNextVerb next = new GetNextVerb();
                    parser.tell(next, self());
                } else {
                    if (logger.isInfoEnabled()) {
                        logger.info("Dial Action is set, executing Dial Action");
                    }
                    executeDialAction(message, sender);
                }
                conference.tell(new StopObserving(self()), null);
            }
        } else if (ConferenceInfo.class.equals(klass)) {
            conferenceInfo = response.get();
            if (logger.isInfoEnabled()) {
                logger.info("VoiceInterpreter received ConferenceResponse from Conference: " + conferenceInfo.name() + ", path: " + sender().path() + ", current confernce size: " + conferenceInfo.globalParticipants() + ", VI state: " + fsm.state());
            }
            if (is(acquiringConferenceInfo)) {
                fsm.transition(message, joiningConference);
            }
        }
    }

    private void onConferenceStateChanged(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        final ConferenceStateChanged event = (ConferenceStateChanged) message;
        if(logger.isInfoEnabled()) {
            logger.info("onConferenceStateChanged: "+event.state());
        }
        switch (event.state()) {
            case RUNNING_MODERATOR_PRESENT:
                conferenceState = event.state();
                conferenceStateModeratorPresent(message);
                break;
            case COMPLETED:
                conferenceState = event.state();
                //Move to finishConferencing only if we are not in Finished state already
                //There are cases were we have already finished conferencing, for example when there is
                //conference timeout
                if (!is(finished))
                    fsm.transition(message, finishConferencing);
                break;
            case STOPPING:
                conferenceState = event.state();
                if(is(joiningConference)){
                    if(logger.isInfoEnabled()) {
                        logger.info("We tried to join a stopping conference. Will ask Conference Center to create a new conference for us.");
                    }
                    final CreateConference create = new CreateConference(conferenceNameWithAccountAndFriendlyName, callSid);
                    conferenceCenter.tell(create, self());
                }
            default:
                break;
        }

        // !!IMPORTANT!!
        // Do not listen to COMPLETED nor FAILED conference state changes
        // When a conference stops it will ask all its calls to Leave
        // Then the call state will change and the voice interpreter will take proper action then
    }

    private void onParserFailed(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        if(logger.isInfoEnabled()) {
            logger.info("ParserFailed received. Will stop the call");
        }
        isParserFailed = true;
        fsm.transition(message, hangingUp);
    }

    private void onStopInterpreter(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        this.liveCallModification = ((StopInterpreter) message).isLiveCallModification();
        if (logger.isInfoEnabled()) {
            String msg = String.format("Got StopInterpreter, liveCallModification %s, CallState %s", liveCallModification, callState);
            logger.info(msg);
        }
        if (CallStateChanged.State.IN_PROGRESS.equals(callState) && !liveCallModification) {
            fsm.transition(message, hangingUp);
        } else {
            fsm.transition(message, finished);
        }
    }

    private void onReceiveTimeout(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        if (logger.isInfoEnabled()) {
            logger.info("Timeout received");
        }
        if (is(pausing)) {
            fsm.transition(message, ready);
        } else if (is(conferencing)) {
            fsm.transition(message, finishConferencing);
        } else if (is(forking)) {
            fsm.transition(message, finishDialing);
        } else if (is(bridged)) {
            fsm.transition(message, finishDialing);
        } else if (is(bridging)) {
            fsm.transition(message, finishDialing);
        } else if (is(playing) || is(initializingCall)) {
            fsm.transition(message, finished);
        }
    }

    private void onEmailResponse(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        final EmailResponse response = (EmailResponse) message;
        if (!response.succeeded()) {
            logger.error(
                    "There was an error while sending an email :" + response.error(),
                    response.cause());
            return;
        }
        fsm.transition(message, ready);
    }

    private void onSmsServiceResponse(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        final SmsServiceResponse response = (SmsServiceResponse) message;
        if (response.succeeded()) {
            if (is(creatingSmsSession)) {
                fsm.transition(message, sendingSms);
            }
        } else {
            fsm.transition(message, hangingUp);
        }
    }

    private void onMediaGroupResponse(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        final MediaGroupResponse response = (MediaGroupResponse) message;
        if(logger.isInfoEnabled()) {
            logger.info("MediaGroupResponse, succeeded: " + response.succeeded() + "  " + response.cause());
        }
        if (response.succeeded()) {
            if (is(playingRejectionPrompt)) {
                fsm.transition(message, hangingUp);
            } else if (is(playing)) {
                fsm.transition(message, ready);
            } else if (is(creatingRecording)) {
                if (logger.isInfoEnabled()) {
                    logger.info("Will move to finishRecording because of MediaGroupResponse");
                }
                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(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 {
                        collectedDigits.append(((CollectedResult)data).getResult());
                        fsm.transition(message, finishGathering);
                    }
                }
            } else if (is(bridging)) {
                // Finally proceed with call bridging
                if (logger.isInfoEnabled()) {
                    String msg = String.format("About to join calls, inbound call %s, outbound call %s", call, outboundCall);
                    logger.info(msg);
                }
                final JoinCalls bridgeCalls = new JoinCalls(call, outboundCall);
                bridge.tell(bridgeCalls, self());
            } else if (msResponsePending) {
                // Move to next verb once media server completed Play
                msResponsePending = false;
                final boolean noBranches = dialBranches == null || dialBranches.size() == 0;
                final boolean activeParser = parser != null;
                final boolean noDialAction = action == null;
                if (noBranches && activeParser && noDialAction) {
                    final GetNextVerb next = new GetNextVerb();
                    parser.tell(next, self());
                }
            }
        } else {
            fsm.transition(message, hangingUp);
        }
    }

    private void onEndMessage(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        //Because of RMS issue https://github.com/RestComm/mediaserver/issues/158 we cannot have List for waitUrl
        if (playWaitUrlPending && conferenceWaitUris != null && conferenceWaitUris.size() > 0) {
            if (logger.isInfoEnabled()) {
                String msg = String.format("End tag received, playWaitUrlPending is %s, conferenceWaitUris.size() %d",playWaitUrlPending, conferenceWaitUris.size());
                logger.info(msg);
            }
            fsm.transition(conferenceWaitUris, conferencing);
            return;
        }
        if (callState.equals(CallStateChanged.State.COMPLETED) || callState.equals(CallStateChanged.State.CANCELED)) {
            if(logger.isInfoEnabled()) {
                String msg = String.format("End tag received, Call state %s , VI state %s will move to finished state",callState, fsm.state());
                logger.info(msg);
            }
            fsm.transition(message, finished);
        } else {
            if (!isParserFailed) {
                if(logger.isInfoEnabled()) {
                    logger.info("End tag received will move to hangup the call, VI state: "+fsm.state());
                }
                fsm.transition(message, hangingUp);
            } else {
                if(logger.isInfoEnabled()) {
                    logger.info("End tag received but parser failed earlier so hangup would have been already sent to the call");
                }
            }
        }
    }

    private void onTagMessage(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        verb = (Tag) message;
        if (logger.isDebugEnabled()) {
            logger.debug("Tag received, name: "+verb.name()+", text: "+verb.text());
        }

        // if 200 ok delay is enabled we need to answer only the calls
        // which are having any further call enabler verbs in their RCML.
        if(enable200OkDelay && callWaitingForAnswer && Verbs.isThisVerbCallEnabler(verb)){
            if(logger.isInfoEnabled()) {
                logger.info("Tag received, but callWaitingForAnswer: will tell call to stop waiting");
            }
            callWaitingForAnswerPendingTag = verb;
            call.tell(new StopWaiting(), self());
        } else {
            if (playWaitUrlPending) {
                if (!(Verbs.play.equals(verb.name()) || Verbs.say.equals(verb.name()))) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Tag for waitUrl is neither Play or Say");
                    }
                    fsm.transition(message, hangingUp);
                }
                if (Verbs.say.equals(verb.name())) {
                    fsm.transition(message, checkingCache);
                } else if (Verbs.play.equals(verb.name())) {
                    fsm.transition(message, caching);
                }
                return;
            }
            if (CallStateChanged.State.RINGING == callState) {
                if (Verbs.reject.equals(verb.name())) {
                    fsm.transition(message, rejecting);
                } else if (Verbs.pause.equals(verb.name())) {
                    fsm.transition(message, pausing);
                } else {
                    fsm.transition(message, initializingCall);
                }
            } else if (Verbs.dial.equals(verb.name()) && callState != CallStateChanged.State.COMPLETED) {
                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
                    dialActionExecuted = false;
                }
                dialRecordAttribute = verb.attribute("record");
                fsm.transition(message, startDialing);
            } else if (Verbs.fax.equals(verb.name())) {
                fsm.transition(message, caching);
            } else if (Verbs.play.equals(verb.name()) && callState != CallStateChanged.State.COMPLETED) {
                fsm.transition(message, caching);
            } else if (Verbs.say.equals(verb.name()) && callState != CallStateChanged.State.COMPLETED) {
                // fsm.transition(message, synthesizing);
                fsm.transition(message, checkingCache);
            } else if (Verbs.gather.equals(verb.name()) && callState != CallStateChanged.State.COMPLETED) {
                gatherVerb = verb;
                fsm.transition(message, processingGatherChildren);
            } else if (Verbs.pause.equals(verb.name()) && callState != CallStateChanged.State.COMPLETED) {
                fsm.transition(message, pausing);
            } else if (Verbs.hangup.equals(verb.name()) && callState != CallStateChanged.State.COMPLETED) {
                if (logger.isInfoEnabled()) {
                    String msg = String.format("Next verb is Hangup, current state is %s , callInfo state %s", fsm.state(), callInfo.state());
                    logger.info(msg);
                }
                if (is(finishDialing)) {
                    fsm.transition(message, finished);
                } else {
                    fsm.transition(message, hangingUp);
                }
            } else if (Verbs.redirect.equals(verb.name()) && callState != CallStateChanged.State.COMPLETED) {
                fsm.transition(message, redirecting);
            } else if (Verbs.record.equals(verb.name()) && callState != CallStateChanged.State.COMPLETED) {
                fsm.transition(message, creatingRecording);
            } else if (Verbs.sms.equals(verb.name())) {

                //Check if Outbound SMS is allowed
                ExtensionController ec = ExtensionController.getInstance();
                final IExtensionFeatureAccessRequest far = new FeatureAccessRequest(FeatureAccessRequest.Feature.OUTBOUND_SMS, accountId);
                ExtensionResponse er = ec.executePreInboundAction(far, this.extensions);

                if (er.isAllowed()) {
                    fsm.transition(message, creatingSmsSession);
                    ec.executePostInboundAction(far, extensions);
                } else {
                    if (logger.isDebugEnabled()) {
                        final String errMsg = "Outbound SMS is not Allowed";
                        logger.debug(errMsg);
                    }
                    final NotificationsDao notifications = storage.getNotificationsDao();
                    final Notification notification = notification(WARNING_NOTIFICATION, 11001, "Outbound SMS is now allowed");
                    notifications.addNotification(notification);
                    fsm.transition(message, rejecting);
                    ec.executePostInboundAction(far, extensions);
                    return;
                }
            } else if (Verbs.email.equals(verb.name())) {
                fsm.transition(message, sendingEmail);
            } else {
                invalidVerb(verb);
            }
        }
    }

    private void onDiskCacheResponse(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        final DiskCacheResponse response = (DiskCacheResponse) message;
        if (response.succeeded()) {
            //Because of RMS issue https://github.com/RestComm/mediaserver/issues/158 we cannot have List for waitUrl
            if (playWaitUrlPending) {
                if (conferenceWaitUris == null)
                    conferenceWaitUris = new ArrayList();
                URI waitUrl = response.get();
                conferenceWaitUris.add(waitUrl);
                final GetNextVerb next = new GetNextVerb();
                parser.tell(next, self());
                return;
            }
            if (is(caching) || is(checkingCache)) {
                if (Verbs.play.equals(verb.name()) || Verbs.say.equals(verb.name())) {
                    fsm.transition(message, playing);
                } else if (Verbs.fax.equals(verb.name())) {
                    fsm.transition(message, faxing);
                } else if (Verbs.email.equals(verb.name())) {
                    fsm.transition(message, sendingEmail);
                }
            } else if (is(processingGatherChildren)) {
                fsm.transition(message, processingGatherChildren);
            }
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("DiskCacheResponse is " + response.toString());
            }
            if (is(checkingCache) || is(processingGatherChildren)) {
                fsm.transition(message, synthesizing);
            } else {
                if(response.cause() != null){
                    Notification notification = notification(WARNING_NOTIFICATION, 13233, response.cause().getMessage());
                    final NotificationsDao notifications = storage.getNotificationsDao();
                    notifications.addNotification(notification);
                    sendMail(notification);
                }
                fsm.transition(message, hangingUp);
            }
        }
    }

    private void onCallManagerResponse(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        final CallManagerResponse response = (CallManagerResponse) message;
        if (response.succeeded()) {
            if (is(startDialing)) {
                fsm.transition(message, processingDialChildren);
            } else if (is(processingDialChildren)) {
                fsm.transition(message, processingDialChildren);
            }
        } else {
            if (logger.isDebugEnabled()) {
                String msg = String.format("CallManager failed to create Call for %s, current state %s, dialChilder.size %s, dialBranches.size %s", response.getCreateCall().to(), fsm.state().toString(), dialChildren.size(), dialBranches.size());
                logger.debug(msg);
            }
            if (dialChildren != null && dialChildren.size() > 0) {
                fsm.transition(message, processingDialChildren);
            } else {
                fsm.transition(message, hangingUp);
            }
        }
    }

    private void onSpeechSynthesizerResponse(Object message) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        if (is(acquiringSynthesizerInfo)) {
            fsm.transition(message, acquiringCallInfo);
        } else if (is(processingGatherChildren) || processingGather) {
            final SpeechSynthesizerResponse response = (SpeechSynthesizerResponse) message;
            if (response.succeeded()) {
                fsm.transition(message, processingGatherChildren);
            } else {
                fsm.transition(message, hangingUp);
            }
        } else if (is(synthesizing)) {
            final SpeechSynthesizerResponse response = (SpeechSynthesizerResponse) message;
            if (response.succeeded()) {
                fsm.transition(message, caching);
            } else {
                fsm.transition(message, hangingUp);
            }
        }
    }

    private void onCallResponse(Object message, State state) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        if (forking.equals(state)) {
            // Allow updating of the callInfo at the VoiceInterpreter so that we can do Dial SIP Screening
            // (https://bitbucket.org/telestax/telscale-restcomm/issue/132/implement-twilio-sip-out) accurately from latest
            // response received
            final CallResponse response = (CallResponse) message;
            // Check from whom is the message (initial call or outbound call) and update info accordingly
            if (sender == call) {
                callInfo = response.get();
            } else {
                outboundCall = sender;
                outboundCallInfo = response.get();
            }
        } else if (acquiringCallInfo.equals(state)) {
            final CallResponse response = (CallResponse) message;
            // Check from whom is the message (initial call or outbound call) and update info accordingly
            if (sender == call) {
                callInfo = response.get();
                if (callInfo.state() == CallStateChanged.State.CANCELED || (callInfo.invite() != null && callInfo.invite().getSession().getState().equals(SipSession.State.TERMINATED))) {
                    fsm.transition(message, finished);
                    return;
                } else {
                    call.tell(new Observe(self()), self());
                    //Enable Monitoring Service for the call
                    if (monitoring != null)
                        call.tell(new Observe(monitoring), self());
                    if (sdr != null)
                        call.tell(new Observe(sdr), self());
                }
            } else {
                outboundCallInfo = response.get();
            }

            final String direction = callInfo.direction();
            if ("inbound".equals(direction)) {
                if (rcml!=null && !rcml.isEmpty()) {
                    if (logger.isInfoEnabled()) {
                        logger.info("System app is present will proceed to ready state, system app: "+rcml);
                    }
                    createInitialCallRecord((CallResponse) message);
                    fsm.transition(message, ready);
                } else {
                    fsm.transition(message, downloadingRcml);
                }
            } else {
                fsm.transition(message, initializingCall);
            }
        } else if (acquiringOutboundCallInfo.equals(state)) {
            final CallResponse response = (CallResponse) message;
            this.outboundCallInfo = response.get();
            fsm.transition(message, creatingBridge);
        }
    }

    private void onDownloaderResponse(Object message, State state) throws IOException, TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        final DownloaderResponse response = (DownloaderResponse) message;
        if (logger.isDebugEnabled()) {
            logger.debug("Download Rcml response succeeded " + response.succeeded());
            if (response.get() != null )
                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) {
                    getContext().stop(parser);
                    parser = null;
                }
                final String type = response.get().getContentType();
                if (type != null) {
                    if (type.contains("text/xml") || type.contains("application/xml") || type.contains("text/html")) {
                        parser = parser(response.get().getContentAsString());
                    } else if (type.contains("audio/wav") || type.contains("audio/wave") || type.contains("audio/x-wav")) {
                        parser = parser("" + request.getUri() + "");
                    } else if (type.contains("text/plain")) {
                        parser = parser("" + response.get().getContentAsString() + "");
                    }
                } else {
                    //If the waitUrl is invalid then move to notFound
                    fsm.transition(message, hangingUp);
                }
                final GetNextVerb next = new GetNextVerb();
                parser.tell(next, self());
                return;
            }
            if (dialBranches == null || dialBranches.size()==0) {
                if(logger.isInfoEnabled()) {
                    logger.info("Downloader response is success, moving to Ready state");
                }
                fsm.transition(message, ready);
            } else {
                return;
            }
        } else if (downloadingRcml.equals(state) && fallbackUrl != null) {
            fsm.transition(message, downloadingFallbackRcml);
        } else if (response.succeeded() && HttpStatus.SC_NOT_FOUND == response.get().getStatusCode()) {
            fsm.transition(message, notFound);
        } else {
            call.tell(new CallFail(response.error()), self());
//                    fsm.transition(message, finished);
        }
    }

    private void onCallStateChanged(Object message, ActorRef sender) throws TransitionFailedException, TransitionNotFoundException, TransitionRollbackException {
        final CallStateChanged event = (CallStateChanged) message;
        if (sender == call)
            callState = event.state();
        else
            if(event.sipResponse()!=null && event.sipResponse()>=400){
                outboundCallResponse = event.sipResponse();
            }
        if(logger.isInfoEnabled()){
            logger.info("VoiceInterpreter received CallStateChanged event: "+event+ " from "+(sender == call? "call" : "outboundCall")+ ", sender path: " + sender.path() +", current VI state: "+fsm.state() +" current outboundCall actor is: "+outboundCall);
        }

        switch (event.state()) {
            case QUEUED:
                //Do nothing
                break;
            case RINGING:
                if (logger.isInfoEnabled()) {
                    String msg = String.format("Got 180 Ringing from outbound call %s",sender);
                    logger.info(msg);
                }
                break;
            case CANCELED:
                if (is(initializingBridge) || is(acquiringOutboundCallInfo) || is(bridging) || is(bridged)) {
                    //This is a canceled branch from a previous forking call. We need to destroy the branch
//                    removeDialBranch(message, sender);
                    callManager.tell(new DestroyCall(sender), self());
                    return;
                } else {
                    if (enable200OkDelay && dialBranches != null && sender.equals(call)) {
                        if (callRecord != null) {
                            final CallDetailRecordsDao records = storage.getCallDetailRecordsDao();
                            callRecord = records.getCallDetailRecord(callRecord.getSid());
                            callRecord = callRecord.setStatus(callState.toString());
                            records.updateCallDetailRecord(callRecord);
                        }
                        fsm.transition(message, finishDialing);
                    } else if (sender == call) {
                        //Move to finished state only if the call actor send the Cancel.
                        fsm.transition(message, finished);
                    } else {
                        //This is a Cancel from a dial branch previously canceled

                        if (dialBranches != null && dialBranches.contains(sender)) {
                            removeDialBranch(message, sender);
                            checkDialBranch(message, sender, action);
                        }
                        else {
                            //case for LCM testTerminateDialForkCallWhileRinging_LCM_to_dial_branches
                            callState = event.state();
                    }
                }
                }
                break;
            case BUSY:
                if (is(forking)) {
                    if (sender == call) {
                        //Move to finishDialing to clear the call and cancel all branches
                        fsm.transition(message, finishDialing);
                    } else {
                        if (dialBranches != null && dialBranches.contains(sender)) {
                            removeDialBranch(message, sender);
                        }
                        if (dialBranches == null || dialBranches.size() == 0){
                            // Stop playing the ringing tone from inbound call
                            msResponsePending = true;
                            call.tell(new StopMediaGroup(), self());
                        }
                        checkDialBranch(message, sender, action);
                        return;
                    }
                } if (is(initializingCall)) {
                    fsm.transition(message, finished);
                } else {
                    fsm.transition(message, finishDialing);
                    return;
                }
                break;
            case NOT_FOUND:
                //Do nothing
                break;
            case NO_ANSWER:
                //NOANSWER calls should be canceled. At CANCELED event will be removed from
                //dialBranches and will be destroyed.
                if (is(bridging) || (is(bridged) && !sender.equals(call))) {
                    fsm.transition(message, finishDialing);
                } else if (is(forking)){
                    if (!sender.equals(call)) {
                        //One of the dial branches sent NO-ANSWER and we should ask to CANCEL
//                        sender.tell(new Cancel(), self());
                    }
                } else if (is(finishDialing)) {
                    if ((dialBranches == null || dialBranches.size()==0) && sender.equals(call)) {
                        //TODO HERE
                        logger.info("No-Answer event received, and dialBrances is either null or 0 size, sender: "+sender.path()+", vi state: "+fsm.state());
                        checkDialBranch(message, sender, action);
                    }
                }
                if (is(initializingCall)) {
                    sender.tell(new Hangup(), self());
                }
                break;
            case FAILED:
                if (!sender.equals(call)) {
                    if (dialBranches != null && dialBranches.contains(sender)) {
                        dialBranches.remove(sender);
                    }
                    checkDialBranch(message,sender,action);
                } else if (sender.equals(call)) {
                    fsm.transition(message, finished);
                }
                break;
            case COMPLETED:
                //NO_ANSWER, COMPLETED and FAILED events are handled the same
                if (logger.isInfoEnabled()) {
                    String msg = String.format("OnCallStateChanged, VI state %s, received %s, is it from inbound call: %s",fsm.state().toString(), callState.toString(), sender.equals(call));
                    logger.info(msg);
                }
                if (is(bridging) || is(bridged)) {
                    if (sender == outboundCall || sender == call) {
                        if(logger.isInfoEnabled()) {
                            String msg = String.format("Received CallStateChanged COMPLETED from call %s, current fsm state %s, will move to finishDialingState ", sender(), fsm.state());
                            logger.info(msg);
                        }
                        fsm.transition(message, finishDialing);
                    } else {
                        if (dialBranches != null && dialBranches.contains(sender)) {
                            removeDialBranch(message, sender);
                        }
                        callManager.tell(new DestroyCall(sender()), self());
                    }
                    return;
                }
                else
                    // changed for https://bitbucket.org/telestax/telscale-restcomm/issue/132/ so that we can do Dial SIP Screening
                    if (is(forking) && ((dialBranches != null && dialBranches.contains(sender)) || outboundCall == null)) {
                        if (!sender.equals(call)) {
                            removeDialBranch(message, sender);
                            //Properly clean up FAILED or BUSY outgoing calls
                            //callManager.tell(new DestroyCall(sender), self());
                            checkDialBranch(message,sender,action);
                            return;
                        } else {
                            fsm.transition(message, finishDialing);
                        }
                    } else if (is(creatingRecording)) {
                        fsm.transition(message, finishRecording);
                    } else if ((is(bridged) || is(forking)) && call == sender()) {
                        if (!dialActionExecuted) {
                            fsm.transition(message, finishDialing);
                        }
                    } else if (is(finishDialing)) {
                        if (sender.equals(call)) {
                            fsm.transition(message, finished);
                        } else {
                            checkDialBranch(message, sender(), action);
                        }
                        break;
                    } else if (is(conferencing) || is(finishConferencing)) {
                        //If the CallStateChanged.Completed event from the Call arrived before the ConferenceStateChange.Completed
                        //event, then return and wait for the FinishConferencing to deal with the event (either execute dial action or
                        //get next verb from parser
                        if (logger.isInfoEnabled()) {
                            logger.info("VoiceInterpreter received CallStateChanged.Completed VI in: " + fsm.state() + " state, will return and wait for ConferenceStateChanged.Completed event");
                        }
                        return;
                    } else {
                        if (!is(finishDialing) && !is(finished))
                            fsm.transition(message, finished);
                    }
                break;
            case WAIT_FOR_ANSWER:
            case IN_PROGRESS:
                if(call!=null && (call == sender) && event.state().equals(CallStateChanged.State.WAIT_FOR_ANSWER)){
                    callWaitingForAnswer  = true;
                }
                if (is(initializingCall) || is(rejecting)) {
                    if (parser != null) {
                        //This is an inbound call
                        fsm.transition(message, ready);
                    } else {
                        //This is a REST API created outgoing call
                        fsm.transition(message, downloadingRcml);
                    }
                } else if (is(forking)) {
                    if (outboundCall == null || !sender.equals(call)) {
                        if (logger.isInfoEnabled()) {
                            String msg = String.format("Got CallStateChanged.InProgress while forking, from outbound call %s, will proceed to cancel other branches", sender());
                            logger.info(msg);
                        }
                        outboundCall = sender;
                        fsm.transition(message, acquiringOutboundCallInfo);
                    }
                } else if (is(conferencing)) {
                    // Call left the conference successfully
                    if (!liveCallModification) {
                        // Hang up the call
                        final Hangup hangup = new Hangup();
                        call.tell(hangup, sender);
                    } else {
                        // XXX start processing new RCML and give instructions to call
                        // Ask the parser for the next action to take.
                        final GetNextVerb next = new GetNextVerb();
                        parser.tell(next, self());
                    }
                } else if (enable200OkDelay && sender.equals(call) && event.state().equals(CallStateChanged.State.IN_PROGRESS) && callWaitingForAnswerPendingTag != null) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Waiting call is inProgress we can proceed to the pending tag execution");
                    }
                    callWaitingForAnswer = false;
                    onTagMessage(callWaitingForAnswerPendingTag);
                }
                // Update the storage for conferencing.
                if (callRecord != null && !is(initializingCall) && !is(rejecting)) {
                    final CallDetailRecordsDao records = storage.getCallDetailRecordsDao();
                    callRecord = records.getCallDetailRecord(callRecord.getSid());
                    callRecord = callRecord.setStatus(callState.toString());
                    records.updateCallDetailRecord(callRecord);
                }
                break;
            }
    }

    private void removeDialBranch(Object message, ActorRef sender) {
        //Just remove the branch from dialBranches and send the CANCEL
        //Later at onCallStateChanged.CANCEL we should ask call manager to destroy call and
        //either execute dial action or ask parser for next verb
        CallStateChanged.State state = null;
        if (message instanceof CallStateChanged) {
            state = ((CallStateChanged)message).state();
        } else if (message instanceof  ReceiveTimeout) {
            state = CallStateChanged.State.NO_ANSWER;
        }
        if(logger.isInfoEnabled()) {
            logger.info("Dial branch new call state: " + state + " call path: " + sender().path() + " VI state: " + fsm.state());
        }
        if (state != null && !state.equals(CallStateChanged.State.CANCELED)) {
            if (logger.isInfoEnabled()) {
                logger.info("At removeDialBranch() will cancel call: "+sender.path()+", isTerminated: "+sender.isTerminated());
            }
            sender.tell(new Cancel(), self());
        }
        if (outboundCall != null && outboundCall.equals(sender)) {
            outboundCall = null;
        }
        if (dialBranches != null && dialBranches.contains(sender))
            dialBranches.remove(sender);
    }

    private void checkDialBranch(Object message, ActorRef sender, Attribute attribute) {
        CallStateChanged.State state = null;
        if (message instanceof CallStateChanged) {
            state = ((CallStateChanged)message).state();
        } else if (message instanceof  ReceiveTimeout) {
            state = CallStateChanged.State.NO_ANSWER;
        }

        if (dialBranches == null || dialBranches.size() == 0) {
            dialBranches = null;

            if (attribute == null) {
                if (logger.isInfoEnabled()) {
                    logger.info("Attribute is null, will destroy call and ask for the next verb from parser");
                }
                if (sender != null && !sender.equals(call)) {
                    callManager.tell(new DestroyCall(sender), self());
                }
                // VI cannot move to next verb if media still being reproduced by media server
                // GetNextVerb is skipped while StopMediaGroup request is sent to media server
                // RCML Parser/VI activity continues when media server successful response is received
                if (parser != null && !msResponsePending) {
                    final GetNextVerb next = new GetNextVerb();
                    parser.tell(next, self());
                }
            } else {
                if (logger.isInfoEnabled()) {
                    logger.info("Executing Dial Action for inbound call");
                }
                if (sender.equals(call)) {
                    executeDialAction(message, outboundCall);
                } else {
                    executeDialAction(message, sender);
                }
                if (sender != null && !sender.equals(call)) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Will destroy sender");
                    }
                    callManager.tell(new DestroyCall(sender), self());
                }
            }
            if (bridge != null) {
                // Stop the bridge
                bridge.tell(new StopBridge(liveCallModification), self());
                recordingCall = false;
                bridge = null;
            }
        } else if (state != null && (state.equals(CallStateChanged.State.BUSY) ||
                state.equals(CallStateChanged.State.CANCELED) ||
                state.equals(CallStateChanged.State.FAILED))) {
            callManager.tell(new DestroyCall(sender), self());
        }
    }

    private void onBridgeManagerResponse(BridgeManagerResponse message, ActorRef self, ActorRef sender) throws Exception {
        if (is(creatingBridge)) {
            this.bridge = message.get();
            fsm.transition(message, initializingBridge);
        }
    }

    private void onBridgeStateChanged(BridgeStateChanged message, ActorRef self, ActorRef sender) throws Exception {
        switch (message.getState()) {
            case READY:
                if (is(initializingBridge)) {
                    fsm.transition(message, bridging);
                }
                break;
            case BRIDGED:
                if (is(bridging)) {
                    fsm.transition(message, bridged);
                }
                break;

            case FAILED:
                if (is(initializingBridge)) {
                    fsm.transition(message, hangingUp);
                }
            default:
                break;
        }
    }

    private void onGetRelatedCall(GetRelatedCall message, ActorRef self, ActorRef sender) {
        final ActorRef callActor = message.call();
        if (is(forking)) {
            sender.tell(dialBranches, self);
            return;
        }
        if (outboundCall != null) {
            if (callActor.equals(outboundCall)) {
                sender.tell(call, self);
            } else if (callActor.equals(call)) {
                sender.tell(outboundCall, self);
            }
        } else {
            // If previously that was a p2p call that changed to conference (for hold)
            // and now it changes again to a new url, the outbound call is null since
            // When we joined the call to the conference, we made outboundCall = null;
            sender.tell(new org.restcomm.connect.telephony.api.NotFound(), sender);
        }
    }

    private void onCallHoldStateChange(CallHoldStateChange message, ActorRef sender){
        if (logger.isInfoEnabled()) {
            logger.info("CallHoldStateChange received, state: " + message.state());
        }
        if (asImsUa){
            if (sender.equals(outboundCall)) {
                call.tell(message, self());
            } else if (sender.equals(call)) {
                outboundCall.tell(message, self());
            }
        }
    }

    private void conferenceStateModeratorPresent(final Object message) {
        if(logger.isInfoEnabled()) {
            logger.info("VoiceInterpreter#conferenceStateModeratorPresent will unmute the call: " + call.path().toString()+", direction: "+callInfo.direction());
        }
        call.tell(new Unmute(), self());

        if (confSubVoiceInterpreter != null) {
        if(logger.isInfoEnabled()) {
            logger.info("VoiceInterpreter stopping confSubVoiceInterpreter");
        }

            // Stop the conference back ground music
            final StopInterpreter stop = new StopInterpreter();
            confSubVoiceInterpreter.tell(stop, self());
        }
    }

    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.getMain().getInstanceId()));
        if (outboundCallInfo != null) {
            final String outboundCallSid = outboundCallInfo.sid().toString();
            parameters.add(new BasicNameValuePair("OutboundCallSid", outboundCallSid));
        }
        final String accountSid = accountId.toString();
        parameters.add(new BasicNameValuePair("AccountSid", accountSid));
        final String from = e164(callInfo.from());
        parameters.add(new BasicNameValuePair("From", from));
        final String to = e164(callInfo.to());
        parameters.add(new BasicNameValuePair("To", to));
        final String state = callState.toString();
        parameters.add(new BasicNameValuePair("CallStatus", state));
        parameters.add(new BasicNameValuePair("ApiVersion", version));
        final String direction = callInfo.direction();
        parameters.add(new BasicNameValuePair("Direction", direction));
        final String callerName = (callInfo.fromName() == null || callInfo.fromName().isEmpty()) ? "null" : callInfo.fromName();
        parameters.add(new BasicNameValuePair("CallerName", callerName));
        final String forwardedFrom = (callInfo.forwardedFrom() == null || callInfo.forwardedFrom().isEmpty()) ? "null" : callInfo.forwardedFrom();
        parameters.add(new BasicNameValuePair("ForwardedFrom", forwardedFrom));
        parameters.add(new BasicNameValuePair("CallTimestamp", callInfo.dateCreated().toString()));
        if (referTarget != null) {
            parameters.add(new BasicNameValuePair("ReferTarget", referTarget));
        }
        if (transferor != null) {
            parameters.add(new BasicNameValuePair("Transferor", transferor));
        }
        if (transferee != null) {
            parameters.add(new BasicNameValuePair("Transferee", transferee));
        }
        // logger.info("Type " + callInfo.type());
        SipServletResponse lastResponse = callInfo.lastResponse();
        if (CreateCallType.SIP == callInfo.type()) {
            // Adding SIP OUT Headers and SipCallId for
            // https://bitbucket.org/telestax/telscale-restcomm/issue/132/implement-twilio-sip-out
            // logger.info("lastResponse " + lastResponse);
            if (lastResponse != null) {
                final int statusCode = lastResponse.getStatus();
                final String method = lastResponse.getMethod();
                // See https://www.twilio.com/docs/sip/receiving-sip-headers
                // Headers on the final SIP response message (any 4xx or 5xx message or the final BYE/200) are posted to the
                // Dial action URL.
                if ((statusCode >= 400 && "INVITE".equalsIgnoreCase(method))
                        || (statusCode >= 200 && statusCode < 300 && "BYE".equalsIgnoreCase(method))) {
                    final String sipCallId = lastResponse.getCallId();
                    parameters.add(new BasicNameValuePair("DialSipCallId", sipCallId));
                    parameters.add(new BasicNameValuePair("DialSipResponseCode", "" + statusCode));
                    processCustomAndDiversionHeaders(lastResponse, "DialSipHeader_", parameters);
                }
            }
        }

        if (lastResponse == null) {
            // Restcomm VoiceInterpreter should check the INVITE for custom headers and pass them to RVD
            // https://telestax.atlassian.net/browse/RESTCOMM-710
            final SipServletRequest invite = callInfo.invite();
            // For outbound calls created with Calls REST API, the invite at this point will be null
            if (invite != null) {
                processCustomAndDiversionHeaders(invite, "SipHeader_", parameters);

            }
        } else {
            processCustomAndDiversionHeaders(lastResponse, "SipHeader_", parameters);
        }

        return parameters;
    }

    private void processCustomAndDiversionHeaders(SipServletMessage sipMessage, String prefix, List parameters) {
        Iterator headerNames = sipMessage.getHeaderNames();
        while (headerNames.hasNext()) {
            String headerName = headerNames.next();
            if (headerName.startsWith("X-")) {
                if (logger.isDebugEnabled()) {
                    logger.debug("%%%%%%%%%%% Identified customer header: " + headerName);
                }
                parameters.add(new BasicNameValuePair(prefix + headerName, sipMessage.getHeader(headerName)));
            } else if (headerName.startsWith("Diversion")) {

                final String sipDiversionHeader = sipMessage.getHeader(headerName);
                if (logger.isDebugEnabled()) {
                    logger.debug("%%%%%%%%%%% Identified diversion header: " + sipDiversionHeader);
                }
                parameters.add(new BasicNameValuePair(prefix + headerName, sipDiversionHeader));

                try {
                    forwardedFrom = sipDiversionHeader.substring(sipDiversionHeader.indexOf("sip:") + 4,
                            sipDiversionHeader.indexOf("@"));

                    for(int i=0; i < parameters.size(); i++) {
                        if (parameters.get(i).getName().equals("ForwardedFrom")) {
                            if (parameters.get(i).getValue().equals("null")) {
                                parameters.remove(i);
                                parameters.add(new BasicNameValuePair("ForwardedFrom", forwardedFrom));
                                break;
                            } else {
                                // Not null, so it's not going to be overwritten with Diversion Header
                                break;
                            }
                        }
                    }
                } catch (Exception e) {
                    logger.warning("Error parsing SIP Diversion header"+ e.getMessage());
                }
            }
        }
    }

    private abstract class AbstractAction implements Action {
        protected final ActorRef source;

        public AbstractAction(final ActorRef source) {
            super();
            this.source = source;
        }

        protected Tag conference(final Tag container) {
            final List children = container.children();
            for (final Tag child : children) {
                if (Nouns.conference.equals(child.name())) {
                    return child;
                }
            }
            return null;
        }

        protected Tag video(final Tag container) {
            final List children = container.children();
            for (final Tag child : children) {
                if (Nouns.video.equals(child.name())) {
                    return child;
                }
            }
            return null;
        }
    }

    private final class InitializingCall extends AbstractAction {
        public InitializingCall(final ActorRef source) {
            super(source);
        }

        @SuppressWarnings("unchecked")
        @Override
        public void execute(final Object message) throws Exception {
            final Class klass = message.getClass();

            if (CallResponse.class.equals(klass)) {
                // Update the interpreter state.
                final CallResponse response = (CallResponse) message;
                callInfo = response.get();
                callState = callInfo.state();
                if (callState.name().equalsIgnoreCase(CallStateChanged.State.IN_PROGRESS.name())) {
                    final CallStateChanged event = new CallStateChanged(CallStateChanged.State.IN_PROGRESS);
                    source.tell(event, source);
                    // fsm.transition(event, acquiringCallMediaGroup);
                    return;
                }

                // Update the storage.
                if (callRecord != null) {
                    callRecord = callRecord.setStatus(callState.toString());
                    final CallDetailRecordsDao records = storage.getCallDetailRecordsDao();
                    records.updateCallDetailRecord(callRecord);
                }

                // Update the application.
                callback();

                // Start dialing.
                call.tell(new Dial(), source);
                // Set the timeout period.
                final UntypedActorContext context = getContext();
                context.setReceiveTimeout(Duration.create(timeout, TimeUnit.SECONDS));
            } else if (Tag.class.equals(klass)) {
                // Update the interpreter state.
                verb = (Tag) message;

                // Answer the call.
                boolean confirmCall = true;
                if (enable200OkDelay && Verbs.dial.equals(verb.name())) {
                    confirmCall=false;
            }
                call.tell(new Answer(callRecord.getSid(),confirmCall), source);
        }
    }
    }

    private final class DownloadingRcml extends AbstractAction {
        public DownloadingRcml(final ActorRef source) {
            super(source);
        }

        @SuppressWarnings("unchecked")
        @Override
        public void execute(final Object message) throws Exception {
            final Class klass = message.getClass();
            if (CallResponse.class.equals(klass)) {
                createInitialCallRecord((CallResponse) message);
            }
            // Ask the downloader to get us the application that will be executed.
            final List parameters = parameters();
            request = new HttpRequestDescriptor(url, method, parameters);
            downloader.tell(request, source);
        }
    }

    private void createInitialCallRecord(CallResponse message) {
        final CallDetailRecordsDao records = storage.getCallDetailRecordsDao();
        final CallResponse response = message;
        callInfo = response.get();
        callState = callInfo.state();
        if (callInfo.direction().equals("inbound")) {
            callRecord = records.getCallDetailRecord(callInfo.sid());
            if (callRecord == null) {
                // 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.setDateCreated(callInfo.dateCreated());
                builder.setAccountSid(accountId);
                builder.setTo(callInfo.to());
                if (callInfo.fromName() != null) {
                    builder.setCallerName(callInfo.fromName());
                } else {
                    builder.setCallerName("Unknown");
                }
                if (callInfo.from() != null) {
                    builder.setFrom(callInfo.from());
                } else {
                    builder.setFrom("Unknown");
                }
                builder.setForwardedFrom(callInfo.forwardedFrom());
                builder.setPhoneNumberSid(phoneId);
                builder.setStatus(callState.toString());
                final DateTime now = DateTime.now();
                builder.setStartTime(now);
                builder.setDirection(callInfo.direction());
                builder.setApiVersion(version);
                builder.setPrice(new BigDecimal("0.00"));
                builder.setMuted(false);
                builder.setOnHold(false);
                // TODO implement currency property to be read from Configuration
                builder.setPriceUnit(Currency.getInstance("USD"));
                final StringBuilder buffer = new StringBuilder();
                buffer.append("/").append(version).append("/Accounts/");
                buffer.append(accountId.toString()).append("/Calls/");
                buffer.append(callInfo.sid().toString());
                final URI uri = URI.create(buffer.toString());
                builder.setUri(uri);

                builder.setCallPath(call.path().toString());

                callRecord = builder.build();
                records.addCallDetailRecord(callRecord);
            }

            // Update the application.
            callback();
        }
    }

    private final class DownloadingFallbackRcml extends AbstractAction {
        public DownloadingFallbackRcml(final ActorRef source) {
            super(source);
        }

        @Override
        public void execute(final Object message) throws Exception {
            final Class klass = message.getClass();
            // Notify the account of the issue.
            if (DownloaderResponse.class.equals(klass)) {
                final DownloaderResponse result = (DownloaderResponse) message;
                final Throwable cause = result.cause();
                Notification notification = null;
                if (cause instanceof ClientProtocolException) {
                    notification = notification(ERROR_NOTIFICATION, 11206, cause.getMessage());
                } else if (cause instanceof IOException) {
                    notification = notification(ERROR_NOTIFICATION, 11205, cause.getMessage());
                } else if (cause instanceof URISyntaxException) {
                    notification = notification(ERROR_NOTIFICATION, 11100, cause.getMessage());
                }
                if (notification != null) {
                    final NotificationsDao notifications = storage.getNotificationsDao();
                    notifications.addNotification(notification);
                    sendMail(notification);
                }
            }
            // Try to use the fall back url and method.
            final List parameters = parameters();
            request = new HttpRequestDescriptor(fallbackUrl, fallbackMethod, parameters);
            downloader.tell(request, source);
        }
    }

    private final class Ready extends AbstractAction {
        public Ready(final ActorRef source) {
            super(source);
        }

        @Override
        public void execute(final Object message) throws IOException {
            final UntypedActorContext context = getContext();
            final State state = fsm.state();
            if (initializingCall.equals(state)) {
                // Update the interpreter state.
                final CallStateChanged event = (CallStateChanged) message;
                callState = event.state();

                // Update the application.
                callback();

                // Update the storage.
                if (callRecord != null) {
                    final CallDetailRecordsDao records = storage.getCallDetailRecordsDao();
                    callRecord = records.getCallDetailRecord(callRecord.getSid());
                    callRecord = callRecord.setStatus(callState.toString());
                    callRecord = callRecord.setStartTime(DateTime.now());
                    callRecord = callRecord.setForwardedFrom(forwardedFrom);
                    records.updateCallDetailRecord(callRecord);
                }

                // Handle pending verbs.
                source.tell(verb, source);
                return;
            } else if (downloadingRcml.equals(state) || downloadingFallbackRcml.equals(state) || redirecting.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) {
                    context.stop(parser);
                    parser = null;
                }
                final String type = response.getContentType();
                if (type != null) {
                        if (type.contains("text/xml") || type.contains("application/xml") || type.contains("text/html")) {
                            parser = parser(response.getContentAsString());
                        } else if (type.contains("audio/wav") || type.contains("audio/wave") || type.contains("audio/x-wav")) {
                            parser = parser("" + request.getUri() + "");
                        } else if (type.contains("text/plain")) {
                            parser = parser("" + response.getContentAsString() + "");
                        }
                } else {
                    if (call != null) {
                        call.tell(new Hangup(outboundCallResponse), null);
                    }
                    final StopInterpreter stop = new StopInterpreter();
                    source.tell(stop, source);
                    return;
                }
            } else if ((message instanceof CallResponse) && (rcml != null && !rcml.isEmpty())) {
                if (parser != null) {
                    context.stop(parser);
                    parser = null;
                }
                parser = parser(rcml);
            } else if (pausing.equals(state)) {
                context.setReceiveTimeout(Duration.Undefined());
            }
            // Ask the parser for the next action to take.
            final GetNextVerb next = new GetNextVerb();
            if (parser != null) {
                parser.tell(next, source);
            } else if(logger.isInfoEnabled()) {
                logger.info("Parser is null");
            }

        }
    }

    private final class NotFound extends AbstractAction {
        public NotFound(final ActorRef source) {
            super(source);
        }

        @Override
        public void execute(final Object message) throws Exception {
            final DownloaderResponse response = (DownloaderResponse) message;
            if (logger.isDebugEnabled()) {
                logger.debug("response succeeded " + response.succeeded() + ", statusCode " + response.get().getStatusCode());
            }
            final Notification notification = notification(WARNING_NOTIFICATION, 21402, "URL Not Found : "
                    + response.get().getURI());
            final NotificationsDao notifications = storage.getNotificationsDao();
            notifications.addNotification(notification);
            // Hang up the call.
            call.tell(new org.restcomm.connect.telephony.api.NotFound(), source);
        }
    }

    private final class Rejecting extends AbstractAction {
        public Rejecting(final ActorRef source) {
            super(source);
        }

        @Override
        public void execute(final Object message) throws Exception {
            final Class klass = message.getClass();
            if (Tag.class.equals(klass)) {
                verb = (Tag) message;
            }
            String reason = "rejected";
            Attribute attribute = verb.attribute("reason");
            if (attribute != null) {
                reason = attribute.value();
                if (reason != null && !reason.isEmpty()) {
                    if ("rejected".equalsIgnoreCase(reason)) {
                        reason = "rejected";
                    } else if ("busy".equalsIgnoreCase(reason)) {
                        reason = "busy";
                    } else {
                        reason = "rejected";
                    }
                } else {
                    reason = "rejected";
                }
            }
            // Reject the call.
            call.tell(new Reject(reason), source);
        }
    }

    private abstract class AbstractDialAction extends AbstractAction {
        public AbstractDialAction(final ActorRef source) {
            super(source);
        }

        protected String callerId(final Tag container) {
            // Parse "from".
            String callerId = null;

            // Issue 210: https://telestax.atlassian.net/browse/RESTCOMM-210
            final boolean useInitialFromAsCallerId = configuration.subset("runtime-settings").getBoolean("from-address-to-proxied-calls");

            Attribute attribute = verb.attribute("callerId");
            if (attribute != null) {
                callerId = attribute.value();
                if (callerId != null && !callerId.isEmpty()) {
                    callerId = e164(callerId);
                    if (callerId == null) {
                        callerId = verb.attribute("callerId").value();
                        final NotificationsDao notifications = storage.getNotificationsDao();
                        final Notification notification = notification(ERROR_NOTIFICATION, 13214, callerId
                                + " is an invalid callerId.");
                        notifications.addNotification(notification);
                        sendMail(notification);
                        final StopInterpreter stop = new StopInterpreter();
                        source.tell(stop, source);
                        return null;
                    }
                }
            }

            if (callerId == null && useInitialFromAsCallerId)
                callerId = callInfo.from();

            return callerId;
        }

        protected int timeout(final Tag container) {
            int timeout = 30;
            Attribute attribute = container.attribute("timeout");
            if (attribute != null) {
                final String value = attribute.value();
                if (value != null && !value.isEmpty()) {
                    try {
                        timeout = Integer.parseInt(value);
                    } catch (final NumberFormatException exception) {
                        final NotificationsDao notifications = storage.getNotificationsDao();
                        final Notification notification = notification(WARNING_NOTIFICATION, 13212, value
                                + " is not a valid timeout value for ");
                        notifications.addNotification(notification);
                    }
                }
            }
            return timeout;
        }

        protected int timeLimit(final Tag container) {
            int timeLimit = 14400;
            Attribute attribute = container.attribute("timeLimit");
            if (attribute != null) {
                final String value = attribute.value();
                if (value != null && !value.isEmpty()) {
                    try {
                        timeLimit = Integer.parseInt(value);
                    } catch (final NumberFormatException exception) {
                        final NotificationsDao notifications = storage.getNotificationsDao();
                        final Notification notification = notification(WARNING_NOTIFICATION, 13216, value
                                + " is not a valid timeLimit value for ");
                        notifications.addNotification(notification);
                    }
                }
            }
            return timeLimit;
        }

        protected MediaAttributes.MediaType videoEnabled(final Tag container) {
            boolean videoEnabled = false;
            Attribute attribute = container.attribute("enable");
            if (attribute != null) {
                final String value = attribute.value();
                if (value != null && !value.isEmpty()) {
                    videoEnabled = Boolean.valueOf(value);
                }
            }
            if (videoEnabled) {
                return MediaAttributes.MediaType.AUDIO_VIDEO;
            } else {
                return MediaAttributes.MediaType.AUDIO_ONLY;
            }
        }

        protected MediaAttributes.VideoMode videoMode(final Tag container){
            MediaAttributes.VideoMode videoMode = MediaAttributes.VideoMode.MCU;
            Attribute attribute = container.attribute("mode");
            if (attribute != null) {
                final String value = attribute.value();
                if (value != null && !value.isEmpty()) {
                    try {
                        videoMode = MediaAttributes.VideoMode.getValueOf(value);
                    } catch (IllegalArgumentException e) {
                        final NotificationsDao notifications = storage.getNotificationsDao();
                        final Notification notification = notification(WARNING_NOTIFICATION, 15001, value
                                + " is not a valid mode value for