com.kana.connect.server.receiver.SMSKeywordDispatchReplyHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.kana.connect.server.receiver.SMSKeywordDispatchReplyHandler.java

Source

/*
 * SMS Keyword Dispatch Reply Handler
 * 
 * Copyright (c) 2016 Brick Street Software, Inc.
 * 
 * This code is provided under the Apache License.
 * http://www.apache.org/licenses/
 */

/* 
 NOTE: In Connect 10r4, custom reply handlers must be in the com.kana.connect.server.receiver package
 because certain classes, like ReceiverMessage are not exported outside this package.
 */

package com.kana.connect.server.receiver;

import com.kana.connect.common.db.CustomerRow;
import com.kana.connect.common.db.CustomerTable;
import com.kana.connect.common.db.ReplyHandlerRow;
import com.kana.connect.common.db.TransactionManager;
import com.kana.connect.common.lib.Debug;
import com.kana.connect.server.receiver.ReplyHandler;
import com.kana.connect.server.receiver.SmppReceiverMessage;
import com.kana.connect.server.receiver.SmppReplyHandler;
import com.kana.connect.server.smpp.Address;
import com.kana.connect.server.smpp.message.SMPPRequest;

import net.brickst.connect.custom.content.XslContent;
import net.brickst.connect.custom.webservices.JMSEndpoint;
import net.brickst.connect.custom.webservices.LogEndpoint;
import net.brickst.connect.custom.webservices.RESTEndpoint;
import net.brickst.connect.custom.webservices.WebEndpoint;

import java.io.File;
import java.io.FileInputStream;
import java.text.MessageFormat;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringEscapeUtils;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;

public class SMSKeywordDispatchReplyHandler extends SmppReplyHandler {
    // The mail processor may create a new handler object for each incoming message.
    // On the other hand, the configuration for this handler is heavy and expensive to set up.
    // As a result, we store the configuration in static variables so that it can be
    // shared across multiple instances of the reply handler.
    //
    // If the config file changes, the mail processor needs to be restarted.
    // Our assumption is that this will happen rarely if ever.

    //
    // STATIC CONFIG VARS
    //
    private static Object configLock = new Object();
    private static Pattern[] matchPatterns;
    private static WebEndpoint[] webEndpoints;
    private static ConcurrentHashMap<String, Integer> numberMappings;
    private static XslContent contentTemplate;
    private static AtomicBoolean didInit = new AtomicBoolean();
    private static int returnValueForMatch;

    public SMSKeywordDispatchReplyHandler() {
        // The constructor can be used for handler-specific initialiation.
        // However, it should be careful to not throw exceptions.
    }

    public static Pattern[] getMatchPatterns() {
        return matchPatterns;
    }

    public static WebEndpoint[] getWebEndpoints() {
        return webEndpoints;
    }

    public static Integer getNumberMapping(String number) {
        return numberMappings.get(number);
    }

    //
    // logging methods
    //

    protected static void log(Debug logger, String pattern, Object... arguments) {
        if (logger.isEnabled()) {
            String msg = pattern;
            if (arguments.length > 0) {
                msg = MessageFormat.format(pattern, arguments);
            }
            logger.println(msg);
        }
    }

    protected static void log(String pattern, Object... arguments) {
        log(Debug.SR, pattern, arguments);
    }

    protected static void logException(Debug logger, Throwable th, String pattern, Object... arguments) {
        if (logger.isEnabled()) {
            String msg = pattern;
            if (arguments.length > 0) {
                msg = MessageFormat.format(pattern, arguments);
            }
            logger.printException(th, msg);
        }
    }

    protected static void logException(Throwable th, String pattern, Object... arguments) {
        logException(Debug.SR, th, pattern, arguments);
    }

    protected static int getIntProperty(Properties props, String propName, int defaultVal) {
        String val = props.getProperty(propName);
        if (val == null) {
            return defaultVal;
        }

        int ival;
        try {
            ival = Integer.parseInt(val);
            return ival;
        } catch (Exception x) {
            logException(x, "Error parsing " + propName + "=" + val);
            return defaultVal;
        }
    }

