com.prowidesoftware.swift.io.parser.MxParser.java Source code

Java tutorial

Introduction

Here is the source code for com.prowidesoftware.swift.io.parser.MxParser.java

Source

/*******************************************************************************
 * Copyright (c) 2016 Prowide Inc.
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as 
 *     published by the Free Software Foundation, either version 3 of the 
 *     License, or (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
 *     
 *     Check the LGPL at <http://www.gnu.org/licenses/> for more details.
 *******************************************************************************/
package com.prowidesoftware.swift.io.parser;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.logging.Level;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;

import com.prowidesoftware.swift.DeleteSchedule;
import com.prowidesoftware.swift.model.MxId;
import com.prowidesoftware.swift.model.MxNode;
import com.prowidesoftware.swift.model.mx.AbstractMX;
import com.prowidesoftware.swift.model.mx.BusinessHeader;
import com.prowidesoftware.swift.model.mx.MxPayload;
import com.prowidesoftware.swift.model.mx.MxSimpleDocument;
import com.prowidesoftware.swift.model.mx.dic.ApplicationHeader;
import com.prowidesoftware.swift.model.mx.dic.BusinessApplicationHeaderV01;
import com.prowidesoftware.swift.utils.Lib;

/**
 * Basic parser for MX messages.
 * <p>IMPORTANT: An MX message is conformed by a set of optional headers 
 * and a message payload or document with the actual specific MX message. 
 * The name of the envelope element that binds a Header to the message 
 * to which it applies is <b>implementation/network specific</b> and not
 * part of the scope of the parser.</p>
 * <br />
 * This parser has three main utilities;
 * <ol>
 * <li>The first one to <em>convert the message into an MxNode tree</em>. 
 * This is a generic structured representation of the
 * complete content that can be used to get specific items by xpath. It parses
 * the complete tree including both payload and overhead information
 * (wrappers, if any, application header and body content).</li>
 * <li>The second utility is to <em>parse the application header</em> extract of the
 * XML into a model object. Notice the application header is mainly used to identify 
 * the specific message, and two versions are supported; the legacy SWIFT 
 * {@link ApplicationHeader} and the ISO {@link BusinessApplicationHeaderV01}.</li>
 * <li>The third one is to provide convenient API to detect the specific Mx message
 * type, to analize the payload structure and to strip the document or header portions
 * from the XML in a lightweight and efficient way.</li>
 * </ol>
 * <br />
 * Notice that support for MX in Prowide Core is limited. Complete model and parser 
 * implementation for each MX message type is implemented into subclasses of 
 * {@link AbstractMX} by <a href="http://www.prowidesoftware.com/integrator.jsp">Prowide Integrator SDK</a>.
 *
 * @since 7.6
 */
public class MxParser {
    private static final java.util.logging.Logger log = java.util.logging.Logger
            .getLogger(MxParser.class.getName());
    /**
     * @since 7.8.4
     */
    public static final String HEADER_LOCALNAME = "AppHdr";
    /**
     * @since 7.8.4
     */
    public static final String DOCUMENT_LOCALNAME = "Document";

    private String buffer = null;
    private MxStructureInfo info = null;

    /**
     * @deprecated the generic constructor is discouraged, use constructor with
     *             specific source parameter
     */
    @Deprecated
    @DeleteSchedule(2016)
    public MxParser() {
        super();
    }

    /**
     * Construct a parser for a file containing a single MX message
     * @param file the file containing a single unit of a message
     * @since 7.7
     * @throws IOException if an error occurs during the read of the file
     */
    public MxParser(final File file) throws IOException {
        Validate.notNull(file);
        this.buffer = Lib.readFile(file);
    }

    /**
     * Construct a parser for a stream containing a single MX message
     * @param stream non null stream containing a single unit of message
     */
    public MxParser(final InputStream stream) throws IOException {
        this.buffer = Lib.readStream(stream);
    }

    /**
     * Creates the parser initializing its content source from the given string.
     * @since 7.7
     */
    public MxParser(final String message) {
        super();
        buffer = message;
    }

    /**
     * Initializes the parser with the given stream, and returns its parsed
     * content.
     * 
     * @deprecated initialize the parser with the stream instead an call the generic {@link #parse()} method
     * @since 7.6
     */
    @Deprecated
    @DeleteSchedule(2016)
    public MxNode parse(final InputStream stream) {
        try {
            this.buffer = Lib.readStream(stream);
            return parse();
        } catch (UnsupportedEncodingException e) {
            log.log(Level.SEVERE, "error reading stream", e);
        } catch (IOException e) {
            log.log(Level.SEVERE, "error reading stream", e);
        }
        return null;
    }

