org.motechproject.mobile.omp.manager.intellivr.IntellIVRBean.java Source code

Java tutorial

Introduction

Here is the source code for org.motechproject.mobile.omp.manager.intellivr.IntellIVRBean.java

Source

/**
 * MOTECH PLATFORM OPENSOURCE LICENSE AGREEMENT
 *
 * Copyright (c) 2010-11 The Trustees of Columbia University in the City of
 * New York and Grameen Foundation USA.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. Neither the name of Grameen Foundation USA, Columbia University, or
 * their respective contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY GRAMEEN FOUNDATION USA, COLUMBIA UNIVERSITY
 * AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL GRAMEEN FOUNDATION
 * USA, COLUMBIA UNIVERSITY OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.motechproject.mobile.omp.manager.intellivr;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.datatype.XMLGregorianCalendar;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.motechproject.mobile.core.dao.MessageRequestDAO;
import org.motechproject.mobile.core.manager.CoreManager;
import org.motechproject.mobile.core.model.GatewayRequest;
import org.motechproject.mobile.core.model.GatewayResponse;
import org.motechproject.mobile.core.model.Language;
import org.motechproject.mobile.core.model.MStatus;
import org.motechproject.mobile.core.model.MessageRequest;
import org.motechproject.mobile.core.model.MessageType;
import org.motechproject.mobile.core.model.NotificationType;
import org.motechproject.mobile.omp.manager.GatewayManager;
import org.motechproject.mobile.omp.manager.GatewayMessageHandler;
import org.motechproject.mobile.omp.manager.utils.MessageStatusStore;
import org.motechproject.ws.server.RegistrarService;
import org.motechproject.ws.server.ValidationException;
import org.springframework.core.io.Resource;
import org.springframework.transaction.annotation.Transactional;

/**
 * Central class for handling the interaction with the Intell IVR system.  Provides several functions.
 *  
 * {@link GatewayManager} to handle the bundling and sending of {@link MessageRequest} to users.
 * {@link GetIVRConfigRequest} to handle requests from the IVR system for content for inbound callers
 * {@link IVRCallRequester} to handle placing requests for calls with the ivr system.  
 * {@link IVRCallSessionProcessor} to handle the initial lifecycle of an {@link IVRCallSession} up to the point the call is requested.
 * {@link ReportHandler} to handle reports from the ivr system about completed calls.    
 * {@link IVRCallStatsProvider} to provider data to be used for operational reporting.
 * @author fcbrooks
 *
 */
