com.cloudhopper.sxmp.SxmpParser.java Source code

Java tutorial

Introduction

Here is the source code for com.cloudhopper.sxmp.SxmpParser.java

Source

package com.cloudhopper.sxmp;

/*
 * #%L
 * ch-sxmp
 * %%
 * Copyright (C) 2012 - 2013 Cloudhopper by Twitter
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import com.cloudhopper.commons.util.HexUtil;
import com.cloudhopper.sxmp.util.MobileAddressUtil;
import com.cloudhopper.commons.util.StringUtil;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.json.JSONObject;
import org.json.JSONException;
import org.xml.sax.*;
import org.xml.sax.ext.Locator2;
import org.xml.sax.helpers.DefaultHandler;

/**
 *
 * @author joelauer
 */
public class SxmpParser {
    private static final Logger logger = LoggerFactory.getLogger(SxmpParser.class);

    static protected DateTimeFormatter dateTimeFormat = ISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC);

    public static final String VERSION_1_0 = "1.0";
    public static final String VERSION_1_1 = "1.1";
    public static final String VERSION_1_2 = "1.2";

    protected final String version;

    // for backwards compatibility
    public SxmpParser() {
        this.version = VERSION_1_0;
    }

    public SxmpParser(final String version) {
        this.version = version;
    }

    /**
    public Node parse(InputSource source) throws IOException, SAXException {
    //        _dtd=null;
    Handler handler = new Handler();
    XMLReader reader = _parser.getXMLReader();
    reader.setContentHandler(handler);
    reader.setErrorHandler(handler);
    reader.setEntityResolver(handler);
    if (logger.isDebugEnabled())
        logger.debug("parsing: sid=" + source.getSystemId() + ",pid=" + source.getPublicId());
    _parser.parse(source, handler);
    if (handler.error != null)
        throw handler.error;
    Node root = (Node)handler.root;
    handler.reset();
    return root;
    }
        
    public synchronized Node parse(String xml) throws IOException, SAXException {
    ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes());
    return parse(is);
    }
        
    public synchronized Node parse(File file) throws IOException, SAXException {
    return parse(new InputSource(file.toURI().toURL().toString()));
    }
        
    public synchronized Node parse(InputStream in) throws IOException, SAXException {
    //_dtd=null;
    Handler handler = new Handler();
    XMLReader reader = _parser.getXMLReader();
    reader.setContentHandler(handler);
    reader.setErrorHandler(handler);
    reader.setEntityResolver(handler);
    _parser.parse(new InputSource(in), handler);
    if (handler.error != null)
        throw handler.error;
    Node root = (Node)handler.root;
    handler.reset();
    return root;
    }
     */

    public Operation parse(InputStream in)
            throws SxmpParsingException, IOException, SAXException, ParserConfigurationException {
        // create a new SAX parser
        SAXParserFactory factory = SAXParserFactory.newInstance();
        //factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);

        SAXParser parser = factory.newSAXParser();
        //_parser.getXMLReader().setFeature("http://xml.org/sax/features/validation", validating);
        parser.getXMLReader().setFeature("http://xml.org/sax/features/namespaces", true);
        parser.getXMLReader().setFeature("http://xml.org/sax/features/namespace-prefixes", true);
        parser.getXMLReader().setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true);

        //_dtd=null;
        Handler handler = new Handler();
        XMLReader reader = parser.getXMLReader();
        reader.setContentHandler(handler);
        reader.setErrorHandler(handler);
        reader.setEntityResolver(handler);

        // try parsing (may throw an SxmpParsingException in the handler)
        try {
            parser.parse(new InputSource(in), handler);
        } catch (com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException e) {
            throw new SxmpParsingException(SxmpErrorCode.INVALID_XML, "XML encoding mismatch", null);
        }

        // check if there was an error
        if (handler.error != null) {
            throw handler.error;
        }

        // check to see if an operation was actually parsed
        if (handler.getOperation() == null) {
            throw new SxmpParsingException(SxmpErrorCode.MISSING_REQUIRED_ELEMENT,
                    "The operation type [" + handler.operationType.getValue() + "] requires a request element",
                    new PartialOperation(handler.operationType));
        }

        // if we got here, an operation was parsed -- now we need to validate it
        // to make sure that it has all required elements
        try {
            handler.getOperation().validate();
        } catch (SxmpErrorException e) {
            throw new SxmpParsingException(e.getErrorCode(), e.getErrorMessage(), handler.getOperation());
        }

        return handler.getOperation();
    }

    private class Handler extends DefaultHandler {

        SAXParseException error;
        private int depth;
        Attributes currentAttrs;
        StringBuilder charBuffer;
        Operation.Type operationType;
        Account account;
        Application application;
        Operation operation;
        // we'll use this hashmap for duplicate element checks
        HashSet<String> elements;

        private String encoding = "unknown";
        private Locator2 locator;

        /*
         * Per http://xerces.apache.org/xerces2-j/javadocs/api/org/xml/sax/ContentHandler.html#setDocumentLocator%28org.xml.sax.Locator%29
         * Note that the locator will return correct information only during the
         * invocation SAX event callbacks after startDocument returns and before
         * endDocument is called. The application should not attempt to use it
         * at any other time.
         */
        @Override
        public void setDocumentLocator(Locator locator) {
            if (locator instanceof Locator2) {
                this.locator = (Locator2) locator;
            }
        }

        // utility class to hold error code and message
        private class ErrorCodeMessage {
            public int code;
            public String message;
        }

        Handler() {
            depth = -1;
            charBuffer = new StringBuilder(200);
            this.elements = new HashSet<String>();
        }

        /**
         * Gets the value from a required attribute.  This method will provide
         * the following error checking.  If the attribute is missing, it will
         * throw a MISSING_ATTRIBUTE exception.  If the attribute was included,
         * but the value was empty, it will throw an EMPTY_VALUE exception.
         * Otherwise, it will return the value.
         * @param attr The attributes to check
         * @param name The attribute name to find
         * @return The value
         * @throws SxmpErrorException See method overview
         */
        public String getOptionalAttributeValue(String tag, Attributes attrs, String name)
                throws SxmpParsingException {
            //
            // make sure the attribute exists
            //
            int size = attrs.getLength();
            String value = null;

            for (int i = 0; i < size; i++) {
                if (attrs.getQName(i).equals(name)) {
                    // is this the second time we found this attribute?
                    if (value != null) {
                        throw new SxmpParsingException(SxmpErrorCode.MULTIPLE_ATTRIBUTES_NOT_SUPPORTED,
                                "Multiple [" + name + "] attributes within the [" + tag
                                        + "] element are not supported",
                                (this.operation != null ? this.operation
                                        : new PartialOperation(this.operationType)));
                    }

                    value = attrs.getValue(i);

                    // is there some sort of value for it?
                    if (StringUtil.isEmpty(value)) {
                        throw new SxmpParsingException(SxmpErrorCode.EMPTY_VALUE,
                                "The [" + name + "] attribute was empty in the [" + tag + "] element",
                                (this.operation != null ? this.operation
                                        : new PartialOperation(this.operationType)));
                    }
                }
            }

            if (value != null) {
                return value;
            } else {
                return null;
            }
        }

        /**
         * Gets the value from a required attribute.  This method will provide
         * the following error checking.  If the attribute is missing, it will
         * throw a MISSING_ATTRIBUTE exception.  If the attribute was included,
         * but the value was empty, it will throw an EMPTY_VALUE exception.
         * Otherwise, it will return the value.
         * @param attr The attributes to check
         * @param name The attribute name to find
         * @return The value
         * @throws SxmpErrorException See method overview
         */
        public String getRequiredAttributeValue(String tag, Attributes attrs, String name)
                throws SxmpParsingException {
            //
            // same process as an optional element
            //
            String value = getOptionalAttributeValue(tag, attrs, name);

            if (value != null) {
                return value;
            } else {
                throw new SxmpParsingException(SxmpErrorCode.MISSING_REQUIRED_ATTRIBUTE,
                        "The attribute [" + name + "] is required with the [" + tag + "] element",
                        (this.operation != null ? this.operation : new PartialOperation(this.operationType)));
            }
        }

        public void parseRequestElement(String tag, Request request, Attributes attrs) throws SxmpParsingException {
            // should only occur once
            if (elements.contains(tag)) {
                // we already had the same operation before, so we'll include that in the error
                throw new SxmpParsingException(SxmpErrorCode.MULTIPLE_ELEMENTS_NOT_SUPPORTED,
                        "Multiple [" + tag + "] elements are not supported", this.operation);
            }

            // add the element tag to something we visited
            elements.add(tag);

            // an account must ALWAYS come before a request
            if (this.account == null) {
                throw new SxmpParsingException(SxmpErrorCode.MISSING_REQUIRED_ELEMENT,
                        "The [account] element is required before a [" + tag + "] element",
                        new PartialOperation(this.operationType));
            }

            // check if the request type matches the operation type
            if (request.getType() != this.operationType) {
                throw new SxmpParsingException(
                        SxmpErrorCode.OPTYPE_MISMATCH, "The operation type [" + operationType.getValue()
                                + "] does not match the [" + tag + "] element",
                        new PartialOperation(this.operationType));
            }

            // save the account in this request
            request.setAccount(this.account);
            request.setApplication(this.application);

            // save the request as our operation
            this.operation = request;

            // all requests have an optional referenceId attribute
            String referenceId = getOptionalAttributeValue("submitRequest", attrs, "referenceId");

            if (!StringUtil.isEmpty(referenceId)) {
                try {
                    request.setReferenceId(referenceId);
                } catch (SxmpErrorException e) {
                    throw new SxmpParsingException(SxmpErrorCode.INVALID_REFERENCE_ID, e.getErrorMessage(),
                            this.operation);
                }
            }
        }

        public void parseResponseElement(String tag, Response response) throws SxmpParsingException {
            // should only occur once
            if (elements.contains(tag)) {
                // we already had the same operation before, so we'll include that in the error
                throw new SxmpParsingException(SxmpErrorCode.MULTIPLE_ELEMENTS_NOT_SUPPORTED,
                        "Multiple [" + tag + "] elements are not supported", this.operation);
            }

            // add the element tag to something we visited
            elements.add(tag);

            // check if the request type matches the operation type
            if (response.getType() != this.operationType) {
                throw new SxmpParsingException(
                        SxmpErrorCode.OPTYPE_MISMATCH, "The operation type [" + operationType.getValue()
                                + "] does not match the [" + tag + "] element",
                        new PartialOperation(this.operationType));
            }

            // save the request as our operation
            this.operation = response;
        }

        public String parseCharacterData(String tag, boolean required) throws SxmpParsingException {
            // should only occur once
            if (elements.contains(tag)) {
                throw new SxmpParsingException(SxmpErrorCode.MULTIPLE_ELEMENTS_NOT_SUPPORTED,
                        "Multiple [" + tag + "] elements are not supported", this.operation);
            }

            // add the element tag to something we visisted
            elements.add(tag);

            // should be able to convert character data to an integer
            String text = charBuffer.toString();

            if (required && StringUtil.isEmpty(text)) {
                throw new SxmpParsingException(SxmpErrorCode.EMPTY_VALUE,
                        "The element [" + tag + "] must contain a value", this.operation);
            }

            return text;
        }

        public Integer parseIntegerValue(String name, String value) throws SxmpParsingException {
            // convert to an integer
            try {
                return Integer.valueOf(value);
            } catch (NumberFormatException e) {
                throw new SxmpParsingException(SxmpErrorCode.UNABLE_TO_CONVERT_VALUE,
                        "Unable to convert [" + value + "] to an integer for [" + name + "]", this.operation);
            }
        }

        public Boolean parseBooleanValue(String name, String value) throws SxmpParsingException {
            // convert to a boolean
            if (value == null) {
                throw new SxmpParsingException(SxmpErrorCode.UNABLE_TO_CONVERT_VALUE,
                        "Unable to convert [null] to a boolean for [" + name + "]", this.operation);
            }

            if (value.equalsIgnoreCase("true")) {
                return Boolean.TRUE;
            } else if (value.equalsIgnoreCase("false")) {
                return Boolean.FALSE;
            } else {
                throw new SxmpParsingException(SxmpErrorCode.UNABLE_TO_CONVERT_VALUE,
                        "Unable to convert [" + value + "] to a boolean for [" + name + "]", this.operation);
            }
        }

        public ErrorCodeMessage parseErrorElement(String tag, Attributes attrs) throws SxmpParsingException {
            // should only occur once at this level
            if (elements.contains(tag)) {
                throw new SxmpParsingException(SxmpErrorCode.MULTIPLE_ELEMENTS_NOT_SUPPORTED,
                        "Multiple [" + tag + "] elements are not supported", this.operation);
            }

            // add the element tag to something we visisted
            elements.add(tag);

            ErrorCodeMessage ecm = new ErrorCodeMessage();
            String errorCodeString = this.getRequiredAttributeValue("error", currentAttrs, "code");
            ecm.message = this.getRequiredAttributeValue("error", currentAttrs, "message");
            ecm.code = this.parseIntegerValue("code", errorCodeString);
            return ecm;
        }

        public Operation getOperation() {
            return this.operation;
        }

        @Override
        public void processingInstruction(String target, String value) {
            //logger.debug("processing instr!");
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException {
            //
            // get the tag stripped of any namespace
            //
            String tag = (StringUtil.isEmpty(uri)) ? qName : localName;

            // debug
            //logger.debug("startElement: tag=" + tag);
            //            logger.trace("startElement: uri=" + uri + ", localName=" + localName + ", qName=" + qName);

            // always increment our depth
            depth++;

            //
            // Depth: zero
            // Element(s): operation
            //
            if (depth == 0) {
                // make sure our encoding is valid
                if (this.locator != null) {
                    this.encoding = this.locator.getEncoding();
                }
                if (version.equals(VERSION_1_1)) {
                    if (!this.encoding.toLowerCase().equals("utf-8")) {
                        throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_TEXT_ENCODING,
                                "Invalid encoding: " + encoding, null);
                    }
                }

                if (!tag.equals("operation")) {
                    throw new SxmpParsingException(SxmpErrorCode.INVALID_XML, "Root element must be an [operation]",
                            null);
                }

                // must contain an attribute with the type
                String opType = getRequiredAttributeValue("operation", attrs, "type");

                // parse it via the enum
                this.operationType = Operation.Type.parse(opType);

                if (this.operationType == null) {
                    throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_OPERATION,
                            "Unsupported operation type [" + opType + "]", null);
                }
                //
                // Depth: 1
                // Element(s): account, submitRequest
                //
            } else if (depth == 1) {

                if (tag.equals("account")) {
                    // should only occur once
                    if (elements.contains(tag)) {
                        throw new SxmpParsingException(SxmpErrorCode.MULTIPLE_ELEMENTS_NOT_SUPPORTED,
                                "Multiple [account] elements are not supported",
                                new PartialOperation(this.operationType));
                    }

                    elements.add(tag);

                    // should have two attributes, username and password
                    String username = getRequiredAttributeValue("account", attrs, "username");
                    String password = getRequiredAttributeValue("account", attrs, "password");
                    this.account = new Account(username, password);
                } else if (tag.equals("application")) {
                    // should only occur once
                    if (elements.contains(tag)) {
                        throw new SxmpParsingException(SxmpErrorCode.MULTIPLE_ELEMENTS_NOT_SUPPORTED,
                                "Multiple [application] elements are not supported",
                                new PartialOperation(this.operationType));
                    }
                } else if (tag.equals("error")) {
                    // this is a top level error -- indicates this should be
                    // the only element to return
                    ErrorCodeMessage ecm = parseErrorElement(tag, currentAttrs);

                    ErrorResponse errorResponse = new ErrorResponse(this.operationType, ecm.code, ecm.message);
                    this.operation = errorResponse;
                } else if (tag.equals("submitRequest")) {
                    parseRequestElement(tag, new SubmitRequest(version), attrs);
                } else if (tag.equals("deliverRequest")) {
                    parseRequestElement(tag, new DeliverRequest(), attrs);
                } else if (tag.equals("deliveryReportRequest")) {
                    parseRequestElement(tag, new DeliveryReportRequest(), attrs);
                } else if (tag.equals("submitResponse")) {
                    parseResponseElement(tag, new SubmitResponse());
                } else if (tag.equals("deliverResponse")) {
                    parseResponseElement(tag, new DeliverResponse());
                } else if (tag.equals("deliveryReportResponse")) {
                    parseResponseElement(tag, new DeliveryReportResponse());
                } else {
                    throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                            "Unsupported [" + tag + "] element found at depth [" + depth + "]",
                            new PartialOperation(this.operationType));
                }

                //
                // Depth: 2
                // Element(s): operatorId, etc.
                //
            } else if (depth == 2) {
                // do nothing here, always process in endElement callback
            } else {
                // NOTE: no other depths are supported in SXMP
                throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                        "Unsupported [" + tag + "] element found at depth [" + depth + "]", this.operation);
            }

            // starting element means reset our character buffer
            charBuffer.setLength(0);
            // save reference to attributes for processing in endElement
            currentAttrs = attrs;
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            //
            // get the tag stripped of any namespace
            //
            String tag = (StringUtil.isEmpty(uri)) ? qName : localName;

            //            logger.trace("endElement: uri=" + uri + ", localName=" + localName + ", qName=" + qName);

            if (depth == 1) {
                if (tag.equals("application")) {
                    // parse the character data, check for duplicates
                    String applicationNameText = parseCharacterData(tag, true);
                    this.application = new Application(applicationNameText);
                }
            } else if (depth == 2) {
                if (tag.equals("operatorId")) {
                    // parse the character data, check for duplicates
                    String operatorIdText = parseCharacterData(tag, true);

                    // convert to an integer
                    Integer operatorId = parseIntegerValue(tag, operatorIdText);

                    try {
                        if (operation instanceof MessageRequest) {
                            ((MessageRequest) operation).setOperatorId(operatorId);
                        } else {
                            throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                                    "The [operatorId] element is not supported for this operation type",
                                    this.operation);
                        }
                    } catch (SxmpErrorException e) {
                        throw new SxmpParsingException(e.getErrorCode(), e.getErrorMessage(), this.operation);
                    }
                } else if (tag.equals("priority")) {
                    // parse the character data, check for duplicates
                    String priorityText = parseCharacterData(tag, true);

                    // convert to an integer
                    Integer priorityFlag = parseIntegerValue(tag, priorityText);

                    try {
                        if (operation instanceof MessageRequest) {
                            ((MessageRequest) operation).setPriority(Priority.fromPriorityFlag(priorityFlag));
                        } else {
                            throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                                    "The [priority] element is not supported for this operation type",
                                    this.operation);
                        }
                    } catch (IllegalArgumentException e) {
                        throw new SxmpParsingException(SxmpErrorCode.INVALID_VALUE, e.getMessage(), this.operation);
                    }
                } else if (tag.equals("error")) {
                    // parse the error element
                    ErrorCodeMessage ecm = parseErrorElement(tag, currentAttrs);

                    if (operation instanceof Response) {
                        ((Response) operation).setErrorCode(ecm.code);
                        ((Response) operation).setErrorMessage(ecm.message);
                    } else {
                        throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                                "The [" + tag + "] element is not supported for this operation type",
                                this.operation);
                    }
                } else if (tag.equals("ticketId")) {
                    // parse the character data, check for duplicates
                    String ticketId = parseCharacterData(tag, true);

                    // we basically support a ticket element on anything (request or response)
                    operation.setTicketId(ticketId);
                } else if (tag.equals("status")) {
                    // should only occur once
                    if (elements.contains(tag)) {
                        throw new SxmpParsingException(SxmpErrorCode.MULTIPLE_ELEMENTS_NOT_SUPPORTED,
                                "Multiple [status] elements are not supported", this.operation);
                    }

                    // add the element tag to something we visisted
                    elements.add(tag);

                    String statusCodeString = this.getRequiredAttributeValue("status", currentAttrs, "code");
                    String statusMessage = this.getRequiredAttributeValue("status", currentAttrs, "message");
                    Integer statusCode = this.parseIntegerValue("code", statusCodeString);

                    DeliveryStatus status = new DeliveryStatus(statusCode, statusMessage);

                    if (operation instanceof DeliveryReportRequest) {
                        ((DeliveryReportRequest) operation).setStatus(status);
                    } else {
                        throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                                "The status element is not supported for this operation type", this.operation);
                    }
                } else if (tag.equals("messageError")) {
                    // should only occur once
                    if (elements.contains(tag)) {
                        throw new SxmpParsingException(SxmpErrorCode.MULTIPLE_ELEMENTS_NOT_SUPPORTED,
                                "Multiple [messageError] elements are not supported", this.operation);
                    }

                    // add the element tag to something we visisted
                    elements.add(tag);

                    String messageErrorCodeString = this.getRequiredAttributeValue("messageError", currentAttrs,
                            "code");
                    Integer messageErrorCode = this.parseIntegerValue("code", messageErrorCodeString);

                    if (operation instanceof DeliveryReportRequest) {
                        ((DeliveryReportRequest) operation).setMessageErrorCode(messageErrorCode);
                    } else {
                        throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                                "The messageError element is not supported for this operation type",
                                this.operation);
                    }
                } else if (tag.equals("createDate")) {
                    // should only occur once
                    if (elements.contains(tag)) {
                        throw new SxmpParsingException(SxmpErrorCode.MULTIPLE_ELEMENTS_NOT_SUPPORTED,
                                "Multiple [createDate] elements are not supported", this.operation);
                    }

                    String createDateText = parseCharacterData(tag, true);

                    DateTime createDate = null;
                    try {
                        createDate = dateTimeFormat.parseDateTime(createDateText);
                    } catch (Exception e) {
                        throw new SxmpParsingException(SxmpErrorCode.INVALID_VALUE,
                                "Unable to convert createDate [" + createDateText + "] into a DateTime",
                                this.operation);
                    }

                    if (operation instanceof DeliveryReportRequest) {
                        ((DeliveryReportRequest) operation).setCreateDate(createDate);
                    } else {
                        throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                                "The createDate element is not supported for this operation type", this.operation);
                    }
                } else if (tag.equals("finalDate")) {
                    // should only occur once
                    if (elements.contains(tag)) {
                        throw new SxmpParsingException(SxmpErrorCode.MULTIPLE_ELEMENTS_NOT_SUPPORTED,
                                "Multiple [finalDate] elements are not supported", this.operation);
                    }

                    String finalDateText = parseCharacterData(tag, true);

                    DateTime finalDate = null;
                    try {
                        finalDate = dateTimeFormat.parseDateTime(finalDateText);
                    } catch (Exception e) {
                        throw new SxmpParsingException(SxmpErrorCode.INVALID_VALUE,
                                "Unable to convert finalDate [" + finalDateText + "] into a DateTime",
                                this.operation);
                    }

                    if (operation instanceof DeliveryReportRequest) {
                        ((DeliveryReportRequest) operation).setFinalDate(finalDate);
                    } else {
                        throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                                "The finalDate element is not supported for this operation type", this.operation);
                    }
                } else if (tag.equals("deliveryReport")) {
                    // parse the character data, check for duplicates
                    String deliveryReportText = parseCharacterData(tag, true);

                    Boolean deliveryReport = parseBooleanValue(tag, deliveryReportText);

                    if (operation instanceof SubmitRequest) {
                        ((SubmitRequest) operation).setDeliveryReport(deliveryReport);
                    } else {
                        throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                                "The [deliveryReport] element is not supported for this operation type",
                                this.operation);
                    }
                } else if (tag.equals("sourceAddress")) {
                    // parse the character data, check for duplicates
                    String addrString = parseCharacterData(tag, true);

                    // a type attribute is required
                    String addrTypeString = getRequiredAttributeValue(tag, currentAttrs, "type");

                    try {
                        // parse and create address
                        MobileAddress srcAddr = MobileAddressUtil.parseAddress(addrTypeString, addrString);

                        if (operation instanceof MessageRequest) {
                            ((MessageRequest) operation).setSourceAddress(srcAddr);
                        } else {
                            throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                                    "The [sourceAddress] element is not supported for this operation type",
                                    this.operation);
                        }
                    } catch (SxmpErrorException e) {
                        throw new SxmpParsingException(e.getErrorCode(), e.getErrorMessage(), this.operation);
                    }
                } else if (tag.equals("destinationAddress")) {
                    // parse the character data, check for duplicates
                    String addrString = parseCharacterData(tag, true);

                    // a type attribute is required
                    String addrTypeString = getRequiredAttributeValue(tag, currentAttrs, "type");

                    try {
                        // dest address is XML-escaped for push
                        if (MobileAddressUtil.parseType(addrTypeString) == MobileAddress.Type.PUSH_DESTINATION) {
                            // TODO: CHANGE TO STRINGUTIL.UNESCAPE MAKE FCN
                            addrString = StringEscapeUtils.unescapeXml(addrString);
                        }
                        // parse and create address
                        MobileAddress destAddr = MobileAddressUtil.parseAddress(addrTypeString, addrString);

                        if (operation instanceof MessageRequest) {
                            ((MessageRequest) operation).setDestinationAddress(destAddr);
                        } else {
                            throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                                    "The [destinationAddress] element is not supported for this operation type",
                                    this.operation);
                        }
                    } catch (SxmpErrorException e) {
                        throw new SxmpParsingException(e.getErrorCode(), e.getErrorMessage(), this.operation);
                    }
                } else if (tag.equals("text")) {
                    // parse the character data, check for duplicates
                    // NOTE: the text element can contain no chars (empty message)
                    String encodedText = parseCharacterData(tag, false);

                    // an encoding MUST be included
                    String encoding = getRequiredAttributeValue(tag, currentAttrs, "encoding");

                    TextEncoding te = TextEncoding.valueOfCharset(encoding);

                    // if no encoding was found, then this is not supported
                    if (te == null) {
                        throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_TEXT_ENCODING,
                                "Unsupported text encoding [" + encoding + "] found", this.operation);
                    }

                    // try to convert hex encoding
                    String text = null;
                    try {
                        if (logger.isTraceEnabled()) {
                            logger.trace("textBeforeDecoding: [" + encodedText + "]");
                        }

                        byte[] bytes = HexUtil.toByteArray(encodedText);

                        // convert these bytes into a string using the provided charset
                        text = new String(bytes, te.getCharset());

                        if (logger.isTraceEnabled()) {
                            logger.debug("textAfterDecoding: [" + text + "]");
                        }
                    } catch (Exception e) {
                        throw new SxmpParsingException(SxmpErrorCode.TEXT_HEX_DECODING_FAILED,
                                "Unable to decode hex data into text", this.operation);
                    }

                    if (operation instanceof MessageRequest) {
                        ((MessageRequest) operation).setText(text, te);
                    } else {
                        throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                                "The [text] element is not supported for this operation type", this.operation);
                    }
                } else if (tag.equals("optionalParams") && version.equals(VERSION_1_1)) {
                    String encodedText = parseCharacterData(tag, false);

                    // optionalParams are XML-escaped
                    encodedText = StringEscapeUtils.unescapeXml(encodedText);

                    if (!StringUtils.isBlank(encodedText)) {
                        //logger.debug("*** Encoded text: '"+encodedText+"'");
                        try {
                            JSONObject jsonObj = new JSONObject(encodedText);
                            OptionalParamMap optionalParams = new OptionalParamMap(OptionalParamMap.HASH_MAP);
                            Iterator<String> nameItr = jsonObj.keys();
                            while (nameItr.hasNext()) {
                                String name = nameItr.next();
                                Object o = jsonObj.get(name);
                                optionalParams.put(name, o);
                            }
                            ((MessageRequest) operation).setOptionalParams(optionalParams);

                        } catch (JSONException e) {
                            logger.warn("", e);
                            throw new SxmpParsingException(SxmpErrorCode.UNABLE_TO_CONVERT_VALUE,
                                    "Unable to decode json data", this.operation);
                        } catch (IllegalArgumentException e) {
                            throw new SxmpParsingException(SxmpErrorCode.INVALID_VALUE,
                                    "Invalid optional parameters", this.operation);
                        }
                    }
                } else {
                    throw new SxmpParsingException(SxmpErrorCode.UNSUPPORTED_ELEMENT,
                            "Unsupported [" + tag + "] element found at depth [" + depth + "]", this.operation);
                }
            }

            // always decrement our depth
            depth--;
        }

        @Override
        public void ignorableWhitespace(char buf[], int offset, int len) throws SAXException {
            // do nothing
            logger.debug("ignorable whitespace included!");
        }

        @Override
        public void characters(char buf[], int offset, int len) throws SAXException {
            if (buf == null || buf.length <= 0) {
                // do nothing
                return;
            }

            // convert to string
            String text = new String(buf, offset, len);

            // now, only set a text value if its not empty
            if (text != null && !text.isEmpty()) {
                charBuffer.append(text);
            }
        }

        @Override
        public void warning(SAXParseException ex) {
            logger.warn("", ex);
            logger.warn("WARNING @ " + getLocationString(ex) + " : " + ex.toString());
        }

        @Override
        public void error(SAXParseException ex) throws SAXException {
            // Save error and continue to report other errors
            if (error == null)
                error = ex;
            //logger.debug(ex);
            logger.error("ERROR @ " + getLocationString(ex) + " : " + ex.toString());
        }

        @Override
        public void fatalError(SAXParseException ex) throws SAXException {
            error = ex;
            //logger.debug(ex);
            logger.error("FATAL @ " + getLocationString(ex) + " : " + ex.toString());
            throw ex;
        }

        private String getLocationString(SAXParseException ex) {
            return ex.getSystemId() + " line:" + ex.getLineNumber() + " col:" + ex.getColumnNumber();
        }

        @Override
        public InputSource resolveEntity(String pid, String sid) {
            //logger.debug("resolveEntity(" + pid + ", " + sid + ")");
            /**
            if (logger.isDebugEnabled())
            logger.debug("resolveEntity(" + pid + ", " + sid + ")");
                
            if (sid!=null && sid.endsWith(".dtd"))
            _dtd=sid;
                
            URL entity = null;
            if (pid != null)
            entity = (URL) _redirectMap.get(pid);
            if (entity == null)
            entity = (URL) _redirectMap.get(sid);
            if (entity == null)
            {
            String dtd = sid;
            if (dtd.lastIndexOf('/') >= 0)
                dtd = dtd.substring(dtd.lastIndexOf('/') + 1);
                
            if (logger.isDebugEnabled())
                logger.debug("Can't exact match entity in redirect map, trying " + dtd);
            entity = (URL) _redirectMap.get(dtd);
            }
                
            if (entity != null)
            {
            try
            {
                InputStream in = entity.openStream();
                if (logger.isDebugEnabled())
                    logger.debug("Redirected entity " + sid + " --> " + entity);
                InputSource is = new InputSource(in);
                is.setSystemId(sid);
                return is;
            }
            catch (IOException e)
            {
                logger.warn(e);
            }
            }
                
             */
            return null;
        }
    }

}