    /**
     * Initializes the parser with the given reader, and returns its parsed
     * content.
     * 
     * @deprecated initialize the parser with the reader instead an call the generic {@link #parse()} method
     * @since 7.6
     */
    @Deprecated
    @DeleteSchedule(2016)
    public MxNode parse(final Reader reader) throws IOException {
        this.buffer = Lib.readReader(reader);
        return parse();
    }

    /**
     * Non-namespace aware parse.<br />
     * Parses the complete message content into an {@link MxNode} tree structure.
     * The parser should be initialized with a valid source.
     *
     * @since 7.7
     */
    public MxNode parse() {
        Validate.notNull(buffer, "the source must be initialized");
        try {
            final javax.xml.parsers.SAXParserFactory spf = javax.xml.parsers.SAXParserFactory.newInstance();
            spf.setNamespaceAware(true);
            final javax.xml.parsers.SAXParser saxParser = spf.newSAXParser();
            final MxNodeContentHandler contentHandler = new MxNodeContentHandler();
            final org.xml.sax.XMLReader xmlReader = saxParser.getXMLReader();
            xmlReader.setContentHandler(contentHandler);
            xmlReader.parse(new org.xml.sax.InputSource(new StringReader(this.buffer)));
            return contentHandler.getRootNode();
        } catch (final Exception e) {
            log.log(Level.SEVERE, "Error parsing: ", e);
        }
        return null;
    }

    /**
     * @deprecated use {@link #stripDocument()} and {@link #stripHeader()} instead
     */
    @Deprecated
    public MxPayload payload() {
        final MxId id = detectMessage();
        log.fine("Detected message " + id);
        final MxPayload result = new MxPayload();

        result.setHeader(parseBusinessHeader());
        result.setDocument(new MxSimpleDocument());
        return result;
    }

    /**
     * Detects the type of header and parses it as a legacy SWIFT Application Header or ISO Business Application Header.
     * Uses the namespace (if present) or an heuristic based on tags names.
     * <br>
     * By default ISO Business Application Header is expected and assumed for the AppHdr tag.
     * 
     * @return parsed header or <code>null</code> if the content cannot be parsed or the header is not present in the XML
     */
    public BusinessHeader parseBusinessHeader() {
        final BusinessHeader bh = new BusinessHeader();
        final MxNode tree = parse();
        boolean isAH = false;
        if (tree != null) {
            MxNode appHdr = tree.findFirstByName(HEADER_LOCALNAME);
            if (appHdr != null) {
                final String ns = appHdr.getAttribute("xmlns");
                if (ns != null && ns.equals(BusinessHeader.NAMESPACE_AH)) {
                    isAH = true;
                } else if (appHdr.findFirstByName("From") != null) {
                    isAH = true;
                }
                if (isAH) {
                    bh.setApplicationHeader(parseApplicationHeader(tree));
                } else {
                    bh.setBusinessApplicationHeader(parseBusinessApplicationHeaderV01(tree));
                }
                return bh;
            }
        }
        return null;
    }

    /**
     * Parse the business header from an XML Element node
     * @see #parseBusinessHeader()
     * @since 7.8
     */
    public static BusinessHeader parseBusinessHeader(final org.w3c.dom.Element e) {
        org.w3c.dom.ls.DOMImplementationLS lsImpl = (org.w3c.dom.ls.DOMImplementationLS) e.getOwnerDocument()
                .getImplementation().getFeature("LS", "3.0");
        org.w3c.dom.ls.LSSerializer serializer = lsImpl.createLSSerializer();
        serializer.getDomConfig().setParameter("xml-declaration", false);
        String xml = serializer.writeToString(e);
        MxParser parser = new MxParser(xml);
        return parser.parseBusinessHeader();
    }

    /**
     * Parses the application header (SWIFT legacy) from the internal message source.
     * <br />
     * @see #parseBusinessHeader()
     * @return parsed header or <code>null</code> if the content cannot be parsed or the header is not present in the XML
     */
    public ApplicationHeader parseApplicationHeader() {
        return parseApplicationHeader(parse());
    }

    /**
     * Parses the application header (SWIFT legacy) from the parameter node.
     * <br />
     * @see #parseBusinessHeader()
     * @return parsed header or <code>null</code> if the content cannot be parsed or the header is not present in the XML
     */
    public static ApplicationHeader parseApplicationHeader(final MxNode tree) {
        return MxBusinessHeaderParser.parseApplicationHeader(tree);
    }

    /**
     * Parses the application header (ISO) from the internal message source.
     * <br />
     * @see #parseBusinessHeader()
     * @return parsed header or <code>null</code> if the content cannot be parsed or the header is not present in the XML
     */
    public BusinessApplicationHeaderV01 parseBusinessApplicationHeaderV01() {
        return parseBusinessApplicationHeaderV01(parse());
    }