public class IntellIVRBean implements GatewayManager, GetIVRConfigRequestHandler, IVRCallRequester,
        IVRCallSessionProcessor, IVRCallStatsProvider, ReportHandler {

    private GatewayMessageHandler messageHandler;
    protected String reportURL;
    private String apiID;
    private String method;
    private String defaultLanguage;
    private String defaultTree;
    private String defaultReminder;
    private IntellIVRServer ivrServer;
    private MessageStatusStore statusStore;
    protected Map<Long, IVRNotificationMapping> ivrNotificationMap;
    protected Map<String, Long> ivrReminderIds;
    private long bundlingDelay;
    private int retryDelay;
    private int maxAttempts;
    private int maxDays;
    private int availableDays;
    private int callCompletedThreshold;
    private int preReminderDelay;
    private boolean accelerateRetries;
    private String noPendingMessagesRecordingName;
    private String welcomeMessageRecordingName;
    private Resource mappingResource;
    private CoreManager coreManager;
    private RegistrarService registrarService;
    private IVRDAO ivrDao;
    private IVRCallRequester ivrCallRequester;

    private Log log = LogFactory.getLog(IntellIVRBean.class);
    private Log reportLog = LogFactory.getLog(IntellIVRBean.class.getName() + ".reportlog");
    private Log callLog = LogFactory.getLog(IntellIVRBean.class.getName() + ".calllog");

    @Transactional
    public void init() {

        ivrNotificationMap = new HashMap<Long, IVRNotificationMapping>();
        ivrReminderIds = new HashMap<String, Long>();

        try {

            File f = mappingResource.getFile();

            log.debug("Looking for Notification to IVR entity mappings in " + f.getName());

            BufferedReader br = new BufferedReader(new FileReader(f));

            Pattern p = Pattern.compile("([0-9]+)=([IiRr]{1}),(.+)");
            Matcher m;

            String line = "";

            while ((line = br.readLine()) != null) {

                m = p.matcher(line);

                if (m.matches()) {

                    long mapID = Long.parseLong(m.group(1));
                    String ivrType = m.group(2).toUpperCase();
                    String ivrEntity = m.group(3);

                    log.debug("Found IVR entity mapping: " + mapID + " => " + ivrType + "," + ivrEntity);

                    IVRNotificationMapping i = new IVRNotificationMapping();
                    i.setId(mapID);
                    i.setType(ivrType);
                    i.setIvrEntityName(ivrEntity);
                    ivrNotificationMap.put(mapID, i);

                    ivrReminderIds.put(ivrEntity, mapID);

                }

            }

        } catch (IOException e) {
            log.error("IOException creating IVR to Notification Map - default tree and message will be used");
        }

        if (accelerateRetries) {
            log.warn("Using accelerated retries.  Configured retry intervals will be ignored.");
            retryDelay = 1;
        }

    }

    public void cleanUp() {
    }

    public String getMessageStatus(GatewayResponse response) {
        log.debug(
                "Returning " + statusStore.getStatus(response.getGatewayMessageId()) + " for " + response.getId());
        return statusStore.getStatus(response.getGatewayMessageId());
    }

    public MStatus mapMessageStatus(GatewayResponse response) {
        log.debug("Returning " + messageHandler.lookupStatus(response.getResponseText()) + " for "
                + response.getId());
        return messageHandler.lookupStatus(response.getResponseText());
    }

    /**
     * Method for the core mobile server to request a message be delivered to a user of the IVR system.
     * 
     * Messages will take at least as long as the {@link #getBundlingDelay()} to be sent after a call to this method.  
     * 
     * When a request is received for a user at a particular phone number, the bean will wait up to the bundling
     * delay for other messages for that user at that phone number before triggering a call from the IVR system.  This 
     * is to compensate for the lack of message bundling in the underlying system.
     * 
     */
    @Transactional
    public Set<GatewayResponse> sendMessage(GatewayRequest gatewayRequest) {
        Set<GatewayResponse> responses = null;
        try {
            log.debug("Received GatewayRequest:" + gatewayRequest);

            String recipientID = gatewayRequest.getMessageRequest().getRecipientId();

            String phone = gatewayRequest.getRecipientsNumber();

            String status = StatusType.OK.value();
            if (recipientID == null || phone == null
                    || gatewayRequest.getMessageRequest().getMessageType() == MessageType.TEXT) {
                status = StatusType.ERROR.value();
            } else {

                String phoneType = gatewayRequest.getMessageRequest().getPhoneNumberType();

                if (phoneType.equalsIgnoreCase("PERSONAL") || phoneType.equalsIgnoreCase("HOUSEHOLD")) {

                    Language language = gatewayRequest.getMessageRequest().getLanguage();

                    Integer[] openState = { IVRCallSession.OPEN };
                    List<IVRCallSession> existingSessions = ivrDao.loadIVRCallSessions(recipientID, phone,
                            language.getName(), openState, 0, 0, IVRCallSession.OUTBOUND);

                    if (existingSessions.size() == 0) {
                        Date now = new Date();
                        Date bundlingExpiration = addToDate(now, GregorianCalendar.MILLISECOND,
                                (int) bundlingDelay);
                        IVRCallSession session = new IVRCallSession(recipientID, phone, language.getName(),
                                IVRCallSession.OUTBOUND, 0, 0, IVRCallSession.OPEN, now, bundlingExpiration);
                        session.getMessageRequests().add(gatewayRequest.getMessageRequest());
                        ivrDao.saveIVRCallSession(session);
                        log.debug("Created session " + session);
                    } else {
                        existingSessions.get(0).getMessageRequests().add(gatewayRequest.getMessageRequest());
                        log.debug("Using session " + existingSessions.get(0));
                    }

                } else {
                    log.debug("GatewayRequest " + gatewayRequest.getId() + " has phone type "
                            + gatewayRequest.getMessageRequest().getPhoneNumberType()
                            + ".  Call will not be made and message will remain pending.");
                }

            }

            responses = messageHandler.parseMessageResponse(gatewayRequest, status);

            for (GatewayResponse response : responses)
                statusStore.updateStatus(response.getGatewayMessageId(), response.getResponseText());

        } catch (Exception e) {
            log.error("Error scheduling intellIVR call", e);
        }
        return responses;
    }

    /**
     * Queries database for the {@link IVRCallSession} in OPEN state with nextAttempt before 
     * the current time.  Changes them to SEND_WAIT state.
     */
    @Transactional
    public void processOpenSessions() {

        log.debug("Start processing OPEN IVRCallSessions");

        Integer[] states = { IVRCallSession.OPEN };

        List<IVRCallSession> sessions = ivrDao.loadIVRCallSessionsByStateNextAttemptBeforeDate(states, new Date());

        for (IVRCallSession session : sessions) {
            log.debug("Changing to SEND_WAIT state session " + session);
            session.setState(IVRCallSession.SEND_WAIT);
        }

        log.debug("End processing OPEN IVRCallSessions");

    }

    /**
     * Queries database for {@link IVRCallSession} in SEND_WAIT state with nextAttempt 
     * before the current time.  Passed request to the {@link IVRCallRequester} interface
     * implementation.
     */
    @Transactional
    public void processWaitingSessions() {

        log.debug("Start processing SEND_WAIT IVRCallSessions");

        Integer[] states = { IVRCallSession.SEND_WAIT };

        List<IVRCallSession> sessions = ivrDao.loadIVRCallSessionsByStateNextAttemptBeforeDate(states, new Date());

        for (IVRCallSession session : sessions)
            ivrCallRequester.requestCall(session, UUID.randomUUID().toString());

        log.debug("End processing SEND_WAIT IVRCallSessions");

    }

    /**
     * Places request with {@link IntellIVRServer} to place the call.  If the server returns
     * a success code the {@link IVRCallSession} is placed into a REPORT_WAIT to await the 
     * report of the completed call.  If the server returns an error the {@link IVRCallSession}
     * is set to a CLOSED state
     */
    @Transactional
    public void requestCall(IVRCallSession session, String externalId) {

        log.debug("Received request to place call for session id " + session.getId() + " using external id "
                + externalId);

        try {

            log.debug("Requesting call for session: " + session);

            //format the request
            RequestType request = createIVRRequest(session, externalId);

            //request the call
            ResponseType response = ivrServer.requestCall(request);

            //parse the response
            String status = response.getStatus() == StatusType.OK ? StatusType.OK.value()
                    : response.getErrorCode().value();

            //update the local status store
            for (MessageRequest messageRequest : session.getMessageRequests())
                statusStore.updateStatus(messageRequest.getId().toString(), status);

            //if response was OK wait for report, if not close the session and take no further action
            if (response.getStatus() == StatusType.OK) {
                session.setState(IVRCallSession.REPORT_WAIT);
                session.setAttempts(session.getAttempts() + 1);
                IVRCall call = new IVRCall(new Date(), null, null, 0, externalId, IVRCallStatus.REQUESTED,
                        "Call request accepted", session);
                session.getCalls().add(call);
            } else {
                session.setState(IVRCallSession.CLOSED);
                session.setAttempts(session.getAttempts() + 1);
                IVRCall call = new IVRCall(new Date(), null, null, 0, externalId, IVRCallStatus.APIERROR,
                        response.getErrorCode().name() + "(" + response.getErrorCode().value() + ")", session);
                session.getCalls().add(call);
            }

            /*
             * format and write log message
             */
            StringBuilder requestIds = new StringBuilder();
            StringBuilder notificationIDs = new StringBuilder();
            boolean firstRequest = true;

            for (MessageRequest mRequest : session.getMessageRequests()) {
                if (firstRequest)
                    firstRequest = false;
                else {
                    requestIds.append("|");
                    notificationIDs.append("|");
                }
                requestIds.append(mRequest.getId());
                notificationIDs.append(mRequest.getNotificationType().getId().toString());
            }

            StringBuilder reminders = new StringBuilder();
            boolean firstReminder = true;

            for (Object o : request.getVxml().getPrompt().getAudioOrBreak()) {
                if (o instanceof AudioType) {
                    if (firstReminder)
                        firstReminder = false;
                    else
                        reminders.append("|");
                    reminders.append(((AudioType) o).getSrc());
                }
            }

            callLog.info("OUT," + session.getPhone() + "," + session.getUserId() + "," + status + "," + externalId
                    + "," + requestIds.toString() + "," + notificationIDs.toString() + "," + request.getTree() + ","
                    + reminders.toString());

        } catch (Exception e) {
            log.error("Error sending intellIVR call", e);
        }

    }

    /**
     * Method to request a {@link RequestType} instance based on the content of the 
     * {@link IVRCallSession} instance and the provided externalId
     * @param session
     * @param externalId
     * @return
     */
    public RequestType createIVRRequest(IVRCallSession session, String externalId) {

        Set<MessageRequest> messageRequests = session.getMessageRequests();

        log.debug("Creating IVR Request for " + messageRequests);

        RequestType ivrRequest = new RequestType();

        /*
         * These first three values are fixed
         */
        ivrRequest.setApiId(apiID);
        ivrRequest.setMethod(method);
        ivrRequest.setReportUrl(reportURL);

        /*
         * recipient's phone number
         */
        ivrRequest.setCallee(session.getPhone());

        /*
         * Set language
         */
        String language = session.getLanguage();
        ivrRequest.setLanguage(language != null ? language : defaultLanguage);

        /*
         * Private id
         */
        ivrRequest.setPrivate(externalId);

        /*
         * Create the content
         */
        MessageRequest infoRequest = null;
        List<String> reminderMessages = new ArrayList<String>();
        for (MessageRequest messageRequest : messageRequests) {

            long notificationId = messageRequest.getNotificationType().getId();

            if (!ivrNotificationMap.containsKey(notificationId)) {
                log.debug("No IVR Notification mapping found for " + notificationId);
            } else {

                IVRNotificationMapping mapping = ivrNotificationMap.get(notificationId);

                if (mapping.getType().equalsIgnoreCase(IVRNotificationMapping.INFORMATIONAL)) {
                    if (infoRequest == null)
                        infoRequest = messageRequest;
                    else {
                        GregorianCalendar currDateFrom = new GregorianCalendar();
                        currDateFrom.setTime(infoRequest.getDateFrom());
                        GregorianCalendar possibleDateFrom = new GregorianCalendar();
                        possibleDateFrom.setTime(messageRequest.getDateFrom());
                        if (currDateFrom.before(possibleDateFrom))
                            infoRequest = messageRequest;
                    }

                } else {
                    reminderMessages.add(mapping.getIvrEntityName());
                }

            }

        }

        if (infoRequest != null) {
            IVRNotificationMapping infoMapping = ivrNotificationMap.get(infoRequest.getNotificationType().getId());
            ivrRequest.setTree(infoMapping.getIvrEntityName());
        }

        RequestType.Vxml vxml = new RequestType.Vxml();
        vxml.setPrompt(new RequestType.Vxml.Prompt());

        /*
         * add a break element if the preReminderDelay is > 0
         */
        if (preReminderDelay > 0) {
            BreakType breakType = new BreakType();
            breakType.setTime(Integer.toString(preReminderDelay) + "s");
            vxml.getPrompt().getAudioOrBreak().add(breakType);
        }

        /*
         * add a welcome message if an outbound call and a recording name has been configured
         */
        if (session.getCallDirection().equalsIgnoreCase(IVRCallSession.OUTBOUND)
                && welcomeMessageRecordingName != null && welcomeMessageRecordingName.trim().length() > 0) {
            AudioType welcome = new AudioType();
            welcome.setSrc(welcomeMessageRecordingName);
            vxml.getPrompt().getAudioOrBreak().add(welcome);
        }

        /*
         * add the reminder messages
         */
        for (String fileName : reminderMessages) {
            AudioType audio = new AudioType();
            audio.setSrc(fileName);
            vxml.getPrompt().getAudioOrBreak().add(audio);
        }
        ivrRequest.setVxml(vxml);

        return ivrRequest;

    }

    @Transactional
    public ResponseType handleRequest(GetIVRConfigRequest request) {
        return handleRequest(request, UUID.randomUUID().toString());
    }

    /**
     * Handles the request from the IVR system for content for incoming callers
     */
    @SuppressWarnings("unchecked")
    @Transactional
    public ResponseType handleRequest(GetIVRConfigRequest request, String externalId) {

        ResponseType r = new ResponseType();
        String userId = request.getUserid();

        log.info("Received ivr config request for id " + userId);

        try {

            String[] enrollments = registrarService.getPatientEnrollments(Integer.parseInt(userId));

            if (enrollments == null || enrollments.length == 0) {
                callLog.info("IN,," + request.getUserid() + ",UNENROLLED");
                r.setErrorCode(ErrorCodeType.MOTECH_INVALID_USER_ID);
                r.setErrorString("Unenrolled user");
                r.setStatus(StatusType.ERROR);
            } else {

                MessageRequestDAO<MessageRequest> mrDAO = coreManager.createMessageRequestDAO();

                Date endDate = new Date();
                Date startDate = addToDate(endDate, GregorianCalendar.DAY_OF_MONTH, (int) availableDays * -1);

                List<MessageRequest> pendingMessageRequests = mrDAO
                        .getMsgRequestByRecipientDateFromBetweenDates(request.getUserid(), startDate, endDate);

                IVRCallSession session = new IVRCallSession(userId, null, null, IVRCallSession.INBOUND, 0, 0,
                        IVRCallSession.REPORT_WAIT, new Date(), null);

                IVRCall call = new IVRCall(new Date(), null, null, 0, externalId, IVRCallStatus.REQUESTED,
                        "Client called IVR system", session);
                session.getCalls().add(call);

                if (pendingMessageRequests.size() == 0) {
                    log.debug("No pending messages found for " + request.getUserid());
                    callLog.info("IN,," + request.getUserid() + ",NO_PENDING");
                    r.setStatus(StatusType.OK);
                    RequestType.Vxml vxml = new RequestType.Vxml();
                    vxml.setPrompt(new RequestType.Vxml.Prompt());
                    AudioType a = new AudioType();
                    a.setSrc(noPendingMessagesRecordingName.trim());
                    vxml.getPrompt().getAudioOrBreak().add(a);
                    r.setVxml(vxml);
                    r.setReportUrl(reportURL);
                    r.setPrivate(externalId);
                } else {

                    log.debug("Found pending messages for " + request.getUserid() + ": " + pendingMessageRequests);

                    for (MessageRequest messageRequest : pendingMessageRequests) {

                        session.getMessageRequests().add(messageRequest);

                        statusStore.updateStatus(messageRequest.getId().toString(), StatusType.OK.value());

                    }

                    /*
                     * ResponseType fields are a subset of the RequestType fields
                     * Can create a RequestType based on this criteria and use
                     * only the fields that are needed to create the ResponseType
                     */
                    RequestType requestType = createIVRRequest(session, externalId);

                    r.setPrivate(requestType.getPrivate());
                    r.setReportUrl(requestType.getReportUrl());
                    r.setStatus(StatusType.OK);
                    r.setTree(requestType.getTree());
                    r.setVxml(requestType.getVxml());

                    StringBuilder notificationIDs = new StringBuilder();
                    boolean firstRequest = true;

                    for (MessageRequest messageRequest : session.getMessageRequests()) {
                        if (firstRequest)
                            firstRequest = false;
                        else
                            notificationIDs.append("|");
                        notificationIDs.append(messageRequest.getNotificationType().getId().toString());
                    }

                    StringBuilder reminders = new StringBuilder();
                    boolean firstReminder = true;

                    for (Object o : r.getVxml().getPrompt().getAudioOrBreak()) {
                        if (o instanceof AudioType) {
                            if (firstReminder)
                                firstReminder = false;
                            else
                                reminders.append("|");
                            reminders.append(((AudioType) o).getSrc());
                        }
                    }

                    callLog.info("IN,," + request.getUserid() + "," + StatusType.OK.value() + ","
                            + call.getExternalId() + ",," + notificationIDs.toString() + "," + r.getTree() + ","
                            + reminders.toString());

                }

                ivrDao.saveIVRCallSession(session);

            }

        } catch (NumberFormatException e) {
            log.error("Invalid user id: id must be numeric");
            callLog.info("IN,," + request.getUserid() + "," + ErrorCodeType.MOTECH_INVALID_USER_ID.name());
            r.setErrorCode(ErrorCodeType.MOTECH_INVALID_USER_ID);
            r.setErrorString("Invalid user id: id must be numeric");
            r.setStatus(StatusType.ERROR);
        } catch (ValidationException e) {
            log.error("Invalid user id: no such id '" + userId + "' on server");
            callLog.info("IN,," + request.getUserid() + "," + ErrorCodeType.MOTECH_INVALID_USER_ID.name());
            r.setErrorCode(ErrorCodeType.MOTECH_INVALID_USER_ID);
            r.setErrorString("Invalid user id: no such id '" + userId + "' on server");
            r.setStatus(StatusType.ERROR);
        } finally {

        }

        return r;
    }

    /**
     * Handles reports detailing the results of calls placed or received by the IVR system
     * {@link IVRCallSession} and {@link IVRCall} instances are updated based on the data
     * received. 
     */
    @Transactional
    public ResponseType handleReport(ReportType report) {
        log.info("Received call report: " + report.toString());

        List<String> messages = formatReportLogMessages(report);
        for (String message : messages)
            reportLog.info(message);

        //private field contains the external id specified in the original request
        String externalId = report.getPrivate();

        if (externalId == null)
            log.error("Unable to identify call in report: " + externalId);
        else {

            //look up the call
            IVRCall call = ivrDao.loadIVRCallByExternalId(externalId);

            if (call == null) {
                log.error("Unable to find IVRCall with external id " + externalId);
            } else {

                //update the call's fields
                call.setConnected(toDate(report.getConnectTime()));
                call.setDisconnected(toDate(report.getDisconnectTime()));
                call.setDuration(report.getDuration());
                call.setStatus(toIvrCallStatus(report.getStatus()));
                call.setStatusReason("");

                //add menu entries to the database
                for (IvrEntryType entry : report.getINTELLIVREntry()) {
                    IVRMenu menu = new IVRMenu(entry.getMenu() == null ? null : entry.getMenu(),
                            entry.getEntrytime() == null ? null : toDate(entry.getEntrytime()), entry.getDuration(),
                            entry.getKeypress() == null ? null : entry.getKeypress(),
                            entry.getFile() == null ? null : entry.getFile());
                    call.getMenus().add(menu);
                }

                String status = report.getStatus().value();
                IVRCallSession session = call.getSession();

                if (session == null) {
                    log.error("Unable to find IVRCallSession for IVRCall with external id " + externalId);
                } else {

                    /*
                     * Retry if necessary
                     */
                    if (report.getStatus() == ReportStatusType.COMPLETED && callExceedsThreshold(session, report)) {
                        //Success.  Call was complete and over the required call time threshold.
                        session.setState(IVRCallSession.CLOSED);
                    } else {

                        if (session.getCallDirection().equalsIgnoreCase(IVRCallSession.INBOUND)) {
                            //can't retry or update status for failed inbound calls.  
                            status = null;
                            session.setState(IVRCallSession.CLOSED);
                        } else {

                            //set to SEND_WAIT to be retried
                            session.setState(IVRCallSession.SEND_WAIT);

                            //if the status is completed it must have been below the call time threshold
                            if (report.getStatus() == ReportStatusType.COMPLETED) {
                                call.setStatus(IVRCallStatus.BELOWTHRESHOLD);
                                status = "BELOWTHRESHOLD";
                            }

                            //check the number of attempts today
                            if (session.getAttempts() < this.maxAttempts) {
                                //try again after the retry delay
                                session.setNextAttempt(
                                        addToDate(session.getNextAttempt(), GregorianCalendar.MINUTE, retryDelay));
                            } else {

                                //all attempts for this day have been exhausted
                                session.setDays(session.getDays() + 1);
                                session.setAttempts(0);

                                //check the number of days attempted
                                if (session.getDays() < this.maxDays) {

                                    //have not exhausted days.  try again tomorrow
                                    //acceletateRetries is a debug option to speed up next day retries to the same day
                                    if (accelerateRetries)
                                        session.setNextAttempt(addToDate(new Date(), GregorianCalendar.MINUTE, 1));
                                    else
                                        session.setNextAttempt(
                                                addToDate(session.getCreated(), GregorianCalendar.DAY_OF_MONTH, 1));

                                } else {
                                    //all attempts for all days have been exhausted
                                    session.setState(IVRCallSession.CLOSED);
                                    status = "MAXATTEMPTS";
                                }

                            }

                        }

                    }

                    /*
                     * Update message status
                     */
                    if (status != null) {
                        Collection<MessageRequest> requests = session.getMessageRequests();
                        for (MessageRequest messageRequest : requests) {

                            log.debug("Updating Message Request " + messageRequest.getId().toString() + " to "
                                    + status);
                            statusStore.updateStatus(messageRequest.getId().toString(), status);

                        }

                    }

                }

            }

        }

        ResponseType r = new ResponseType();
        r.setStatus(StatusType.OK);
        return r;
    }

    /**
     * Contains logic for a call being over the threshold for a completed calls.
     * Basically if the first non-reminder message is greater than the callCompleteThreshold value
     * it is considered complete.  
     * @param session
     * @param report
     * @return
     */
    protected boolean callExceedsThreshold(IVRCallSession session, ReportType report) {

        int effectiveCallTime = 0;
        int reminderCount = 0;
        boolean shouldHaveInformationalMessage = false;

        //get the message request and determine if we should expect to find a informational message in the report
        //if there is no informational message then the first non-reminder logic does not apply
        for (MessageRequest request : session.getMessageRequests()) {
            long notificationId = request.getNotificationType().getId();
            if (ivrNotificationMap.containsKey(notificationId))
                if (ivrNotificationMap.get(notificationId).getType()
                        .equalsIgnoreCase(IVRNotificationMapping.INFORMATIONAL))
                    shouldHaveInformationalMessage = true;
        }

        //to hold a reference to the first non-reminder
        IvrEntryType firstInfoEntry = null;

        //get the list of recording that were heard
        List<IvrEntryType> entries = report.getINTELLIVREntry();

        //iterate.  increment counter for reminder messages.  identify the first non-reminder 
        for (IvrEntryType entry : entries)
            if (ivrReminderIds.containsKey(entry.getMenu()) || entry.getMenu().equalsIgnoreCase("break")
                    || entry.getMenu().equalsIgnoreCase(welcomeMessageRecordingName))
                reminderCount++;
            else if (firstInfoEntry == null
                    && (session.getCallDirection().equalsIgnoreCase(IVRCallSession.OUTBOUND) || reminderCount > 0))
                firstInfoEntry = entry;

        if (firstInfoEntry == null)//did not find first non-reminder
            if (shouldHaveInformationalMessage)//there should have been one
                effectiveCallTime = 0;//insure it is below threshold
            else if (reminderCount > 0)//no info message expected but no reminders play either
                effectiveCallTime = callCompletedThreshold;//say it was over threshold
            else
                effectiveCallTime = report.getDuration();//fall back to the duration of the entire call
        else
            effectiveCallTime = firstInfoEntry.getDuration();//duration of the first non-reminder

        return effectiveCallTime >= callCompletedThreshold;
    }

    @Transactional
    public int getCountIVRCallSessions() {
        return ivrDao.countIVRCallSesssions();
    }

    @Transactional
    public int getCountIVRSessionsInLastMinutes(int minutes) {
        minutes = minutes < 0 ? 0 : minutes;
        Date end = new Date();
        Date start = addToDate(end, GregorianCalendar.MINUTE, (int) minutes * (-1));
        return ivrDao.countIVRCallSessionsCreatedBetweenDates(start, end);
    }

    @Transactional
    public int getCountIVRCallSessionsInLastHours(int hours) {
        hours = hours < 0 ? 0 : hours;
        Date end = new Date();
        Date start = addToDate(end, GregorianCalendar.HOUR_OF_DAY, (int) hours * (-1));
        return ivrDao.countIVRCallSessionsCreatedBetweenDates(start, end);
    }

    @Transactional
    public int getCountIVRCallSessionsInLastDays(int days) {
        days = days < 0 ? 0 : days;
        GregorianCalendar end = new GregorianCalendar();
        GregorianCalendar lastMidnight = new GregorianCalendar(end.get(GregorianCalendar.YEAR),
                end.get(GregorianCalendar.MONTH), end.get(GregorianCalendar.DAY_OF_MONTH));
        Date start = addToDate(lastMidnight.getTime(), GregorianCalendar.DAY_OF_MONTH, (int) (days - 1) * (-1));
        return ivrDao.countIVRCallSessionsCreatedBetweenDates(start, end.getTime());
    }

    @Transactional
    public int getCountIVRCalls() {
        return ivrDao.countIVRCalls();
    }

    @Transactional
    public int getCountIVRCallsInLastMinutes(int minutes) {
        minutes = minutes < 0 ? 0 : minutes;
        Date end = new Date();
        Date start = addToDate(end, GregorianCalendar.MINUTE, (int) minutes * (-1));
        return ivrDao.countIVRCallsCreatedBetweenDates(start, end);
    }

    @Transactional
    public int getCountIVRCallsInLastHours(int hours) {
        hours = hours < 0 ? 0 : hours;
        Date end = new Date();
        Date start = addToDate(end, GregorianCalendar.HOUR_OF_DAY, (int) hours * (-1));
        return ivrDao.countIVRCallsCreatedBetweenDates(start, end);
    }

    @Transactional
    public int getCountIVRCallsInLastDays(int days) {
        days = days < 0 ? 0 : days;
        GregorianCalendar end = new GregorianCalendar();
        GregorianCalendar lastMidnight = new GregorianCalendar(end.get(GregorianCalendar.YEAR),
                end.get(GregorianCalendar.MONTH), end.get(GregorianCalendar.DAY_OF_MONTH));
        Date start = addToDate(lastMidnight.getTime(), GregorianCalendar.DAY_OF_MONTH, (int) (days - 1) * (-1));
        return ivrDao.countIVRCallsCreatedBetweenDates(start, end.getTime());
    }

    @Transactional
    public int getCountIVRCallsWithStatus(IVRCallStatus status) {
        return ivrDao.countIVRCallsWithStatus(status);
    }

    @Transactional
    public int getCountIVRCallsInLastMinutesWithStatus(int minutes, IVRCallStatus status) {
        minutes = minutes < 0 ? 0 : minutes;
        Date end = new Date();
        Date start = addToDate(end, GregorianCalendar.MINUTE, (int) minutes * (-1));
        return ivrDao.countIVRCallsCreatedBetweenDatesWithStatus(start, end, status);
    }

    @Transactional
    public int getCountIVRCallsInLastHoursWithStatus(int hours, IVRCallStatus status) {
        hours = hours < 0 ? 0 : hours;
        Date end = new Date();
        Date start = addToDate(end, GregorianCalendar.HOUR_OF_DAY, (int) hours * (-1));
        return ivrDao.countIVRCallsCreatedBetweenDatesWithStatus(start, end, status);
    }

    @Transactional
    public int getCountIVRCallsInLastDaysWithStatus(int days, IVRCallStatus status) {
        days = days < 0 ? 0 : days;
        GregorianCalendar end = new GregorianCalendar();
        GregorianCalendar lastMidnight = new GregorianCalendar(end.get(GregorianCalendar.YEAR),
                end.get(GregorianCalendar.MONTH), end.get(GregorianCalendar.DAY_OF_MONTH));
        Date start = addToDate(lastMidnight.getTime(), GregorianCalendar.DAY_OF_MONTH, (int) (days - 1) * (-1));
        return ivrDao.countIVRCallsCreatedBetweenDatesWithStatus(start, end.getTime(), status);
    }

    @Transactional
    public List<IVRRecordingStat> getIVRRecordingStats() {
        return ivrDao.getIVRRecordingStats();
    }

    @Transactional
    public List<IVRCallStatusStat> getIVRCallStatusStats() {
        return ivrDao.getIVRCallStatusStats();
    }

    @Transactional
    public List<IVRCallStatusStat> getIVRCallStatusStatsFromLastMinutes(int minutes) {
        minutes = minutes < 0 ? 0 : minutes;
        Date end = new Date();
        Date start = addToDate(end, GregorianCalendar.MINUTE, (int) minutes * (-1));
        return ivrDao.getIVRCallStatusStatsBetweenDates(start, end);
    }

    @Transactional
    public List<IVRCallStatusStat> getIVRCallStatusStatsFromLastHours(int hours) {
        hours = hours < 0 ? 0 : hours;
        Date end = new Date();
        Date start = addToDate(end, GregorianCalendar.HOUR_OF_DAY, (int) hours * (-1));
        return ivrDao.getIVRCallStatusStatsBetweenDates(start, end);
    }

    @Transactional
    public List<IVRCallStatusStat> getIVRCallStatusStatsFromLastDays(int days) {
        days = days < 0 ? 0 : days;
        GregorianCalendar end = new GregorianCalendar();
        GregorianCalendar lastMidnight = new GregorianCalendar(end.get(GregorianCalendar.YEAR),
                end.get(GregorianCalendar.MONTH), end.get(GregorianCalendar.DAY_OF_MONTH));
        Date start = addToDate(lastMidnight.getTime(), GregorianCalendar.DAY_OF_MONTH, (int) (days - 1) * (-1));
        return ivrDao.getIVRCallStatusStatsBetweenDates(start, end.getTime());
    }

    @Transactional
    public List<IVRCallSession> getIVRCallSessions() {
        return ivrDao.loadIVRCallSessions();
    }

    @Transactional
    public List<IVRCallSession> getIVRCallSessionsInLastMinutes(int minutes) {
        minutes = minutes < 0 ? 0 : minutes;
        Date end = new Date();
        Date start = addToDate(end, GregorianCalendar.MINUTE, (int) minutes * (-1));
        return ivrDao.loadIVRCallSessionsCreatedBetweenDates(start, end);
    }

    @Transactional
    public List<IVRCallSession> getIVRCallSessionsInLastHours(int hours) {
        hours = hours < 0 ? 0 : hours;
        Date end = new Date();
        Date start = addToDate(end, GregorianCalendar.HOUR_OF_DAY, (int) hours * (-1));
        return ivrDao.loadIVRCallSessionsCreatedBetweenDates(start, end);
    }

    @Transactional
    public List<IVRCallSession> getIVRCallSessionsInLastDays(int days) {
        days = days < 0 ? 0 : days;
        GregorianCalendar end = new GregorianCalendar();
        GregorianCalendar lastMidnight = new GregorianCalendar(end.get(GregorianCalendar.YEAR),
                end.get(GregorianCalendar.MONTH), end.get(GregorianCalendar.DAY_OF_MONTH));
        Date start = addToDate(lastMidnight.getTime(), GregorianCalendar.DAY_OF_MONTH, (int) (days - 1) * (-1));
        return ivrDao.loadIVRCallSessionsCreatedBetweenDates(start, end.getTime());
    }

    @Transactional
    public List<IVRCallSession> getIVRCallSessionsForUser(String user) {
        return ivrDao.loadIVRCallSessionsByUser(user);
    }

    @Transactional
    public List<IVRCallSession> getIVRCallSessionsForPhone(String phone) {
        return ivrDao.loadIVRCallSessionsByPhone(phone);
    }

    public void setMessageHandler(GatewayMessageHandler messageHandler) {
        this.messageHandler = messageHandler;
    }

    public GatewayMessageHandler getMessageHandler() {
        return messageHandler;
    }

    /**
     * The URL to which the IVR system will be requested to post call reports
     * @return reportURL
     */
    public String getReportURL() {
        return reportURL;
    }

    /**
     * Set URL to which the IVR system will be requested to post call reports
     * @param reportURL
     */
    public void setReportURL(String reportURL) {
        this.reportURL = reportURL;
    }

    /**
     * The API key for the IntellIVR server
     * @return
     */
    public String getApiID() {
        return apiID;
    }

    /**
     * Set the API key for the IntellIVR server
     * @param apiID
     */
    public void setApiID(String apiID) {
        this.apiID = apiID;
    }

    /**
     * The implementation of the {@link IntellIVRServer} interface being used
     * @return
     */
    public IntellIVRServer getIvrServer() {
        return ivrServer;
    }

    /**
     * Set the implementation of the {@link IntellIVRServer} interface being used
     * @param ivrServer
     */
    public void setIvrServer(IntellIVRServer ivrServer) {
        this.ivrServer = ivrServer;
    }

    /**
     * The method being used for IntellIVR server
     * @return
     */
    public String getMethod() {
        return method;
    }

    /**
     * Set the method being used for IntellIVR server.  Generally 'ivroriginate'.
     * @param method
     */
    public void setMethod(String method) {
        this.method = method;
    }

    /**
     * The default language to use if not otherwise specified.
     * @return
     */
    public String getDefaultLanguage() {
        return defaultLanguage;
    }

    /**
     * Set the default language to use if not otherwise specified.
     * @param defaultLanguage
     */
    public void setDefaultLanguage(String defaultLanguage) {
        this.defaultLanguage = defaultLanguage;
    }

    /**
     * The default tree.  Not used.
     * @return
     */
    public String getDefaultTree() {
        return defaultTree;
    }

    /**
     * Set the default tree.  Not used.
     * @param defaultTree
     */
    public void setDefaultTree(String defaultTree) {
        this.defaultTree = defaultTree;
    }

    /**
     * The default reminder.  Not used.
     * @return
     */
    public String getDefaultReminder() {
        return defaultReminder;
    }

    /**
     * Set the default reminder.  Not used.
     * @param defaultReminder
     */
    public void setDefaultReminder(String defaultReminder) {
        this.defaultReminder = defaultReminder;
    }

    /**
     * The {@link MessageStatusStore} used to store request statuses.
     * @return
     */
    public MessageStatusStore getStatusStore() {
        return statusStore;
    }

    /**
     * Set the {@link MessageStatusStore} used to store request statuses.
     * @param statusStore
     */
    public void setStatusStore(MessageStatusStore statusStore) {
        this.statusStore = statusStore;
    }

    /**
     * Delay to bundle additional messages for a user before sending
     * See {@link #sendMessage(GatewayRequest, MotechContext)} for more details. 
     * @return
     */
    public long getBundlingDelay() {
        return bundlingDelay;
    }

    /**
     * Set delay in milliseconds to bundle additional messages for a user before sending
     * See {@link #sendMessage(GatewayRequest, MotechContext)} for more details. 
     * @param bundlingDelay
     */
    public void setBundlingDelay(long bundlingDelay) {
        this.bundlingDelay = bundlingDelay;
    }

    /**
     * Delay in minutes to wait before retrying after a failed message delivery.
     * @return
     */
    public int getRetryDelay() {
        return retryDelay;
    }

    /**
     * Set delay in minutes to wait before retrying after a failed message delivery.
     * @param retryDelay
     */
    public void setRetryDelay(int retryDelay) {
        this.retryDelay = retryDelay;
    }

    /**
     * Max attempts to try a deliver a message each day.
     * @return
     */
    public int getMaxAttempts() {
        return maxAttempts;
    }

    /**
     * Set max attempts to try a deliver a message each day.
     * @param maxAttempts
     */
    public void setMaxAttempts(int maxAttempts) {
        this.maxAttempts = maxAttempts;
    }

    /**
     * Max days to retry message delivery
     * @return
     */
    public int getMaxDays() {
        return maxDays;
    }

    /**
     * Set max days to retry message delivery
     * @param maxDays
     */
    public void setMaxDays(int maxDays) {
        this.maxDays = maxDays;
    }

    /**
     * Number of days a message should remain available to replayed
     * @return
     */
    public int getAvailableDays() {
        return availableDays;
    }

    /**
     * Set number of days a message should remain available to replayed
     * @param availableDays
     */
    public void setAvailableDays(int availableDays) {
        this.availableDays = availableDays;
    }

    /**
     * Seconds of the first primary informational message that the user
     * must have listened to to consider the message delivered.
     * @return
     */
    public int getCallCompletedThreshold() {
        return callCompletedThreshold;
    }

    /**
     * Set seconds of the first primary informational message that the user
     * must have listened to to consider the message delivered.
     * @param callCompletedThreshold
     */
    public void setCallCompletedThreshold(int callCompletedThreshold) {
        this.callCompletedThreshold = callCompletedThreshold;
    }

    /**
     * Seconds of silence that is pre-pended to beginning of each call. 
     * @return
     */
    public int getPreReminderDelay() {
        return preReminderDelay;
    }

    /**
     * Set seconds of silence that is pre-pended to beginning of each call.
     * @param preReminderDelay
     */
    public void setPreReminderDelay(int preReminderDelay) {
        this.preReminderDelay = preReminderDelay;
    }

    /**
     * If true, the next day retries are tried immediately.  For testing.
     * @return
     */
    public boolean isAccelerateRetries() {
        return accelerateRetries;
    }

    /**
     * Enables/disables accelerated retries
     * @param accelerateRetries
     */
    public void setAccelerateRetries(boolean accelerateRetries) {
        this.accelerateRetries = accelerateRetries;
    }

    /**
     * Name of recording to play in the event a user has no pending messages
     * @return
     */
    public String getNoPendingMessagesRecordingName() {
        return noPendingMessagesRecordingName;
    }

    /**
     * Set the name of recording to play in the event a user has no pending messages
     * @param noPendingMessagesRecordingName
     */
    public void setNoPendingMessagesRecordingName(String noPendingMessagesRecordingName) {
        this.noPendingMessagesRecordingName = noPendingMessagesRecordingName;
    }

    /**
     * Name of a recording of a welcome message to be played before all other messages
     * @return
     */
    public String getWelcomeMessageRecordingName() {
        return welcomeMessageRecordingName;
    }

    /**
     * Set the name of a recording of a welcome message to be played before all other messages
     * @param welcomeMessageRecordingName
     */
    public void setWelcomeMessageRecordingName(String welcomeMessageRecordingName) {
        this.welcomeMessageRecordingName = welcomeMessageRecordingName;
    }

    /**
     * Name of file resource that contains the mapping between {@link NotificationType} ids
     * and file names on the IVR server.  Each line should match the following expression:
     * 
     * [0-9]+=[IiRr]{1},.+
     * 
     * @return
     */
    public Resource getMappingResource() {
        return mappingResource;
    }

    /**
     * Set the file resource for mapping.  See {@link #getMappingResource()}.
     * @param mappingsFile
     */
    public void setMappingResource(Resource mappingsFile) {
        this.mappingResource = mappingsFile;
    }

    /**
     * For access to core motech mobile services
     * @return
     */
    public CoreManager getCoreManager() {
        return coreManager;
    }

    /**
     * Set the {@link CoreManager}.
     * @param coreManager
     */
    public void setCoreManager(CoreManager coreManager) {
        this.coreManager = coreManager;
    }

    /**
     * Interface the to the Motech Server.  
     * @return
     */
    public RegistrarService getRegistrarService() {
        return registrarService;
    }

    /**
     * Set the {@link RegistrarService}.
     * @param registrarService
     */
    public void setRegistrarService(RegistrarService registrarService) {
        this.registrarService = registrarService;
    }

    public IVRDAO getIvrDao() {
        return ivrDao;
    }

    public void setIvrDao(IVRDAO ivrDao) {
        this.ivrDao = ivrDao;
    }

    public IVRCallRequester getIvrCallRequester() {
        return ivrCallRequester;
    }

    public void setIvrCallRequester(IVRCallRequester ivrCallRequester) {
        this.ivrCallRequester = ivrCallRequester;
    }

    private Date toDate(XMLGregorianCalendar time) {
        return time == null ? null : time.toGregorianCalendar().getTime();
    }

    private Date addToDate(Date start, int field, int amount) {
        GregorianCalendar cal = new GregorianCalendar();
        cal.setTime(start);
        cal.add(field, amount);
        return cal.getTime();
    }

    private IVRCallStatus toIvrCallStatus(ReportStatusType status) {
        if (status == null)
            return null;
        if (status.name() == null)
            return null;
        return IVRCallStatus.valueOf(status.name());
    }

    private List<String> formatReportLogMessages(ReportType report) {

        List<String> result = new ArrayList<String>();

        StringBuilder common = new StringBuilder();
        common.append(report.getCallee());
        common.append("," + report.getDuration());
        common.append("," + report.getINTELLIVREntryCount());
        common.append("," + report.getPrivate());
        common.append("," + report.getConnectTime());
        common.append("," + report.getDisconnectTime());
        common.append("," + report.getStatus().value());

        result.add(common.toString());

        for (IvrEntryType entry : report.getINTELLIVREntry()) {

            StringBuilder message = new StringBuilder();
            message.append(common.toString());
            message.append("," + entry.getFile());
            message.append("," + entry.getKeypress());
            message.append("," + entry.getMenu());
            message.append("," + entry.getDuration());
            message.append("," + entry.getEntrytime());

            result.add(message.toString());

        }

        return result;
    }

}