Package org.mokai.connector.smpp

Source Code of org.mokai.connector.smpp.SmppConnector$DeliveryReceipt

package org.mokai.connector.smpp;

import ie.omk.smpp.Address;
import ie.omk.smpp.Connection;
import ie.omk.smpp.event.ConnectionObserver;
import ie.omk.smpp.event.ReceiverExceptionEvent;
import ie.omk.smpp.event.ReceiverExitEvent;
import ie.omk.smpp.event.SMPPEvent;
import ie.omk.smpp.message.BindResp;
import ie.omk.smpp.message.DeliverSM;
import ie.omk.smpp.message.EnquireLink;
import ie.omk.smpp.message.SMPPPacket;
import ie.omk.smpp.message.SubmitSM;
import ie.omk.smpp.message.SubmitSMResp;
import ie.omk.smpp.message.tlv.TLVTable;
import ie.omk.smpp.message.tlv.Tag;
import ie.omk.smpp.net.TcpLink;
import ie.omk.smpp.util.ASCIIEncoding;
import ie.omk.smpp.util.AlphabetEncoding;
import ie.omk.smpp.util.DefaultAlphabetEncoding;
import ie.omk.smpp.util.Latin1Encoding;
import ie.omk.smpp.util.UCS2Encoding;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import org.mokai.ConnectorContext;
import org.mokai.ExposableConfiguration;
import org.mokai.Message;
import org.mokai.MessageProducer;
import org.mokai.MonitorStatusBuilder;
import org.mokai.Monitorable;
import org.mokai.Processor;
import org.mokai.Serviceable;
import org.mokai.annotation.Description;
import org.mokai.annotation.Name;
import org.mokai.annotation.Resource;
import org.mokai.connector.smpp.SmppConfiguration.BindType;
import org.mokai.connector.smpp.SmppConfiguration.DlrIdConversion;
import org.mokai.persist.MessageCriteria;
import org.mokai.persist.MessageStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A connector that sends and receives messages to an SMSC using the SMPP protocol.
* This implementation is based on the SMPP API (http://smppapi.sourceforge.net/).
*
* @author German Escobar
*/
@Name("SMPP Connector")
@Description("Sends and receives messages using SMPP protocol")
public class SmppConnector implements Processor, Serviceable, Monitorable,
    ExposableConfiguration<SmppConfiguration> {

  private Logger log = LoggerFactory.getLogger(SmppConnector.class);

  /**
   * The folder in which we will save the file with the sequence number.
   */
  private static final String SEQUENCE_NUMBER_FOLDER = "data/connectors/smpp/";

  /**
   * The extension of the file that will store the sequence number.
   */
  private static final String SEQUENCE_NUMBER_EXT = ".seq";

  /**
   * Holds information about the processor like the assigned id. It is used to add a
   * header on every log message and to create the file that will hold the sequence
   * number.
   */
  @Resource
  private ConnectorContext context;

  /**
   * Used to produce received messages from the SMSC into the gateway.
   */
  @Resource
  private MessageProducer messageProducer;

  /**
   * Used to find messages and update them when a submit response or delivery receipt
   * is received.
   */
  @Resource
  private MessageStore messageStore;

  /**
   * The configuration of the connector
   */
  private SmppConfiguration configuration;

  /**
   * The status of the connector.
   */
  private Status status = MonitorStatusBuilder.unknown();

  /**
   * The smppapi Connection used to connect to the SMSC.
   */
  private Connection connection;

  /**
   * Tells if the processor is started so we keep trying to connect
   * in case of failure.
   */
  private volatile boolean started = false;

  private volatile boolean connecting = false;

  /**
   * Tells if the session is bound.
   */
  private volatile boolean bound;

  /**
   * Stores the submit responses until they are processed by the SubmitSmResponseThread
   * (defined in this same class).
   */
  private List<SubmitSmResp> submitSmResponses = Collections.synchronizedList(new ArrayList<SubmitSmResp>());

  /**
   * Stores the delivery receipts until they are processed by the DeliveryReceiptThread
   * (defined in this same class)
   */
  private List<DeliveryReceipt> deliveryReceipts = Collections.synchronizedList(new ArrayList<DeliveryReceipt>());

  /**
   * Constructor. Creates an instance with the default configuration information.
   *
   * @see SmppConfiguration
   */
  public SmppConnector() {
    this(new SmppConfiguration());
  }

  /**
   * Constructor. Creates an instance with the supplied configuration
   * object.
   * @param configuration the configuration for this instance.
   */
  public SmppConnector(SmppConfiguration configuration) {
    this.configuration = configuration;
  }

  /**
   * Binds to the SMSC with the parameters of the {@link LogicaConfiguration}
   * instance. If the first attempt fails, it will start a new thread to keep
   * trying the connection as long as the connector is started.
   */
  @Override
  public void doStart() throws Exception {
    log.debug(getLogHead() + "starting SmppConnector ... ");

    started = true;

    // try to connect in this same thread (only one attempt)
    // the reason for this is that if the processor service
    // has queued messages and it is started without setting
    // up the connection, some messages will fail until the
    // connection is up
    new ConnectionThread(1, 0).run();

    // if we couldn't connect the first time, start a thread to keep trying
    if (status.equals(Status.FAILED)) {
      new Thread(
        new ConnectionThread(Integer.MAX_VALUE, configuration.getInitialReconnectDelay())
      ).start();
    }

    // sends enquire links periodically to check connection
    new EnquireLinkThread().start();

    // process the submit_sm response
    new SubmitSmResponseThread().start();

    // process the delivery receipts
    new DeliveryReceiptThread().start();
  }

  /**
   * Unbinds from the SMSC.
   */
  @Override
  public void doStop() throws Exception {
    started = false;
    status = MonitorStatusBuilder.unknown();

    if (!bound) {
      log.debug(getLogHead() + " connection not bound");
      return;
    }

    bound = false;
    try {
      connection.unbind();
    } catch (Exception e) {
      log.warn(getLogHead() + "Exception unbinding: " + e.getMessage(), e);
    }
  }

  /**
   * Sends an SMS message to the SMSC. If not connected, an IllegalStateException
   * will be thrown. This method expects the following properties keys:
   *
   * <ul>
   *   <li>"to" - the destination of the SMS message</li>
   *   <li>"from - the short code of the SMS message</li>
   *   <li>"text" - the text of the SMS message</li>
   * </ul>
   */
  @Override
  public void process(Message message) throws Exception {
    int sequenceNumber = getSequenceNumber();
    log.debug(getLogHead() + "processing message with sequence '" + sequenceNumber + "', to '" + message.getProperty("to", String.class)
        + "' and text '" + message.getProperty("text", String.class) + "'");

    if (!status.equals(Status.OK)) {
      throw new IllegalStateException("SMPP client not connected.");
    }

    int msgRefNum = new Random().nextInt(10000); // used if we send more than one segment

    String[] texts = getMessageTexts( message.getProperty("text", String.class) );
    for (int i=0; i < texts.length; i++) {
      log.trace("sending segment " + (i+1) + " of " + texts.length);

      SubmitSM request = buildSubmitSM(message);

      request.setMessage( getEncodedBytes(texts[i]) );
      request.setDataCoding( getDataCoding().getDataCoding() );
      request.setSequenceNum( sequenceNumber );

      if (texts.length > 1) {
        request.setOptionalParameter(Tag.SAR_TOTAL_SEGMENTS, texts.length);
        request.setOptionalParameter(Tag.SAR_SEGMENT_SEQNUM, i + 1);
        request.setOptionalParameter(Tag.SAR_MSG_REF_NUM, msgRefNum);
      }

      if (i == 0) {
        message.setProperty("sequenceNumber", request.getSequenceNum());
      }

      connection.sendRequest(request);

    }
  }

  private SubmitSM buildSubmitSM(Message message) {
    SubmitSM request = new SubmitSM();

    Address sourceAr = new Address();
    if (notEmpty(configuration.getSourceNPI())) {
      sourceAr.setNPI(Byte.valueOf(configuration.getSourceNPI()));
    }
    if (notEmpty(configuration.getSourceTON())) {
      sourceAr.setTON(Byte.valueOf(configuration.getSourceTON()));
    }
    sourceAr.setAddress(message.getProperty("from").toString());
    request.setSource(sourceAr);

    Address destAr = new Address();
    if (notEmpty(configuration.getDestNPI())) {
      destAr.setNPI(Byte.valueOf(configuration.getDestNPI()));
    }
    if (notEmpty(configuration.getDestTON())) {
      destAr.setTON(Byte.valueOf(configuration.getDestTON()));
    }
    destAr.setAddress(message.getProperty("to", String.class));
    request.setDestination(destAr);

    if (configuration.isRequestDeliveryReceipts()) {
      request.setRegistered((byte) 0x01);
    }

    return request;
  }

  private String[] getMessageTexts(String text) {
    if (text == null) {
      return new String[] { "" };
    }

    return text.split("(?<=\\G.{160})");
  }

  private byte[] getEncodedBytes(String text) throws IllegalStateException, UnsupportedEncodingException {
    AlphabetEncoding enc = getDataCoding();
    byte[] encodedBytes = enc.encodeString(text);

    StringBuffer strBytes = new StringBuffer();
    for (int i=0; i < encodedBytes.length; i++) {
      strBytes.append(" : ").append(encodedBytes[i]);
    }
    log.trace("encoded bytes of the message: " + strBytes);

    return encodedBytes;
  }

  private AlphabetEncoding getDataCoding() throws IllegalStateException, UnsupportedEncodingException {
    int dataCoding = configuration.getDataCoding();

    if (dataCoding == 0) {
      return new DefaultAlphabetEncoding();
    } else if (dataCoding == 1) {
      return new ASCIIEncoding();
    } else if (dataCoding == 3) {
      return new Latin1Encoding();
    } else if (dataCoding == 8) {
      return new UCS2Encoding();
    }

    throw new IllegalStateException("Data Coding " + configuration.getDataCoding() + " not recognized");
  }

  /**
   * Helper method that will return a sequence number that is sent with the request to the
   * SMSC. It reads the number from a file, increments it, saves it again in the file and
   * returns it.
   *
   * @return the sequence number to use in the request that is going to be sent to the SMSC.
   * @throws Exception if anything goes wrong.
   */
  private synchronized int getSequenceNumber() throws Exception {
    int sequence = 1;

    // check if the file exists and read the number
    File file = new File(SEQUENCE_NUMBER_FOLDER + context.getId() + SEQUENCE_NUMBER_EXT);
    if (file.exists()) {
      BufferedReader in = null;

      try {
        in = new BufferedReader(new FileReader(file));
        sequence = Integer.parseInt(in.readLine());
      } finally {
        if (in != null) {
          try { in.close(); } catch (Exception e) {}
        }
      }
    }

    // create the directories if they aren't created already
    File fDir = new File(SEQUENCE_NUMBER_FOLDER);
    fDir.mkdirs();

    // increment the number and save it in the file
    PrintWriter out = null;
    try {
      out = new PrintWriter(new FileWriter(SEQUENCE_NUMBER_FOLDER + context.getId() + SEQUENCE_NUMBER_EXT));
      out.println(sequence + 1);
    } finally {
      if (out != null) {
        try { out.close(); } catch (Exception e) {}
      }
    }

    return sequence;
  }

  @Override
  public boolean supports(Message message) {
    String to = message.getProperty("to", String.class);

    if (to == null || "".equals(to)) {
      return false;
    }

    return to.matches("[0-9]*");
  }

  @Override
  public SmppConfiguration getConfiguration() {
    return configuration;
  }

  /**
   * Returns the status of the connector.
   *
   * <ul>
   *   <li>Status.UNKNOWN - if the connector is stopped.</li>
   *   <li>Status.OK - if the connector is bound to the SMSC.</li>
   *   <li>Status.FAILED - if the connection to the SMSC has failed.</li>
   * </ul>
   */
  @Override
  public Status getStatus() {
    return status;
  }

  /**
   * Helper method.
   * @param s the String to validate
   * @return true if the the String is not null or empty, false otherwise
   */
  private boolean notEmpty(String s) {
    if (s != null && !"".equals(s)) {
      return true;
    }

    return false;
  }

  /**
   * Helper method that returns the header that should be appended to all log messages.
   *
   * @return the log header.
   */
  private String getLogHead() {
    return "[processor=" + context.getId() + "] ";
  }

  /**
   * Helper class to connect and bind to the SMSC. It implements java.lang.Runnable so it can run
   * asynchronously.
   *
   * @author German Escobar
   */
  private class ConnectionThread implements Runnable {

    /**
     * The maximum number of retries before it gives up
     */
    private int maxRetries;

    /**
     * The initial delay before attempting the first connection
     */
    private long initialDelay;

    /**
     * Constructor. Creates an instance with the specified parameters.
     *
     * @param maxRetries the maximum number of retries.
     * @param initialDelay the initial delay before the first try.
     */
    public ConnectionThread(int maxRetries, long initialDelay) {
      this.maxRetries = maxRetries;
      this.initialDelay = initialDelay;
    }

    @Override
    public void run() {
      if (connecting) {
        log.debug(getLogHead() + "wont start connection thread because it looks like there is another already running");
        return;
      }

      connecting = true;
      log.info(getLogHead() + "schedule connect after " + initialDelay + " millis");
      try {
        Thread.sleep(initialDelay);
      } catch (InterruptedException e) {
      }

      // keep trying while the connector is started and until it exceeds the max retries or successfully connects
      int attempt = 0;
      while (attempt < maxRetries
          && started
          && !bound) {

        try {
          log.info(getLogHead() "trying to connect to " + getConfiguration().getHost() + " - attempt #" + (++attempt) + "...");

          // try to bind
          connection = bind();

          // if bound, change the status and show log that we are connected
          bound = true;
          status = MonitorStatusBuilder.ok();
          log.info(getLogHead() "connected to '" + configuration.getHost() + ":" + configuration.getPort() + "'");

        } catch (Exception e) {
          // log the exception and change status
          logException(e, attempt == 1);
          status = MonitorStatusBuilder.failed("could not connect", e);

          // close session just in case
          try { connection.closeLink(); } catch (Exception f) {}

          // wait the configured delay between reconnects
          try {
            Thread.sleep(configuration.getReconnectDelay());
          } catch (InterruptedException ee) {
          }

        }
      }
      connecting = false;

    }

    /**
     * Helper method that creates an Connection and tries to bind.
     *
     * @return
     * @throws Exception
     */
    private Connection bind() throws Exception {
      TcpLink link = null;

      try {
        link = new TcpLink(configuration.getHost(), configuration.getPort());
        connection = new Connection(link, true);
        autoAckMessages(connection);

        MessageListener messageListener = new MessageListener();
        connection.addObserver(messageListener);

        connection.bind(getConnectionType(configuration), configuration.getSystemId(), configuration.getPassword(), configuration.getSystemType(),
            getBindTON(), getBindNPI(), null);

        BindResp bindResp = messageListener.getBindResponse(configuration.getBindTimeout());
        if (bindResp == null || bindResp.getCommandStatus() != 0) {
          // close the link if no response
          if (link != null) {
            try { link.close(); } catch (Exception f) {}
          }
          throw new Exception("Bind Response failed: " + bindResp);
        }

        return connection;
      } catch (Exception e) {
        // close the link if an exception was thrown
        if (link != null) {
          try { link.close(); } catch (Exception f) {}
        }

        throw e;
      }
    }

    private void autoAckMessages(Connection connection) {
      connection.autoAckLink(true);
      connection.autoAckMessages(true);
    }

    private int getConnectionType(SmppConfiguration configuration) {
      int type = Connection.TRANSCEIVER;
      if (configuration.getBindType().equals(BindType.RECEIVER)) {
        type = Connection.RECEIVER;
      } else if (configuration.getBindType().equals(BindType.TRANSMITTER)) {
        type = Connection.TRANSMITTER;
      }

      return type;
    }

    private int getBindNPI() {
      int  bindNPI = 0;
      if (configuration.getBindNPI() != null && !"".equals(configuration.getBindNPI())) {
        bindNPI = Integer.parseInt(configuration.getBindNPI());
      }

      return bindNPI;
    }

    private int getBindTON() {
      int bindTON = 0;
      if (configuration.getBindTON() != null && !"".equals(configuration.getBindTON())) {
        bindTON = Integer.parseInt(configuration.getBindTON());
      }

      return bindTON;
    }

    /**
     * Helper method to log the exception when fail. We don't want to print the exception
     * on every attempt,
     * just the first time
     * @param e
     * @param firstTime
     */
    private void logException(Exception e, boolean firstTime) {
      String logError = getLogHead() + "failed to connect";

      // print the exception only the first time
      if (firstTime) {
        log.error(logError, e);
      } else {
        log.error(logError + ": " + e.getMessage());
      }
    }

  }

  /**
   * Sends a enquire_link request to the server to check the connection periodically. The interval
   * between requests is determined by the {@link LogicaConfiguration#getEnquireLinkTimer()} method.
   *
   * @author German Escobar
   */
  class EnquireLinkThread extends Thread {

    public void run() {
      while (started) {
        try {
          // sleep the amount of time determined in the configuration
          Thread.sleep(configuration.getEnquireLinkTimer());

          if (bound) {
            // send the request
            boolean success = enquireLink();

            // if not success, try to restart the connection
            if (!success) {
              bound = false;
              status = MonitorStatusBuilder.failed("enquire link failed");

              try { connection.closeLink(); } catch (Exception e) {}

              log.info("creating new ConnectionThread after a enquire link failed");
              new Thread(
                new ConnectionThread(Integer.MAX_VALUE, configuration.getInitialReconnectDelay())
              ).start();
            }
          }
        } catch (InterruptedException e) {}
      }
    }

    /**
     * Helper method that actually sends the request.
     *
     * @return true if the connection is alive, false otherwise.
     */
    private boolean enquireLink() {
      try {
        EnquireLink request = new EnquireLink();
        connection.sendRequest(request);

        log.trace(getLogHead() + "Enquire Link: " + request.toString());

        return true;
      } catch (Exception e) {
        log.info(getLogHead() + "Exception while Enquire Link: " + e.getMessage(), e);
      }

      return false;
    }
  }

  /**
   * This thread is started in the {@link #doStart()} method and it process the submit_sm
   * responses that are queued in the submitSmResponses list.
   *
   * @author German Escobar
   */
  class SubmitSmResponseThread extends Thread {

    public void run() {
      while (started) {
        try {
          if (bound) {
            process();

            synchronized (submitSmResponses) {
              submitSmResponses.wait(500);
            }
          }
        } catch (Exception e) {
          log.warn(getLogHead() + "Exception receiving event: " + e.getMessage(), e);
        }
      }
    }

    /**
     * Helper method that will retrieve all the queued submit responses and will process them if
     * the lastProcessTime is null or it hasn't been checked in the last 500 ms.
     */
    private void process() {
      List<SubmitSmResp> submitSmResponseCopy = new ArrayList<SubmitSmResp>(submitSmResponses);

      if (submitSmResponseCopy.size() > 10) {
        log.debug("processing " + submitSmResponseCopy.size() + " submit_sm responses");
      }
      for (SubmitSmResp response : submitSmResponseCopy) {
        // check if we have to precess this response
        if (response.lastProcessedTime == null || (new Date().getTime() - response.lastProcessedTime.getTime()) > 500) {
          process(response);
        }
      }
    }

    /**
     * Helper method that will process a submit_sm response. It will try to find the message that originated the
     * response and update the messageId and the commandStatus
     *
     * @param response the submit_sm response to be processed
     */
    private void process(SubmitSmResp response) {
      int sequenceNumber = response.submitSMResp.getSequenceNum();
      String messageId = response.submitSMResp.getMessageId();
      int commandStatus = response.submitSMResp.getCommandStatus();

      log.debug(getLogHead() + "processing submit_sm_response for sequence " + sequenceNumber
          + " with messageId '" + messageId + "' and command status " + commandStatus);

      // if no messageStore is set, we can't process this response
      if (messageStore == null) {
        submitSmResponses.remove(response);
        log.warn("MessageStore is null: ignoring submit_sm response: " + response.submitSMResp.toString());
        return;
      }

      // try to find a message that matches the criteria
      MessageCriteria criteria = new MessageCriteria()
        .direction(context.getDirection())
        .addProperty("destination", context.getId())
        .addProperty("smsc_sequencenumber", response.submitSMResp.getSequenceNum());

      long startTime = new Date().getTime();
      Collection<Message> messages = messageStore.list(criteria);
      long endTime = new Date().getTime();
      log.trace(getLogHead() + "retrieve message with smsc_sequencenumber " + response.submitSMResp.getSequenceNum()
          + " took " + (endTime - startTime) + " milis");

      Message message = null;
      if (!messages.isEmpty()) {
        message = messages.iterator().next();
      }

      // if the message is found, update it, otherwise, try later
      if (message != null) {
        submitSmResponses.remove(response);

        message.setProperty("messageId", messageId);
        message.setProperty("commandStatus", commandStatus);
        message.setProperty("reponseTime", new Date());

        if (configuration.getFailedCommandStatuses().contains(commandStatus + "")) {
          message.setStatus(Message.STATUS_FAILED);
        }

        startTime = new Date().getTime();
        messageStore.saveOrUpdate(message);
        endTime = new Date().getTime();
        log.trace(getLogHead() + "update message with id " + message.getId() + " took " + (endTime - startTime)
            + " milis");
      } else {
        // we couldn't find a matching message, try later or delete if too old
        if (response.lastProcessedTime != null && System.currentTimeMillis() - response.lastProcessedTime.getTime() > 5 * 60 * 1000) {
          log.warn(getLogHead() + "message with smsc_sequencenumber "
              + response.submitSMResp.getSequenceNum() + " not found after " + response.retries
              + " retries ... ignoring");
          submitSmResponses.remove(response);
        } else {
          response.lastProcessedTime = new Date();
          response.retries++;
        }
      }
    }

  }

  /**
   * This thread is started in the {@link #doStart()} method and it process the delivery receipts
   * that are queued in the deliverReceipts list.
   *
   * @author German Escobar
   */
    class DeliveryReceiptThread extends Thread {
    public void run() {
      while (started) {
        try {
          if (bound) {
            process();

            synchronized (deliveryReceipts) {
              deliveryReceipts.wait(500);
            }
          }
        } catch (Exception e) {
          log.warn(getLogHead() + "Exception receiving: " + e.getMessage(), e);
        }
      }
    }

    /**
     * Helper method that will retrieve all the queued delivery receipt and will process them if
     * the lastProcessTime is null or it hasn't been checked in the last 900 ms.
     */
    private void process() {
      List<DeliveryReceipt> deliveryReceiptsCopy = new ArrayList<DeliveryReceipt>(deliveryReceipts);

      for (DeliveryReceipt dr : deliveryReceiptsCopy) {
        long idleTime = 900L * dr.retries;

        if (dr.lastProcessedTime == null || (new Date().getTime() - dr.lastProcessedTime.getTime()) > idleTime) {
          process(dr);
        }
      }
    }

    /**
     * Helper method that will process a delivery receipt. It will try to find the message that matches the
     * id of the delivery receipt and update its status.
     *
     * @param dr the delivery receipt to be processed.
     */
    private void process(DeliveryReceipt dr) {
      if (messageStore == null) {
        log.warn("MessageStore is null: ignoring delivery receipt: " + dr.deliverSm.toString());
        return;
      }

      // retrieve the delivery receipt message and try to find the original message that originated the dr
      Message drMessage = dr.message;

      Message originalMessage = findOriginalMessage(drMessage);

      if (originalMessage != null) {
        deliveryReceipts.remove(dr);

        // update original message
        String receiptStatus = drMessage.getProperty("finalStatus", String.class);
        Date doneDate = drMessage.getProperty("doneDate", Date.class);

        originalMessage.setProperty("receiptStatus", receiptStatus);
        originalMessage.setProperty("receiptTime", doneDate);
        messageStore.saveOrUpdate(originalMessage);

        log.debug("message with id " + originalMessage.getId() + " updated with receiptStatus: "
            + receiptStatus);

        // update the reference of the delivery receipt
        drMessage.setProperty("originalReference", originalMessage.getReference());

        // only route delivery receipt if original message contains a receiptDestination
        String receiptDestination = originalMessage.getProperty("receiptDestination", String.class);
        if (receiptDestination != null && !"".equals(receiptDestination)) {
          drMessage.setDestination(receiptDestination); // this skips the routing mechanism
          messageProducer.produce(drMessage);
        }
      } else {
        // we couldn't find a matching message, remove or try later if less than 6 retries
        if (dr.retries > 9) {
          log.warn(getLogHead() + " could not find message with messageId '" + convertMessageId(drMessage.getProperty("messageId", String.class)) + "' after " + dr.retries + " retries ... ignoring.");
          deliveryReceipts.remove(dr);
        } else {
          dr.lastProcessedTime = new Date();
          dr.retries++;

          log.debug(getLogHead() + " could not find message with messageId: " + convertMessageId(drMessage.getProperty("messageId", String.class)) + " ... trying later, retry " + dr.retries);
        }
      }
    }

    /**
     * Helper method that will try to find the original message of the delivery receipt.
     *
     * @param drMessage the delivery receipt message.
     * @return the Message that originated the delivery receipt, or null if not found.
     */
    private Message findOriginalMessage(Message drMessage) {
      Message originalMessage = null; // this is what we are returning

      String messageId = drMessage.getProperty("messageId", String.class);
      messageId = convertMessageId(messageId);
      String to = drMessage.getProperty("to", String.class);
      String from = drMessage.getProperty("from", String.class);

      MessageCriteria criteria = new MessageCriteria()
        .direction(context.getDirection())
        .addProperty("destination", context.getId())
        .addProperty("smsc_messageid", messageId);

      long startTime = new Date().getTime();
      Collection<Message> messages = messageStore.list(criteria);
      long endTime = new Date().getTime();

      log.trace(getLogHead() + "retrieve message with smsc_messageid: " + messageId + ", to: " + to + ", from: " + from + " took "
          + (endTime - startTime) + " milis");

      if (messages.size() == 1) {
        originalMessage = messages.iterator().next();
      } else if (messages.size() > 1) {
        log.debug(messages.size() + " messages matched the id: " + messageId);

        // iterate through the matched messages to find one that matches exactly
        Iterator<Message> iterMessages = messages.iterator();
        while (iterMessages.hasNext() && originalMessage == null) {
          Message message = iterMessages.next();

          String mTo = message.getProperty("to", String.class);
          if (mTo.equals(to) || mTo.equals(from)) {
            originalMessage = message;
          }
        }
      }

      return originalMessage;
    }

    /**
     * Helper method that checks the configuration to see if we have to convert the delivery receipt id to
     * hexadecimal or decimal.
     *
     * @param messageId the string to be converted.
     * @return the converted messageId according to the configuration (can happen that the value is not converted at all).
     */
    private String convertMessageId(String messageId) {
      if (configuration.getDlrIdConversion().equals(DlrIdConversion.HEXA_TO_DEC)) {
        return SmppUtil.toMessageIdAsLong(messageId) + "";
      } else if (configuration.getDlrIdConversion().equals(DlrIdConversion.DEC_TO_HEXA)) {
        try {
          long messageIdAsLong = Long.parseLong(messageId);
          return SmppUtil.toMessageIdAsHexString(messageIdAsLong);
        } catch (NumberFormatException e) {
          log.warn("NumberFormatException trying to convert '" + messageId + " to hexadecimal");
        }
      }

      return messageId;
    }

  }

  private class MessageListener implements ConnectionObserver {

    private BindResp bindResponse;

    @Override
    public void packetReceived(Connection source, SMPPPacket packet) {
      if (packet.isRequest()) {
        if (packet.getCommandId() == SMPPPacket.DELIVER_SM) {
          int esmClass = packet.getEsmClass();

          // check if it is a short message or a delivery receipt and handle appropriately
          if (esmClass == SMPPPacket.SMC_RECEIPT) {
            try {
              handleDeliveryReceipt((DeliverSM) packet);
            } catch (ParseException e) {
              log.warn(getLogHead() + "ParseException processing delivery receipt: " + e.getMessage());
            }
          } else {
            handleDeliverSm((DeliverSM) packet);
          }
        }
      } else {
        if (packet.getCommandId() == SMPPPacket.BIND_RECEIVER_RESP || packet.getCommandId() == SMPPPacket.BIND_TRANSMITTER_RESP
            || packet.getCommandId() == SMPPPacket.BIND_TRANSCEIVER_RESP) {
          bindResponse = (BindResp) packet;
        } else if (packet.getCommandId() == SMPPPacket.SUBMIT_SM_RESP) {
          handleSubmitSmResponse((SubmitSMResp) packet);
        }
      }
    }

    @Override
    public void update(Connection source, SMPPEvent event) {
      log.info(getLogHead() + "an SMPPEvent was received: " + event.toString());

      if (event.getType() == SMPPEvent.RECEIVER_EXIT && started) {
        ReceiverExitEvent exitEvent = (ReceiverExitEvent) event;

        String msg = getLogHead() + "Received an exit event (ReceiverExitEvent) with reason " + exitEvent.getReason()
            + ", state is " + exitEvent.getState();
        if (exitEvent.getReason() == ReceiverExitEvent.EXCEPTION) {
          msg += ": " + exitEvent.getException().getMessage();
        }
        log.error(msg, exitEvent.getException());

        // restart the connection if there was an exception
        if (exitEvent.getReason() == ReceiverExitEvent.EXCEPTION) {
          bound = false;
          status = MonitorStatusBuilder.failed("received an exit event");

          try { connection.closeLink(); } catch (Exception e) {}

          log.info(getLogHead() + "creating new ConnectionThread after a ReceiverExitEvent");
          if (!connecting) {
            new Thread(
              new ConnectionThread(Integer.MAX_VALUE, configuration.getInitialReconnectDelay())
            ).start();
          }
        }
      } else if (event.getType() == SMPPEvent.RECEIVER_EXCEPTION) {
        ReceiverExceptionEvent exceptionEvent = (ReceiverExceptionEvent) event;
        log.error(getLogHead() + "Received ReceiverExceptionEvent with state " + exceptionEvent.getState()
            + ": " + exceptionEvent.getException().getMessage(), exceptionEvent.getException());
      }
    }

    public BindResp getBindResponse(long timeout) {
      long startTime = new Date().getTime();

      while ((new Date().getTime() - startTime) < timeout && bindResponse == null) {
        try { Thread.sleep(500); } catch (Exception e) {}
      }

      return bindResponse;
    }

    /**
     * Helper method that handles a submit_sm response. It will wrap it into a SubmitSmResponse class
     * (defined in this same file) and add it to the submitSmResponses list. It will then be retrieved by the
     * SubmitSmResponseThread (defined in this file) and processed accordingly.
     *
     * @param response the SubmitSMResp that was received.
     */
    private void handleSubmitSmResponse(SubmitSMResp response) {
      log.debug(getLogHead() + "received submit_sm response with sequence '" + response.getSequenceNum() + "' and status " + response.getCommandStatus());

      SubmitSmResp submitSmResponse = new SubmitSmResp();
      submitSmResponse.submitSMResp = response;

      submitSmResponses.add(submitSmResponse);

      // the SubmitSmResponseThread might be waiting on the list, notify that a new response arrived
      synchronized (submitSmResponses) {
        submitSmResponses.notifyAll();
      }
    }

    /**
     * Helper method that handle deliver_sm PDU's. It will create a Message object to inject it into the
     * gateway using the MessageProducer.
     *
     * @param deliverSm the deliver_sm PDU that was received.
     */
    private void handleDeliverSm(DeliverSM deliverSm) {
      if (!configuration.isDiscardIncomingMsgs()) {
        log.debug(getLogHead() + "received DeliverSM: " +  deliverSm.toString());

        String from = deliverSm.getSource().getAddress();
        String to = deliverSm.getDestination().getAddress();
        String text = new String(deliverSm.getMessageText());
        log.debug(getLogHead() + "DeliverSM text: " + new String(deliverSm.getMessageText()));
        log.debug(getLogHead() + "DeliverSM text bytes: " + decodeBytes(deliverSm.getMessage()));
        log.debug(getLogHead() + "DeliverSM data coding: " + deliverSm.getDataCoding());
        TLVTable tlvTable = deliverSm.getTLVTable();
        if (tlvTable != null) {
          Collection values = deliverSm.getTLVTable().values();
          for (Object tlv : values) {
            log.debug(getLogHead() + "DeliverSM tlv: " + tlv);
          }
        } else {
          log.debug(getLogHead() + "DeliverSM has no TLVTable!");
        }

        Message message = new Message();
        message.setProperty("to", to);
        message.setProperty("from", from);
        message.setProperty("text", text);

        messageProducer.produce(message);
      } else {
        log.warn(getLogHead() + "received DeliverSM discardIncomingMessages is set ... ignoring message: " + deliverSm.toString());
      }
    }

    private String decodeBytes(byte[] bytes) {
      StringBuilder sb = new StringBuilder();
      for (byte b : bytes) {
        sb.append(String.format("%02X ", b));
      }
      return sb.toString();
    }

    /**
     * Helper method that handle delivery receipts PDU's. It will create a Message object out of the delivery
     * receipt, wrap it in the DeliveryReceipt class (defined in this file) and add it to the deliveryReceipts
     * list. It will be retrieved later by the DeliveryReceiptThread (defined in this file) and processed
     * accordingly
     *
     * @param deliverSm the deliver_sm PDU that was received, it is a delivery receipt.
     * @throws ParseException if the format of the short message (delivery receipt) is wrong.
     */
    private void handleDeliveryReceipt(DeliverSM deliverSm) throws ParseException {
      String dest = "null";
      if (deliverSm.getDestination() != null) {
        dest = deliverSm.getDestination().getAddress();
      }

      String source = "null";
      if (deliverSm.getSource() != null) {
        source = deliverSm.getSource().getAddress();
      }

      log.info(getLogHead() + "received Delivery Receipt: source: " + source + ", dest: " + dest + ", text: " + deliverSm.getMessageText());

      String shortMessage = new String(deliverSm.getMessageText());
      String id = getDeliveryReceiptValue("id", shortMessage);

      int submitted = Integer.parseInt(getDeliveryReceiptValue("sub", shortMessage));
      int delivered = Integer.parseInt(getDeliveryReceiptValue("dlvrd", shortMessage));

      SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmm");
      Date submitDate = sdf.parse(getDeliveryReceiptValue("submit date", shortMessage));
      Date doneDate = sdf.parse(getDeliveryReceiptValue("done date", shortMessage));

      String finalStatus = getDeliveryReceiptValue("stat", shortMessage);
      //String error = getDeliveryReceiptValue("err", shortMessage);

      // create the message
      Message message = new Message();

      message.setProperty("isDLR", true);

      String to = deliverSm.getDestination().getAddress();
      String from = deliverSm.getSource().getAddress();

      message.setProperty("to", to);
      message.setProperty("from", from);

      // set the id
      message.setProperty("messageId", id);

      // set the number of submitted and submit date
      message.setProperty("submitted", submitted);
      message.setProperty("submitDate", submitDate);

      // set the number of delivered
      message.setProperty("delivered", delivered);

      // set done date
      message.setProperty("doneDate", doneDate);

      // set final status
      message.setProperty("finalStatus", finalStatus);

      DeliveryReceipt deliveryReceipt = new DeliveryReceipt();
      deliveryReceipt.message = message;
      deliveryReceipt.deliverSm = deliverSm;

      // add to the deliveryReceipts list and notify the DeliveryReceiptThread that might be waiting
      deliveryReceipts.add(deliveryReceipt);
      synchronized (deliveryReceipts) {
        deliveryReceipts.notifyAll();
      }
    }

    /**
     * Helper method used to get the delivery receipt attributes values.
     *
     * @param attrName is the attribute name.
     * @param source the original source id:IIIIIIIIII sub:SSS dlvrd:DDD submit
     *        date:YYMMDDhhmm done date:YYMMDDhhmm stat:DDDDDDD err:E
     *        Text:....................
     * @return the value of specified attribute.
     * @throws IndexOutOfBoundsException
     */
    private String getDeliveryReceiptValue(String attrName, String source) throws IndexOutOfBoundsException {
      String tmpAttr = attrName + ":";
      int startIndex = source.indexOf(tmpAttr);
      if (startIndex < 0) {
        return null;
      }
      startIndex = startIndex + tmpAttr.length();
      int endIndex = source.indexOf(" ", startIndex);
      if (endIndex > 0) {
        return source.substring(startIndex, endIndex);
      }

      return source.substring(startIndex);
    }

  }

  /**
   * Helper class that wraps the submit_sm response with the last time it was tried to be processed and number
   * of retries.
   *
   * @author German Escobar
   */
  class SubmitSmResp {
    public SubmitSMResp submitSMResp;
    public Date lastProcessedTime;
    public int retries = 0;
  }

  /**
   * Helper class that wraps the delivery receipt PDU and the delivery receipt messages, besides the last time
   * it was tried to be processed and the number of retries.
   *
   * @author German Escobar
   */
  class DeliveryReceipt {
    public Message message;
    public DeliverSM deliverSm;
    public Date lastProcessedTime;
    public int retries = 0;
  }

}
TOP

Related Classes of org.mokai.connector.smpp.SmppConnector$DeliveryReceipt

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.