    /**
     * Parses the application header (ISO) from the parameter node.
     * <br />
     * @see #parseBusinessHeader()
     * @return parsed header or <code>null</code> if the content cannot be parsed or the header is not present in the XML
     */
    public static BusinessApplicationHeaderV01 parseBusinessApplicationHeaderV01(final MxNode tree) {
        return MxBusinessHeaderParser.parseBusinessApplicationHeaderV01(tree);
    }

    /**
     * Takes an xml with an MX message and detects the specific message type
     * parsing just the namespace from the Document element. If the Document
     * element is not present, or without the namespace or if the namespace url
     * contains invalid content it will return null.
     * <br><br>
     * Example of a recognizable Document element:<br>
     * <Doc:Document xmlns:Doc="urn:swift:xsd:camt.003.001.04" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
     * <br>
     * The implementation is intended to be lightweight and efficient, based on {@link javax.xml.stream.XMLStreamReader} 
     *
     * @return id with the detected MX message type or null if it cannot be determined.
     * @since 7.7
     */
    public MxId detectMessage() {
        if (StringUtils.isBlank(this.buffer)) {
            log.log(Level.SEVERE, "cannot detect message from null or empty content");
            return null;
        }
        final javax.xml.stream.XMLInputFactory xif = javax.xml.stream.XMLInputFactory.newInstance();
        try {
            final javax.xml.stream.XMLStreamReader reader = xif
                    .createXMLStreamReader(new StringReader(this.buffer));
            while (reader.hasNext()) {
                int event = reader.next();
                if (javax.xml.stream.XMLStreamConstants.START_ELEMENT == event
                        && reader.getLocalName().equals(DOCUMENT_LOCALNAME)) {
                    if (reader.getNamespaceCount() > 0) {
                        //log.finest("ELEMENT START: " + reader.getLocalName() + " , namespace count is: " + reader.getNamespaceCount());
                        for (int nsIndex = 0; nsIndex < reader.getNamespaceCount(); nsIndex++) {
                            final String nsPrefix = StringUtils.trimToNull(reader.getNamespacePrefix(nsIndex));
                            final String elementPrefix = StringUtils.trimToNull(reader.getPrefix());
                            if (StringUtils.equals(nsPrefix, elementPrefix)) {
                                String nsId = reader.getNamespaceURI(nsIndex);
                                //log.finest("\tNamepsace prefix: " + nsPrefix + " associated with URI " + nsId);
                                return new MxId(nsId);
                            }
                        }
                    }
                }
            }
        } catch (final Exception e) {
            log.log(Level.SEVERE, "error while detecting message", e);
        }
        return null;
    }

    /**
     * Convenient API to get structure information from an MX message.
     * <br ><br>
     * This can be helpful when the actual content of an XML is unknown and 
     * some preprocessing of the XML must be done in order to parse or
     * validate its content properly.
     * <br >
     * The implementation is intended to be lightweight and efficient, based on {@link javax.xml.stream.XMLStreamReader}
     *  
     * @since 7.8.4
     */
    public MxStructureInfo analizeMessage() {
        if (this.info != null) {
            return this.info;
        }
        this.info = new MxStructureInfo();
        if (StringUtils.isBlank(this.buffer)) {
            log.log(Level.WARNING, "cannot analize message from null or empty content");
            return this.info;
        }
        final javax.xml.stream.XMLInputFactory xif = javax.xml.stream.XMLInputFactory.newInstance();
        try {
            final javax.xml.stream.XMLStreamReader reader = xif
                    .createXMLStreamReader(new StringReader(this.buffer));
            boolean first = true;
            while (reader.hasNext()) {
                int event = reader.next();
                if (javax.xml.stream.XMLStreamConstants.START_ELEMENT == event) {
                    if (reader.getLocalName().equals(DOCUMENT_LOCALNAME)) {
                        this.info.containsDocument = true;
                        this.info.documentNamespace = readNamespace(reader);
                        this.info.documentPrefix = StringUtils.trimToNull(reader.getPrefix());
                    } else if (reader.getLocalName().equals(HEADER_LOCALNAME)) {
                        this.info.containsHeader = true;
                        this.info.headerNamespace = readNamespace(reader);
                        this.info.headerPrefix = StringUtils.trimToNull(reader.getPrefix());
                    } else if (first) {
                        this.info.containsWrapper = true;
                    }
                    first = false;
                }
            }
        } catch (final Exception e) {
            log.log(Level.SEVERE, "error while analizing message: " + e.getMessage());
            info.exception = e;
        }
        return this.info;
    }

