/*
* TeleStax, Open Source Cloud Communications
* Copyright 2011-2016, Telestax Inc and individual contributors
* by the @authors tag.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.restcomm.media.control.mgcp.pkg.au.pc;
import java.net.MalformedURLException;
import java.util.concurrent.TimeUnit;
import org.apache.log4j.Logger;
import org.restcomm.media.control.mgcp.pkg.MgcpEventSubject;
import org.restcomm.media.control.mgcp.pkg.au.OperationComplete;
import org.restcomm.media.control.mgcp.pkg.au.OperationFailed;
import org.restcomm.media.control.mgcp.pkg.au.Playlist;
import org.restcomm.media.control.mgcp.pkg.au.ReturnCode;
import org.restcomm.media.spi.ResourceUnavailableException;
import org.restcomm.media.spi.dtmf.DtmfDetector;
import org.restcomm.media.spi.dtmf.DtmfDetectorListener;
import org.restcomm.media.spi.listener.TooManyListenersException;
import org.restcomm.media.spi.player.Player;
import org.restcomm.media.spi.player.PlayerListener;
import org.squirrelframework.foundation.fsm.impl.AbstractStateMachine;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
/**
* @author Henrique Rosa (henrique.rosa@telestax.com)
*
*/
public class PlayCollectFsmImpl extends
AbstractStateMachine<PlayCollectFsm, PlayCollectState, PlayCollectEvent, PlayCollectContext> implements PlayCollectFsm {
private static final Logger log = Logger.getLogger(PlayCollectFsmImpl.class);
// Scheduler
private final ListeningScheduledExecutorService executor;
// Event Listener
private final MgcpEventSubject mgcpEventSubject;
// Media Components
private final DtmfDetector detector;
final DtmfDetectorListener detectorListener;
private final Player player;
final PlayerListener playerListener;
// Execution Context
private final PlayCollectContext context;
public PlayCollectFsmImpl(DtmfDetector detector, DtmfDetectorListener detectorListener, Player player,
PlayerListener playerListener, MgcpEventSubject mgcpEventSubject, ListeningScheduledExecutorService executor,
PlayCollectContext context) {
super();
// Scheduler
this.executor = executor;
// Event Listener
this.mgcpEventSubject = mgcpEventSubject;
// Media Components
this.detector = detector;
this.detectorListener = detectorListener;
this.player = player;
this.playerListener = playerListener;
// Execution Context
this.context = context;
}
private void playAnnouncement(String url, long delay) {
try {
this.player.setInitialDelay(delay);
this.player.setURL(url);
this.player.activate();
} catch (MalformedURLException e) {
log.warn("Could not play malformed segment " + url);
context.setReturnCode(ReturnCode.BAD_AUDIO_ID.code());
fire(PlayCollectEvent.FAIL, context);
// TODO create transition from PROMPTING to FAILED
} catch (ResourceUnavailableException e) {
log.warn("Could not play unavailable segment " + url);
context.setReturnCode(ReturnCode.BAD_AUDIO_ID.code());
fire(PlayCollectEvent.FAIL, context);
// TODO create transition from PROMPTING to FAILED
}
}
@Override
public void enterPlayCollect(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered PLAY_COLLECT state");
}
}
@Override
public void exitPlayCollect(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Exited PLAY_COLLECT state");
}
}
@Override
public void enterLoadingPlaylist(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered LOADING PLAYLIST state");
}
if (event == null) {
final Playlist prompt = context.getInitialPrompt();
if (prompt.isEmpty()) {
fire(PlayCollectEvent.NO_PROMPT, context);
} else {
fire(PlayCollectEvent.PROMPT, context);
}
} else {
switch (event) {
case RESTART:
final Playlist reprompt = context.getReprompt();
if (reprompt.isEmpty()) {
fire(PlayCollectEvent.NO_PROMPT, context);
} else {
fire(PlayCollectEvent.REPROMPT, context);
}
break;
case NO_DIGITS:
final Playlist noDigitsReprompt = context.getNoDigitsReprompt();
if (noDigitsReprompt.isEmpty()) {
fire(PlayCollectEvent.NO_PROMPT, context);
} else {
fire(PlayCollectEvent.NO_DIGITS, context);
}
break;
case REINPUT:
default:
fire(PlayCollectEvent.NO_PROMPT, context);
break;
}
}
}
@Override
public void exitLoadingPlaylist(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Exited LOADING PLAYLIST state");
}
}
@Override
public void enterPrompting(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered PROMPTING state");
}
final Playlist prompt = context.getInitialPrompt();
final String track = prompt.next();
try {
this.player.addListener(this.playerListener);
playAnnouncement(track, 0L);
} catch (TooManyListenersException e) {
log.error("Too many player listeners", e);
context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code());
fire(PlayCollectEvent.FAIL, context);
}
}
@Override
public void onPrompting(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("On PROMPTING state");
}
final Playlist prompt = context.getInitialPrompt();
final String track = prompt.next();
if (track.isEmpty()) {
// No more announcements to play
fire(PlayCollectEvent.END_PROMPT, context);
} else {
playAnnouncement(track, 10 * 100);
}
}
@Override
public void exitPrompting(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Exited PROMPTING state");
}
this.player.removeListener(this.playerListener);
this.player.deactivate();
}
@Override
public void enterReprompting(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered REPROMPTING state");
}
final Playlist prompt = context.getReprompt();
final String track = prompt.next();
try {
this.player.addListener(this.playerListener);
playAnnouncement(track, 0L);
} catch (TooManyListenersException e) {
log.error("Too many player listeners", e);
context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code());
fire(PlayCollectEvent.FAIL, context);
}
}
@Override
public void onReprompting(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("On REPROMPTING state");
}
final Playlist prompt = context.getReprompt();
final String track = prompt.next();
if (track.isEmpty()) {
// No more announcements to play
fire(PlayCollectEvent.END_PROMPT, context);
} else {
playAnnouncement(track, 10 * 100);
}
}
@Override
public void exitReprompting(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Exited REPROMPTING state");
}
this.player.removeListener(this.playerListener);
this.player.deactivate();
}
@Override
public void enterNoDigitsReprompting(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered NO DIGITS REPROMPTING state");
}
final Playlist prompt = context.getNoDigitsReprompt();
final String track = prompt.next();
try {
this.player.addListener(this.playerListener);
playAnnouncement(track, 0L);
} catch (TooManyListenersException e) {
log.error("Too many player listeners", e);
context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code());
fire(PlayCollectEvent.FAIL, context);
}
}
@Override
public void onNoDigitsReprompting(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("On NO DIGITS REPROMPTING state");
}
final Playlist prompt = context.getNoDigitsReprompt();
final String track = prompt.next();
if (track.isEmpty()) {
// No more announcements to play
fire(PlayCollectEvent.END_PROMPT, context);
} else {
playAnnouncement(track, 10 * 100);
}
}
@Override
public void exitNoDigitsReprompting(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Exited NO DIGITS REPROMPTING state");
}
this.player.removeListener(this.playerListener);
this.player.deactivate();
}
@Override
public void enterPrompted(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
// Check if no digit has been pressed while prompt was playing
if (context.countCollectedDigits() == 0) {
// Activate timer for first digit
if(log.isTraceEnabled()) {
log.trace("Scheduled First Digit Timer to fire in " + context.getFirstDigitTimer() + " ms");
}
this.executor.schedule(new DetectorTimer(context), context.getFirstDigitTimer(), TimeUnit.MILLISECONDS);
}
}
@Override
public void enterCollecting(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered COLLECTING state");
}
try {
// Activate DTMF detector and bind listener
this.detector.addListener(this.detectorListener);
this.detector.activate();
} catch (TooManyListenersException e) {
log.error("Too many DTMF listeners", e);
}
}
@Override
public void onCollecting(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace(
"On COLLECTING state [digits=" + context.getCollectedDigits() + ", attempt=" + context.getAttempt() + "]");
}
// Stop current prompt IF is interruptible
if (!context.getNonInterruptibleAudio()) {
// TODO check if child state PLAYING is currently active
fire(PlayCollectEvent.END_PROMPT, context);
}
final char tone = context.getLastTone();
if (context.getReinputKey() == tone) {
// Force collection to cancel any scheduled timeout
context.collectDigit(tone);
fire(PlayCollectEvent.REINPUT, context);
} else if (context.getRestartKey() == tone) {
// Force collection to cancel any scheduled timeout
context.collectDigit(tone);
fire(PlayCollectEvent.RESTART, context);
} else if (context.getEndInputKey() == tone) {
fire(PlayCollectEvent.END_INPUT, context);
} else {
// Make sure first digit matches StartInputKey
if (context.countCollectedDigits() == 0 && context.getStartInputKeys().indexOf(tone) == -1) {
log.info("Dropping tone " + tone + " because it does not match any of StartInputKeys "
+ context.getStartInputKeys());
return;
}
// Append tone to list of collected digits
context.collectDigit(tone);
// Stop collecting if maximum number of digits was reached.
// Only verified if no Digit Pattern was defined.
if (!context.hasDigitPattern() && context.countCollectedDigits() == context.getMaximumDigits()) {
fire(PlayCollectEvent.END_INPUT, context);
} else {
// Start interdigit timer
if(log.isTraceEnabled()) {
log.trace("Scheduled Inter Digit Timer to fire in " + context.getFirstDigitTimer() + " ms");
}
this.executor.schedule(new DetectorTimer(context), context.getInterDigitTimer(), TimeUnit.MILLISECONDS);
}
}
}
@Override
public void exitCollecting(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Exited COLLECTING state");
}
this.detector.removeListener(this.detectorListener);
this.detector.deactivate();
}
@Override
public void enterEvaluating(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered EVALUATING state.");
}
final int digitCount = context.countCollectedDigits();
if (digitCount == 0) {
// No digits were collected
fire(PlayCollectEvent.NO_DIGITS, context);
} else if (context.hasDigitPattern()) {
// Succeed if digit pattern matches. Otherwise retry
if (context.getCollectedDigits().matches(context.getDigitPattern())) {
fire(PlayCollectEvent.SUCCEED, context);
} else {
fire(PlayCollectEvent.PATTERN_MISMATCH, context);
}
} else if (digitCount < context.getMinimumDigits()) {
// Minimum digits not met
fire(PlayCollectEvent.PATTERN_MISMATCH, context);
} else {
// Pattern validation was successful
fire(PlayCollectEvent.SUCCEED, context);
}
}
@Override
public void exitEvaluating(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Exited EVALUATING state");
}
}
@Override
public void enterCanceled(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered CANCELED state");
}
final int digitCount = context.countCollectedDigits();
if (digitCount == 0) {
// No digits were collected
context.setReturnCode(ReturnCode.NO_DIGITS.code());
fire(PlayCollectEvent.FAIL, context);
} else if (context.hasDigitPattern()) {
// Succeed if digit pattern matches. Otherwise retry
if (context.getCollectedDigits().matches(context.getDigitPattern())) {
fire(PlayCollectEvent.SUCCEED, context);
} else {
context.setReturnCode(ReturnCode.DIGIT_PATTERN_NOT_MATCHED.code());
fire(PlayCollectEvent.FAIL, context);
}
} else if (digitCount < context.getMinimumDigits()) {
// Minimum digits not met
context.setReturnCode(ReturnCode.DIGIT_PATTERN_NOT_MATCHED.code());
fire(PlayCollectEvent.FAIL, context);
} else {
// Pattern validation was successful
fire(PlayCollectEvent.SUCCEED, context);
}
}
@Override
public void exitCanceled(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Exited CANCELED state");
}
}
@Override
public void enterSucceeding(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered SUCCEEDING state");
}
final Playlist prompt = context.getSuccessAnnouncement();
if (prompt.isEmpty()) {
fire(PlayCollectEvent.NO_PROMPT, context);
} else {
fire(PlayCollectEvent.PROMPT, context);
}
}
@Override
public void exitSucceeding(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered SUCCEEDING state");
}
}
@Override
public void enterPlayingSuccess(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered PLAYING SUCCESS state");
}
final Playlist prompt = context.getSuccessAnnouncement();
final String track = prompt.next();
try {
this.player.addListener(this.playerListener);
playAnnouncement(track, 0L);
} catch (TooManyListenersException e) {
log.error("Too many player listeners", e);
context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code());
fire(PlayCollectEvent.FAIL, context);
}
}
@Override
public void onPlayingSuccess(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("On PLAYING SUCCESS state");
}
final Playlist prompt = context.getSuccessAnnouncement();
final String track = prompt.next();
if (track.isEmpty()) {
// No more announcements to play
fire(PlayCollectEvent.END_PROMPT, context);
} else {
playAnnouncement(track, 10 * 100);
}
}
@Override
public void exitPlayingSuccess(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Exited PLAYING SUCCESS state");
}
this.player.removeListener(this.playerListener);
this.player.deactivate();
}
@Override
public void enterSucceeded(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered SUCCEEDED state");
}
final int attempt = context.getAttempt();
String collectedDigits = context.getCollectedDigits();
if (context.getIncludeEndInputKey()) {
collectedDigits += context.getEndInputKey();
}
final OperationComplete operationComplete = new OperationComplete(PlayCollect.SYMBOL, ReturnCode.SUCCESS.code());
operationComplete.setParameter("na", String.valueOf(attempt));
operationComplete.setParameter("dc", collectedDigits);
this.mgcpEventSubject.notify(this.mgcpEventSubject, operationComplete);
}
@Override
public void enterFailing(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered FAILING state");
}
if (context.hasMoreAttempts()) {
context.newAttempt();
switch (event) {
case RESTART:
case REINPUT:
case NO_DIGITS:
fire(event, context);
break;
case PATTERN_MISMATCH:
default:
fire(PlayCollectEvent.RESTART, context);
break;
}
} else {
switch (event) {
case NO_DIGITS:
context.setReturnCode(ReturnCode.NO_DIGITS.code());
break;
case PATTERN_MISMATCH:
context.setReturnCode(ReturnCode.DIGIT_PATTERN_NOT_MATCHED.code());
break;
case RESTART:
case REINPUT:
context.setReturnCode(ReturnCode.MAX_ATTEMPTS_EXCEEDED.code());
break;
default:
context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code());
break;
}
final Playlist prompt = context.getFailureAnnouncement();
if (prompt.isEmpty()) {
fire(PlayCollectEvent.NO_PROMPT, context);
} else {
fire(PlayCollectEvent.PROMPT, context);
}
}
}
@Override
public void exitFailing(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Exited FAILING state");
}
}
@Override
public void enterPlayingFailure(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered PLAYING FAILURE state");
}
final Playlist prompt = context.getFailureAnnouncement();
final String track = prompt.next();
try {
this.player.addListener(this.playerListener);
playAnnouncement(track, 0L);
} catch (TooManyListenersException e) {
log.error("Too many player listeners", e);
context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code());
fire(PlayCollectEvent.FAIL, context);
}
}
@Override
public void onPlayingFailure(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("On PLAYING FAILURE state");
}
final Playlist prompt = context.getFailureAnnouncement();
final String track = prompt.next();
if (track.isEmpty()) {
// No more announcements to play
fire(PlayCollectEvent.END_PROMPT, context);
} else {
playAnnouncement(track, 10 * 100);
}
}
@Override
public void exitPlayingFailure(PlayCollectState from, PlayCollectState to, PlayCollectEvent event,
PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Exited PLAYING FAILURE state");
}
this.player.removeListener(this.playerListener);
this.player.deactivate();
}
@Override
public void enterFailed(PlayCollectState from, PlayCollectState to, PlayCollectEvent event, PlayCollectContext context) {
if (log.isTraceEnabled()) {
log.trace("Entered FAILED state");
}
final OperationFailed operationFailed = new OperationFailed(PlayCollect.SYMBOL, context.getReturnCode());
this.mgcpEventSubject.notify(this.mgcpEventSubject, operationFailed);
}
/**
* Timer that defines interval the system will wait for user's input. May interrupt Collect process.
*
* @author Henrique Rosa (henrique.rosa@telestax.com)
*
*/
private final class DetectorTimer implements Runnable {
private final long timestamp;
private final PlayCollectContext context;
public DetectorTimer(PlayCollectContext context) {
this.timestamp = System.currentTimeMillis();
this.context = context;
}
@Override
public void run() {
if (context.getLastCollectedDigitOn() <= this.timestamp) {
if (PlayCollectState.PLAY_COLLECT.equals(getCurrentState())) {
if (log.isDebugEnabled()) {
log.debug("Timing out collect operation! " + context.getLastCollectedDigitOn() + " <= " + this.timestamp);
}
fire(PlayCollectEvent.TIMEOUT, context);
}
} else {
if (log.isTraceEnabled()) {
log.trace("Aborting timeout operation because a tone has been received in the meantime.");
}
}
}
}
}