/*
* Copyright (C) 2017 by Fonoster Inc (http://fonoster.com)
* http://astivetoolkit.org
*
* This file is part of Astive Toolkit(ATK)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.astivetoolkit.menu;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import org.apache.log4j.Logger;
import org.astivetoolkit.agi.AgiException;
import org.astivetoolkit.agi.AgiResponse;
import org.astivetoolkit.agi.CommandProcessor;
import org.astivetoolkit.menu.action.Action;
import org.astivetoolkit.menu.event.ActionEvent;
import org.astivetoolkit.menu.event.AuthenticationEvent;
import org.astivetoolkit.menu.event.DigitsEvent;
import org.astivetoolkit.menu.event.FailEvent;
import org.astivetoolkit.menu.event.InterDigitsTimeoutEvent;
import org.astivetoolkit.menu.event.KeyEvent;
import org.astivetoolkit.menu.event.MaxFailureEvent;
import org.astivetoolkit.menu.event.MaxTimeoutEvent;
import org.astivetoolkit.menu.event.PositionChangeEvent;
/**
* The engine of Menu API. It eliminate the necessity of the loops commonly
* needed by AGI, when creating user iteration/navigation.
*
* @since 1.0
*/
public class MenuNavigator {
private static final Logger LOG = Logger.getLogger(MenuNavigator.class);
private AgiResponse agiResponse;
private Menu currentMenu;
private boolean answered;
private boolean autoAnswer;
/**
* Creates a new instance of MenuNavigator.
*
* @param agiResponse allows user iteration with telephony system.
*/
public MenuNavigator(AgiResponse agiResponse) {
this.agiResponse = agiResponse;
autoAnswer = true;
answered = false;
}
private void authenticate(MenuItem item) {
if (item.getMustAuthenticate()) {
if (!item.getAuthenticator().isAuthenticated()) {
AuthenticationEvent evt = new AuthenticationEvent(item, item.getAuthenticator());
item.getAuthenticator().signIn();
if (item.getAuthenticator().isAuthenticated()) {
item.fireAuthenticationEvent_onSuccess(null);
} else {
item.fireAuthenticationEvent_onFailure(evt);
}
}
}
}
private boolean checkMaxFailure(Menu menu, String opt) {
if (menu.getFailuresCount() >= menu.getMaxFailures()) {
MaxFailureEvent event = new MaxFailureEvent(menu, opt, menu.getMaxFailures());
menu.fireMaxFailureEvent_maxFailurePerform(event);
// do break the flow
return true;
}
return false;
}
private boolean checkMaxTimeout(Menu menu, String opt) {
if (menu.getTimeoutCount() >= menu.getMaxTimeouts()) {
MaxTimeoutEvent event = new MaxTimeoutEvent(menu, opt, menu.getMaxTimeouts());
menu.fireMaxTimeoutEvent_maxTimeoutPerform(event);
// do break the flow
return true;
}
return false;
}
private ArrayList<String> getChildsKeys(ArrayList<MenuItem> menuChilds) {
ArrayList<String> result = new ArrayList<String>();
for (MenuItem child : menuChilds) {
final String digits = ((MenuItem) child).getDigits();
result.add(digits);
}
return result;
}
private Menu getCurrentMenu() {
return currentMenu;
}
private String getData(String file, int milliSecondsWatting, int maxDigits,
AgiResponse agiResponse, MenuItem item, char c)
throws AgiException {
Menu menu;
String result = new String();
if (item.getParent() != null) {
menu = ((Menu) item.getParent());
} else {
menu = getCurrentMenu();
}
// Has not press any key
if (c == 0) {
// WARNING: Not sure about using all keys...
c = agiResponse.streamFile(file, "0123456789*#");
if (c == 0 /*
* && milliSecondsWatting == 0
*/) {
try {
Thread.sleep(milliSecondsWatting);
} catch (InterruptedException ex) {
LOG.warn(ex.getMessage());
}
return "(timeout)";
}
} else {
result = "" + c;
c = agiResponse.waitForDigit(menu.getInterDigitsTimeout());
}
KeyEvent evt;
if (c != 0) {
evt = new KeyEvent(item, Digit.getDigit(c));
item.fireKeyEvent_keyTyped(evt);
result += ("" + c);
}
while (true) {
if ((result.length() == maxDigits) || (c == '#')) {
return result;
}
c = agiResponse.waitForDigit(menu.getInterDigitsTimeout());
if (c != 0) {
result += ("" + c);
evt = new KeyEvent(item, Digit.getDigit(c));
item.fireKeyEvent_keyTyped(evt);
} else {
InterDigitsTimeoutEvent event =
new InterDigitsTimeoutEvent(item, result, menu.getInterDigitsTimeout());
menu.fireInterDigitsTimeoutListener_timeoutPerform(event);
break;
}
}
return result;
}
private MenuItem getMenuItem(Menu menu, String digits) {
for (MenuItem item : menu.getChilds()) {
if (item.getDigits().equals(digits)) {
return item;
}
}
return null;
}
// XXX: Im not sure about this criteria
private ArrayList<String> getSortedChildsKeys(ArrayList<MenuItem> menuChilds) {
ArrayList result = new ArrayList();
Comparator comparator =
new Comparator() {
@Override
public int compare(Object o1, Object o2) {
int c1 = ((MenuItem) o1).getPriority();
int c2 = ((MenuItem) o2).getPriority();
return (new Integer(c1)).compareTo(new Integer(c2));
}
;
};
Object[] opts = new Object[menuChilds.size()];
int cnt = 0;
for (MenuItem child : menuChilds) {
opts[cnt++] = child;
}
// Sorting the items
Arrays.sort(opts, comparator);
for (int i = 0; i < opts.length; i++) {
String digits = ((MenuItem) opts[i]).getDigits();
result.add(digits);
}
return result;
}
private Boolean isAutoAnswer() {
return autoAnswer;
}
/**
* Start the menu execution.
*
* @param menu and object containing all menu and menu items.
*/
public void run(Menu menu) throws AgiException {
String digits = null;
setCurrentMenu(menu);
// If channel is closed by customer the player must be auto-halt.
if (agiResponse.getChannelStatus().getCode() == -1) {
return;
}
// WARNING: To be reviewed
if ((answered == false) && (isAutoAnswer() == true)) {
agiResponse.answer();
answered = true;
}
ArrayList<String> childsKeys;
// Sort elements
if (!menu.isSortChildsByDigits()) {
childsKeys = getChildsKeys(menu.getChilds());
} else {
childsKeys = getSortedChildsKeys(menu.getChilds());
}
if (LOG.isDebugEnabled()) {
LOG.debug("Total menu options: " + menu.getChilds().size());
}
if (((menu.isGreetingsPlayed() == false) || menu.isPlayGreetingsAllways())
&& (menu.getGreetingsFile() != null) && !menu.getGreetingsFile().isEmpty()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Playing menu intro: " + menu.getGreetingsFile());
}
char c = 0;
digits = getData(menu.getGreetingsFile(), 0, menu.getMaxDigits(), agiResponse, menu, c);
menu.setGreetingsPlayed(true);
}
if ((digits == null) || digits.equals("(timeout)")) {
List<VoiceComposition> voiceCompositions = menu.getVoiceCompositions();
Iterator<VoiceComposition> vcIterator = voiceCompositions.iterator();
char c = 0;
while (vcIterator.hasNext()) {
VoiceComposition cVc = vcIterator.next();
Iterator<Object> commands = cVc.getCommands().iterator();
while (commands.hasNext()) {
Object o = commands.next();
String cmd = CommandProcessor.buildCommand(o);
c = agiResponse.sendAgiCommand(cmd).getResultCodeAsChar();
if (c != 0) {
KeyEvent evt = new KeyEvent(menu, Digit.getDigit(c));
menu.fireKeyEvent_keyTyped(evt);
digits = getData(menu.getFile(), 0, menu.getMaxDigits(), agiResponse, menu, c);
break;
}
}
if (c != 0) {
break;
}
}
}
if ((digits == null) || digits.equals("(timeout)")) {
if (LOG.isDebugEnabled()) {
LOG.debug("Playing menu options");
}
int pos = 0;
for (String opts : childsKeys) {
MenuItem option = getMenuItem(menu, opts);
int millisecondsWatting = menu.getInterDigitsTimeout();
if (((menu.getChilds().size() - 1) >= 0)
&& menu.getChilds().get(menu.getChilds().size() - 1).equals(option)) {
millisecondsWatting = menu.getLastDigitsTimeout();
}
if ((digits == null) || digits.equals("(timeout)")) {
// Waiting time in between the item file and digits item file.
int msw;
msw = millisecondsWatting;
if ((option.getFile() != null) && !option.getFile().isEmpty()) {
char c = 0;
digits = getData(option.getFile(), msw, menu.getMaxDigits(), agiResponse, menu, c);
}
}
MenuItem oldOption = option;
if (pos > 0) {
oldOption = getMenuItem(menu, childsKeys.get(pos - 1));
}
PositionChangeEvent evt = new PositionChangeEvent(oldOption, option, pos - 1);
menu.firePositionChangeEvent_positionChange(evt);
pos++;
if (LOG.isDebugEnabled()) {
LOG.debug("pos = " + pos);
}
}
}
if ((digits != null) && !digits.equals("(timeout)")) {
// Do break menu
if (LOG.isDebugEnabled()) {
LOG.debug("Enter digits is: " + digits);
}
// WARNING: Should this event be only at Menu level?
DigitsEvent evt = new DigitsEvent((Object) menu, digits);
menu.fireDigitsEvent_digitsEnter(evt);
}
// Selected none option
if ((digits == null) || digits.equals("(timeout)")) {
// XXX: Not only timeout, but also for # sign.
if (digits == null) {
FailEvent evt = new FailEvent(menu, digits, menu.getFailuresCount());
menu.fireFailureListener_failurePerform(evt);
} else if (digits.equals("(timeout)")) {
// WARNNING:
//menu.fireTimeoutListener_timeoutPerform(null);
}
// Try again
if (LOG.isDebugEnabled()) {
LOG.debug("Unregistered option");
}
menu.incrementFailuresCount();
if (!checkMaxFailure(menu, digits) && !checkMaxTimeout(menu, digits)) {
run(menu);
return;
}
if ((menu.getExitFile() != null) && !menu.getExitFile().isEmpty()) {
agiResponse.streamFile(menu.getExitFile());
}
return;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Getting Menu/MenuItem for digits: " + digits);
}
MenuItem selectedOption = getMenuItem(menu, digits);
if (selectedOption != null) {
menu.resetFailuresCount();
menu.resetTimeoutCount();
Action action = selectedOption.getAction();
if (action != null) {
authenticate(selectedOption);
selectedOption.getAction().doAction();
}
ActionEvent evt = new ActionEvent(selectedOption, digits);
selectedOption.fireActionEvent_processAction(evt);
if (selectedOption instanceof Menu) {
run((Menu) selectedOption);
} else {
run(menu);
}
return;
} else {
// Invalid option
if ((menu.getInvalidDigitsFile() != null) && !menu.getInvalidDigitsFile().isEmpty()) {
agiResponse.streamFile(menu.getInvalidDigitsFile());
}
menu.incrementFailuresCount();
if (!checkMaxFailure(menu, digits) && !checkMaxTimeout(menu, digits)) {
run(menu);
return;
}
if ((menu.getExitFile() != null) && !menu.getExitFile().isEmpty()) {
agiResponse.streamFile(menu.getExitFile());
}
return;
}
}
private void setCurrentMenu(Menu currentMenu) {
this.currentMenu = currentMenu;
}
}