    /**
     * Gets the namespace, if any, from current position in the parameter reader
     * @since 7.8.4
     */
    private String readNamespace(final javax.xml.stream.XMLStreamReader reader) {
        if (reader.getNamespaceCount() > 0) {
            //log.finest("ELEMENT START: " + reader.getLocalName() + " , namespace count is: " + reader.getNamespaceCount());
            for (int nsIndex = 0; nsIndex < reader.getNamespaceCount(); nsIndex++) {
                final String nsPrefix = StringUtils.trimToNull(reader.getNamespacePrefix(nsIndex));
                final String elementPrefix = StringUtils.trimToNull(reader.getPrefix());
                if (StringUtils.equals(nsPrefix, elementPrefix)) {
                    //log.finest("\tNamepsace prefix: " + nsPrefix + " associated with URI " + nsId);
                    return reader.getNamespaceURI(nsIndex);
                }
            }
        }
        return null;
    }

    /**
     * Helper bean used by {@link MxParser#analizeMessage()} to return 
     * structure information from an MX message
     * 
     * @since 7.8.4
     */
    public class MxStructureInfo {
        boolean containsWrapper = false;
        boolean containsHeader = false;
        boolean containsDocument = false;
        String documentNamespace = null;
        String documentPrefix = null;
        String headerNamespace = null;
        String headerPrefix = null;
        Exception exception = null;

        public boolean containsWrapper() {
            return containsWrapper;
        }

        public boolean containsHeader() {
            return containsHeader;
        }

        public boolean containsDocument() {
            return containsDocument;
        }

        public String getDocumentNamespace() {
            return documentNamespace;
        }

        public String getDocumentPrefix() {
            return documentPrefix;
        }

        public String getHeaderNamespace() {
            return headerNamespace;
        }

        public String getHeaderPrefix() {
            return headerPrefix;
        }

        public Exception getException() {
            return exception;
        }
    }

    @Deprecated
    public MxPayload mx() {
        return null;
    }

    /**
     * Distinguished Name structure: cn=name,ou=payment,o=bank,o=swift
     * <br />
     * Example: o=spxainjj,o=swift
     * 
     * @param dn the DN element content
     * @return returns capitalized "bank", in the example SPXAINJJ
     */
    public static String getBICFromDN(final String dn) {
        for (String s : StringUtils.split(dn, ",")) {
            if (StringUtils.startsWith(s, "o=") && !StringUtils.equals(s, "o=swift")) {
                return StringUtils.upperCase(StringUtils.substringAfter(s, "o="));
            }
            /*
            else if (StringUtils.startsWith(s, "ou=") && !StringUtils.equals(s, "ou=swift")) {
               return StringUtils.upperCase(StringUtils.substringAfter(s, "ou="));
            }
            */
        }
        return null;
    }

    /**
     * Helper API to strip Document portion of message XML.
     * <br >
     * This operation is intended to be lightweight and efficient so it actually
     * does a simple substring operation on the XML using information provided
     * by the result of {@link #analizeMessage()}
     * <br >
     * This API is convenient when only the Document element of an MX message
     * is needed and the wrapper/payload structure is unknown.
     * 
     * @since 7.8.4
     * @return XML with Document element of the Mx message or null if message is blank or invalid
     */
    public String stripDocument() {
        analizeMessage();
        if (this.info.containsWrapper() || this.info.containsHeader()) {
            final String tag = this.info.getDocumentPrefix() != null
                    ? this.info.getDocumentPrefix() + ":" + MxParser.DOCUMENT_LOCALNAME
                    : MxParser.DOCUMENT_LOCALNAME;
            int beginIndex = this.buffer.indexOf("<" + tag);
            int endIndex = this.buffer.indexOf("</" + tag);
            return this.buffer.substring(beginIndex, endIndex) + "</" + tag + ">";
        } else {
            return this.buffer;
        }
    }

    /**
     * Helper API to strip AppHdr portion of message XML.
     * <br >
     * This operation is intended to be lightweight and efficient so it actually
     * does a simple substring operation on the XML using information provided
     * by the result of {@link #analizeMessage()}
     * <br >
     * This API is convenient when only the header element of an MX message
     * is needed and the wrapper/payload structure is unknown.
     * <br>
     * To gather the header already parsed into objects see {@link #parseBusinessHeader()}
     * 
     * @since 7.8.4
     * @return XML with AppHdr element of the Mx message or null if not found
     */
    public String stripHeader() {
        analizeMessage();
        if (this.info.containsHeader()) {
            final String tag = this.info.getHeaderPrefix() != null
                    ? this.info.getHeaderPrefix() + ":" + MxParser.HEADER_LOCALNAME
                    : MxParser.HEADER_LOCALNAME;
            int beginIndex = this.buffer.indexOf("<" + tag);
            int endIndex = this.buffer.indexOf("</" + tag);
            return this.buffer.substring(beginIndex, endIndex) + "</" + tag + ">";
        }
        return null;
    }
}