    public static boolean loadConfig(File configProps) {
        synchronized (configLock) {

            if (matchPatterns != null || webEndpoints != null) {
                // assume config has already been loaded
                return false;
            }

            //
            // initialize configuration properties
            //
            Properties props = new Properties();

            // String handlerName = handlerRow.getName();
            FileInputStream propsInput = null;
            try {
                propsInput = new FileInputStream(configProps);
            } catch (Exception x) {
                throw new IllegalArgumentException("Unable to find " + configProps, x);
            }
            try {
                props.load(propsInput);
            } catch (Exception x) {
                throw new RuntimeException(x);
            }

            //
            // get regexes
            //
            int regexCount = getIntProperty(props, "regex_count", 0);
            matchPatterns = new Pattern[regexCount];
            for (int i = 0; i < regexCount; i++) {
                String regex = props.getProperty("regex_" + i + ".pattern");
                int flags = getIntProperty(props, "regex_" + i + ".flags", 0);

                Pattern p = null;
                try {
                    if (flags > 0) {
                        p = Pattern.compile(regex, flags);
                    } else {
                        p = Pattern.compile(regex);
                    }
                } catch (Throwable th) {
                    throw new RuntimeException("Error compiling regex " + i, th);
                }
                log(Debug.SRV, "SMSKeywordDispatch: got pattern " + regex);
                matchPatterns[i] = p;
            }

            //
            // init endpoint retry dir
            //
            String endpointRetryDir = props.getProperty("endpoint_retrydir");
            if (endpointRetryDir == null) {
                endpointRetryDir = "smsretryqueue";
            }
            File retryTop = new File(endpointRetryDir);
            WebEndpoint.setTopLevelRetryDir(retryTop);
            try {
                log(Debug.SRV, "SMSKeywordDispatch: retry dir: {0}", retryTop.getCanonicalPath());
            } catch (Exception x) {
                ; //ignore
            }

            //
            // load endpoints
            //
            int endpointCount = getIntProperty(props, "endpoint_count", 0);
            webEndpoints = new WebEndpoint[endpointCount];
            for (int i = 0; i < endpointCount; i++) {
                WebEndpoint wep = null;
                String epPrefix = "endpoint_" + i + ".";
                String epName = epPrefix + "type";
                String epType = props.getProperty(epName);
                if ("JMS".equalsIgnoreCase(epType)) {
                    wep = new JMSEndpoint();
                } else if ("REST".equalsIgnoreCase(epType)) {
                    wep = new RESTEndpoint();
                } else if ("LOG".equalsIgnoreCase(epType)) {
                    wep = new LogEndpoint();
                } else if ("CUSTOM".equalsIgnoreCase(epType)) {
                    String endpointClassName = props.getProperty(epPrefix + "className");
                    if (endpointClassName == null) {
                        throw new IllegalArgumentException("CUSTOM endpoint must have className property");
                    }
                    try {
                        wep = (WebEndpoint) Class.forName(endpointClassName).newInstance();
                    } catch (Exception x) {
                        throw new IllegalArgumentException("bad className:" + endpointClassName, x);
                    }
                } else {
                    throw new IllegalArgumentException("Invalid endpoint type: " + epType);
                }

                // got endpoint, now do init 
                wep.initFromProperties(props, epPrefix);

                // init retry dir
                File retry = new File(WebEndpoint.getTopLevelRetryDir(), "endpoint_" + i);
                wep.setRetryDir(retry);

                // retry interval
                int retryIntervalSec = getIntProperty(props, epPrefix + "retryIntervalSeconds", 300); // default 5 mins
                wep.setRetryIntervalMS(retryIntervalSec * 1000);

                // start retry task
                wep.startRetryTask();

                log(Debug.SRV, "SMSKeywordDispatch: endpoint " + i + ": " + wep);

                webEndpoints[i] = wep;
            }

            //
            // mappings from destinations to endpoints
            //
            int mappingCount = getIntProperty(props, "mapping_count", 0);
            numberMappings = new ConcurrentHashMap<String, Integer>();
            for (int i = 0; i < mappingCount; i++) {
                String prefix = "mapping_" + i + ".";
                String number = props.getProperty(prefix + "number");
                int epNumber = getIntProperty(props, prefix + "endpoint", -1);
                if (epNumber < 0) {
                    throw new IllegalArgumentException("Invalid endpoint in mapping " + i);
                }
                numberMappings.put(number, Integer.valueOf(epNumber));
                log(Debug.SRV, "SMSKeywordDispatch: Map endpoint " + number + " -> " + epNumber);
            }

            //
            // content
            //
            String contentType = props.getProperty("content.type");
            if ("XSL".equalsIgnoreCase(contentType)) {
                contentTemplate = new XslContent();
                contentTemplate.initFromPropsFile(props, "content.");
            } else {
                throw new IllegalArgumentException("Invalid Content Type: " + contentType);
            }

            //
            // return value
            //
            String returnVal = props.getProperty("return_value");
            if (returnVal == null || returnVal.trim().length() == 0) {
                returnValueForMatch = HANDLED;
            } else {
                try {
                    int propVal = Integer.parseInt(returnVal);
                    // only accept valid values for return_val
                    if (propVal >= 1 && propVal <= 3) {
                        returnValueForMatch = propVal;
                    } else {
                        throw new IllegalArgumentException("Invalid return_value; must be 1-3");
                    }
                } catch (NumberFormatException x) {
                    throw new IllegalArgumentException("Cannot parse return_value: " + returnVal);
                }
            }

            // did load, need to init
            return true;
        }
    }

    public void initNetworkResources() {
        // should we init or let another thread do it???
        boolean doInit = didInit.compareAndSet(false, true);
        if (!doInit) {
            return;
        }
        // we win, init here

        int endpointCount = webEndpoints.length;
        for (int i = 0; i < endpointCount; i++) {
            WebEndpoint wep = webEndpoints[i];
            wep.initNetworkResources();
        }

        contentTemplate.initNetworkResources();
    }

    public void init(ReplyHandlerRow handlerRow) {
        // call superclass method
        super.init(handlerRow);

        String configFileName = handlerRow.getName() + ".properties";
        File configFile = new File(configFileName);

        // call static config loader
        loadConfig(configFile);
        initNetworkResources();
    }

    private static String fillRight(String str, int len, char cc) {
        StringBuffer buf = new StringBuffer(str);

        if (buf.length() >= len) {
            buf.append(cc);
            return buf.toString();
        }

        for (int i = buf.length(); i < len; i++) {
            buf.append(cc);
        }
        return buf.toString();
    }

    /**
     * Helper method that transforms an SMPP Message into an XML document
     */
    public static String smppToXml(SmppReceiverMessage msg) {
        SMPPRequest smppReq = msg.getSmppRequest();

        StringBuffer buf = new StringBuffer();
        buf.append("<smpp>\n");

        // smpp header 
        // numeric values do not need to be escaped
        buf.append("<header>");
        buf.append("<command_id>").append(smppReq.getCommandId()).append("</command_id>");
        buf.append("<sequence_number>").append(smppReq.getSequenceNum()).append("</sequence_number>");
        buf.append("</header>\n");

        // smpp source
        Address smppSource = smppReq.getSource();
        buf.append("<source>");
        buf.append("<ton>").append(smppSource.getTON()).append("</ton>");
        buf.append("<npi>").append(smppSource.getNPI()).append("</npi>");

        String srcAddr = smppSource.getAddress();
        srcAddr = StringEscapeUtils.escapeXml11(srcAddr);
        buf.append("<address>").append(srcAddr).append("</address>");
        buf.append("</source>\n");

        // smpp dest
        Address smppDest = smppReq.getDestination();
        buf.append("<destination>");
        buf.append("<ton>").append(smppDest.getTON()).append("</ton>");
        buf.append("<npi>").append(smppDest.getNPI()).append("</npi>");

        String dstAddr = smppDest.getAddress();
        dstAddr = StringEscapeUtils.escapeXml11(dstAddr);
        buf.append("<address>").append(dstAddr).append("</address>");
        buf.append("</destination>\n");

        // message id
        String msgId = smppReq.getMessageId();
        if (msgId == null) {
            msgId = "";
        }
        msgId = StringEscapeUtils.escapeXml11(msgId);
        buf.append("<messageid>").append(msgId).append("</messageid>\n");

        // smpp message
        String msgText = smppReq.getMessageText();
        msgText = StringEscapeUtils.escapeXml11(msgText);
        buf.append("<message>");
        buf.append(msgText);
        buf.append("</message>\n");

        // base64 message
        msgText = smppReq.getMessageText();
        msgText = Base64.encodeBase64String(msgText.getBytes());
        buf.append("<messageBase64>");
        buf.append(msgText);
        buf.append("</messageBase64>\n");

        //
        // timestamps in different formats
        //

        long now = System.currentTimeMillis();

        // ISO8601 http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html
        DateTimeFormatter isoFormat = ISODateTimeFormat.dateTime();
        isoFormat.withZoneUTC();
        String headerTimestamp = isoFormat.print(now);
        headerTimestamp = StringEscapeUtils.escapeXml11(headerTimestamp);

        // custom format http://joda-time.sourceforge.net/apidocs/org/joda/time/format/DateTimeFormat.html
        DateTimeFormatter otherFormat = DateTimeFormat.forPattern("ddMMYYYYHHmmz");
        String payloadTimestamp = otherFormat.print(now);
        payloadTimestamp = StringEscapeUtils.escapeXml11(payloadTimestamp);

        buf.append("<headertimestamp>").append(headerTimestamp).append("</headertimestamp>");
        buf.append("<payloadtimestamp>").append(payloadTimestamp).append("</payloadtimestamp>");

        buf.append("</smpp>\n");
        return buf.toString();
    }

    /**
     * This method will be called for each incoming SMS message. It can examine
     * the incoming message and take action based on the message content.
     */
    public int handle(SmppReceiverMessage msg, TransactionManager tm) {
        // print log message to MailProcessor log under the "SMPP Receiver" Verbose Diagnostic
        log(Debug.SRV, "SMSKeywordDispatch: attempting to handle: {0}", msg);

        // extract info about the incoming message from the SmppReceiverMessage
        // object
        String smsSource = msg.getFieldToMatch("from");
        String smsDest = msg.getFieldToMatch("to");
        String smsMessage = msg.getFieldToMatch("body");
        log(Debug.SRVV, "SMSKeywordDispatch: src: {0} dst:{1} msg:{2}", smsSource, smsDest, smsMessage);

        //
        // MATCH INCOMING MESSAGE AGAINST REGEXES
        //

        // matchMatcher / matchIndex hold the result of the match
        Matcher matchMatcher = null;
        int matchIndex = -1;

        int regexCount = matchPatterns.length;
        for (int i = 0; i < regexCount; i++) {
            Pattern pattern = matchPatterns[i];
            Matcher matcher = pattern.matcher(smsMessage);
            if (matcher.matches()) {
                log(Debug.SRVV, "SMSKeywordDispatch: match {0}", pattern.pattern());
                matchIndex = i;
                matchMatcher = matcher;
                break;
            }
        }

        // quit if no match
        if (matchIndex < 0) {
            log(Debug.SRVV, "SMSKeywordDispatch: no match");
            return NOT_HANDLED; // NOT_HANDLED is inherited from parent class
        }

        //
        // IF REGEXES MATCH, FIND ASSOCIATED MAPPING FOR DEST NUMBERN
        //
        Integer targetEndpoint = numberMappings.get(smsDest);
        if (targetEndpoint == null) {
            // no match for destination number; log and give up???
            log(Debug.SR, "SMSKeywordDispatch: WARNING: no endpoint for dest: {0}", smsDest);
            return NOT_HANDLED;
        }
        WebEndpoint wep = webEndpoints[targetEndpoint.intValue()];
        if (wep == null) {
            // no match for endpoint number; log and give up???
            log(Debug.SR, "SMSKeywordDispatch: WARNING: no endpoint for target: {0}", targetEndpoint);
            return NOT_HANDLED;
        }

        //
        // CREATE CONTENT
        //
        // attempt to find customer from sms number
        // TODO incorporate this into the XML document
        CustomerRow cust = CustomerTable.getInstance().getCustomerBySMSNumber(smsSource);

        if (Debug.SR.isEnabled()) {
            if (cust != null) {
                Debug.SR.println("SMSLoggingReplyHandler: message from " + smsSource + " (customer:" + cust.getID()
                        + ") to " + smsDest + " body:\"" + smsMessage + "\"");
            } else {
                Debug.SR.println("SMSLoggingReplyHandler: message from " + smsSource + " (customer: unknown) to "
                        + smsDest + " body:\"" + smsMessage + "\"");
            }
        }

        // create xml doc
        String xmlContent = smppToXml(msg);
        log(Debug.SRV, "SMSKeywordDispatch: XML: {0}", xmlContent);

        String xslOutput = null;
        // do xsl transform 
        if (contentTemplate == null) {
            xslOutput = xmlContent;
        } else {
            try {
                xslOutput = contentTemplate.transformDocument(xmlContent);
            } catch (Throwable th) {
                throw new RuntimeException(th);
            }
        }
        log(Debug.SRV, "SMSKeywordDispatch: XSL Output: {0}", xslOutput);

        //
        // DELIVER MESSAGE TO ENDPOINT
        //
        try {
            wep.deliverMessage(xslOutput);
            log(Debug.SR, "SMSKeywordDispatch: delivered to {0}" + wep);
        } catch (Exception x) {
            logException(x, "SMSKeywordDispatch: reshedule delivery to {0}", wep);

            //
            // TODO LOG EXCEPTION
            //
            try {
                wep.scheduleRetry(xslOutput);

                // each handler must set msg result codes
                msg.setHandlerID(getHandlerID());
                msg.setHandleType(getHandleType());
                msg.setHandleCode(returnValueForMatch);
                return returnValueForMatch;
            } catch (Throwable th) {
                // TODO log exception
                logException(x, "SMSKeywordDispatch: RESHEDULE FAILURE to {0}", wep);

                // If we cannot deliver the message and we cannot save to a
                // file,
                // we are seriously hosed and should crash the process.
                throw new RuntimeException(th);
            }
        }

        //
        // If the handler returns HANDLED, then other reply handlers WILL NOT run.
        // If the handler returns HANDLED_CONTINUE, then other reply handler WILL run.
        // Archiving and redirecting WILL occur if the handler returns HANDLED
        // or HANDLED_CONTINUE.
        //
        // If the handler returns NOT_HANDLED, then other reply handlers WILL run. 
        // Archiving and redirecting WILL NOT occur if the handler returns NOT_HANDLED.
        //

        // to return HANDLED or HANDLED_CONTINUE...
        // each handler must set msg result codes
        msg.setHandlerID(getHandlerID());
        msg.setHandleType(getHandleType());
        msg.setHandleCode(returnValueForMatch);
        return returnValueForMatch;
    }

}