prowide/prowide-core

View on GitHub
src/main/java/com/prowidesoftware/swift/io/parser/XMLParser.java

Summary

Maintainability
D
2 days
Test Coverage
/*
 * Copyright 2006-2023 Prowide
 *
 * 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.
 */
package com.prowidesoftware.swift.io.parser;

import com.prowidesoftware.swift.io.writer.FINWriterVisitor;
import com.prowidesoftware.swift.model.*;
import com.prowidesoftware.swift.model.field.Field;
import com.prowidesoftware.swift.utils.SafeXmlUtils;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import javax.xml.parsers.DocumentBuilder;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * This is the main parser for WIFE's XML internal representation.<br>
 * The supported XML format is consider <i>internal</i> because it is an ad-hoc
 * defined XML structure for Swift messages, so it's not the SWIFT XML
 * Standard for FIN Messages.<br>
 * <br>
 * <p>
 * This implementation should be used by calling some of the the conversion
 * services.
 *
 * @author sebastian
 * @see com.prowidesoftware.swift.io.IConversionService
 * @since 5.0
 */
public class XMLParser {
    private static final transient java.util.logging.Logger log =
            java.util.logging.Logger.getLogger(XMLParser.class.getName());

    private static final String UNPARSEDTEXTS = "unparsedtexts";

    /**
     * Given a String containing a message in its WIFE internal XML
     * representation, returns a SwiftMessage object.
     * If there is any error during conversion this method returns null
     *
     * @param xml the string containing the XML to parse
     * @return the XML parsed into a SwiftMessage object
     * @see com.prowidesoftware.swift.io.IConversionService#getMessageFromXML(java.lang.String)
     */
    public SwiftMessage parse(final String xml) {
        Validate.notNull(xml);
        try {
            final DocumentBuilder db = SafeXmlUtils.documentBuilder();
            final Document doc = db.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
            return createMessage(doc);
        } catch (final Exception e) {
            log.log(Level.WARNING, "Error parsing XML", e);
            return null;
        }
    }

    /**
     * Helper method for XML representation parsing.<br>
     *
     * @param doc Document object containing a message in XML format
     * @return SwiftMessage object populated with the given XML message data
     */
    private SwiftMessage createMessage(final Document doc) {
        final NodeList messageNL = doc.getElementsByTagName("message");

        if (messageNL.getLength() == 1) {
            final Node message = messageNL.item(0);
            final SwiftMessage m = new SwiftMessage(false);

            final NodeList blocksNL = message.getChildNodes();
            if (log.isLoggable(Level.FINE)) {
                log.fine("blocks in message: " + blocksNL.getLength());
            }

            for (int i = 0; i < blocksNL.getLength(); i++) {
                final Node blockNode = blocksNL.item(i);
                if (log.isLoggable(Level.FINE)) {
                    log.fine("evaluating node " + blockNode.getNodeName());
                }
                if (blockNode.getNodeType() == Node.ELEMENT_NODE) {
                    final String blockName = blockNode.getNodeName();

                    if ("block1".equalsIgnoreCase(blockName)) {
                        m.setBlock1(getBlock1FromNode(blockNode));
                    } else if ("block2".equalsIgnoreCase(blockName)) {
                        m.setBlock2(getBlock2FromNode(blockNode));
                    } else if (UNPARSEDTEXTS.equalsIgnoreCase(blockName)) {
                        // unparsed texts at <message> level
                        m.setUnparsedTexts(getUnparsedTextsFromNode(blockNode));
                    } else {
                        // blocks 3, 4, 5 or user blocks
                        m.addBlock(getTagListBlockFromNode(blockNode));
                    }
                }
            } // end block list iteration
            return m;
        } else {
            throw new IllegalArgumentException("<message> tag not found");
        }
    }

    /**
     * Helper method for XML representation parsing.<br>
     * Given the &lt;block1&gt; node in the XML tree, returns the SwiftBlock1 object.
     *
     * @param blockNode Node object of the &lt;block1&gt; tag in the XML message
     * @return SwiftBlock1 object populated with the given portion of the XML message
     */
    private SwiftBlock1 getBlock1FromNode(final Node blockNode) {
        final NodeList fields = blockNode.getChildNodes();
        if (log.isLoggable(Level.FINE)) {
            log.fine(fields.getLength() + " children in <block1>");
        }

        final SwiftBlock1 b1 = new SwiftBlock1();

        for (int i = 0; i < fields.getLength(); i++) {
            final Node n = fields.item(i);
            if ("APPLICATIONID".equalsIgnoreCase(n.getNodeName())) {
                b1.setApplicationId(getText(n));
            } else if ("SERVICEID".equalsIgnoreCase(n.getNodeName())) {
                b1.setServiceId(getText(n));
            } else if ("LOGICALTERMINAL".equalsIgnoreCase(n.getNodeName())) {
                b1.setLogicalTerminal(getText(n));
            } else if ("SESSIONNUMBER".equalsIgnoreCase(n.getNodeName())) {
                b1.setSessionNumber(getText(n));
            } else if ("SEQUENCENUMBER".equalsIgnoreCase(n.getNodeName())) {
                b1.setSequenceNumber(getText(n));
            } else if (UNPARSEDTEXTS.equalsIgnoreCase(n.getNodeName())) {
                b1.setUnparsedTexts(getUnparsedTextsFromNode(n));
            }
        }

        return b1;
    }

    private String getText(final Node n) {
        String text = null;
        final Node c = n.getFirstChild();
        if (c != null) {
            if (c.getNodeType() == Node.TEXT_NODE) {
                text = c.getNodeValue();
            } else {
                log.warning("Node is not TEXT_NODE: " + c);
            }
        }
        return text;
    }

    /**
     * Helper method for XML representation parsing.<br>
     * Given the &lt;block2&gt; node in the XML tree, returns the SwiftBlock1 object.
     * The method checks for the "type" attribute in the &lt;block2&gt; tag and
     * returns a SwiftBlock2Input or SwiftBlock2Output.
     *
     * @param blockNode Node object of the &lt;block2&gt; tag in the XML message
     * @return SwiftBlock2 object populated with the given portion of the XML message
     * @see #getBlock2InputFromNode(Node)
     * @see #getBlock2OutputFromNode(Node)
     */
    private SwiftBlock2 getBlock2FromNode(final Node blockNode) {
        final String type = getNodeAttribute(blockNode, "type");

        if (type == null) {
            log.severe("atrribute 'type' was expected but not found at <block2> xml tag");
            return null;
        } else if ("input".equals(type)) {
            return getBlock2InputFromNode(blockNode);
        } else if ("output".equals(type)) {
            return getBlock2OutputFromNode(blockNode);
        } else {
            log.severe(
                    "expected 'input' or 'output' value for 'type' atribute at <block2> xml tag, and found: " + type);
            return null;
        }
    }

    /**
     * Helper method for XML representation parsing.<br>
     * Given the &lt;block2 type="input"&gt; node in the XML tree, returns the
     * SwiftBlock2Input object.
     *
     * @param blockNode Node object of the &lt;block2&gt; tag in the XML message
     * @return SwiftBlock2Input object populated with the given portion of the XML message
     */
    private SwiftBlock2Input getBlock2InputFromNode(final Node blockNode) {
        final NodeList fields = blockNode.getChildNodes();
        if (log.isLoggable(Level.FINE)) {
            log.fine(fields.getLength() + " childrens in <block2 type=\"input\">");
        }

        final SwiftBlock2Input b2 = new SwiftBlock2Input();

        for (int i = 0; i < fields.getLength(); i++) {
            final Node n = fields.item(i);
            if ("MESSAGETYPE".equalsIgnoreCase(n.getNodeName())) {
                b2.setMessageType(getText(n));
            } else if ("RECEIVERADDRESS".equalsIgnoreCase(n.getNodeName())) {
                b2.setReceiverAddress(getText(n));
            } else if ("MESSAGEPRIORITY".equalsIgnoreCase(n.getNodeName())) {
                b2.setMessagePriority(getText(n));
            } else if ("DELIVERYMONITORING".equalsIgnoreCase(n.getNodeName())) {
                b2.setDeliveryMonitoring(getText(n));
            } else if ("OBSOLESCENCEPERIOD".equalsIgnoreCase(n.getNodeName())) {
                b2.setObsolescencePeriod(getText(n));
            } else if (UNPARSEDTEXTS.equalsIgnoreCase(n.getNodeName())) {
                b2.setUnparsedTexts(getUnparsedTextsFromNode(n));
            }
        }

        return b2;
    }

    /**
     * Helper method for XML representation parsing.<br>
     * Given the &lt;block2 type="output" node in the XML tree, returns the
     * SwiftBlock2Output object.
     *
     * @param blockNode Node object of the &lt;block2&gt; tag in the XML message
     * @return SwiftBlock2Output object populated with the given portion of the XML message
     */
    private SwiftBlock2Output getBlock2OutputFromNode(final Node blockNode) {
        final NodeList fields = blockNode.getChildNodes();
        if (log.isLoggable(Level.FINE)) {
            log.fine(fields.getLength() + " childrens in <block2 type=\"output\">");
        }

        final SwiftBlock2Output b2 = new SwiftBlock2Output();

        for (int i = 0; i < fields.getLength(); i++) {
            final Node n = fields.item(i);
            if ("MESSAGETYPE".equalsIgnoreCase(n.getNodeName())) {
                b2.setMessageType(getText(n));
            } else if ("SENDERINPUTTIME".equalsIgnoreCase(n.getNodeName())) {
                b2.setSenderInputTime(getText(n));
            } else if ("MIRDATE".equalsIgnoreCase(n.getNodeName())) {
                b2.setMIRDate(getText(n));
            } else if ("MIRLOGICALTERMINAL".equalsIgnoreCase(n.getNodeName())) {
                b2.setMIRLogicalTerminal(getText(n));
            } else if ("MIRSESSIONNUMBER".equalsIgnoreCase(n.getNodeName())) {
                b2.setMIRSessionNumber(getText(n));
            } else if ("MIRSEQUENCENUMBER".equalsIgnoreCase(n.getNodeName())) {
                b2.setMIRSequenceNumber(getText(n));
            } else if ("RECEIVEROUTPUTDATE".equalsIgnoreCase(n.getNodeName())) {
                b2.setReceiverOutputDate(getText(n));
            } else if ("RECEIVEROUTPUTTIME".equalsIgnoreCase(n.getNodeName())) {
                b2.setReceiverOutputTime(getText(n));
            } else if ("MESSAGEPRIORITY".equalsIgnoreCase(n.getNodeName())) {
                b2.setMessagePriority(getText(n));
            } else if (UNPARSEDTEXTS.equalsIgnoreCase(n.getNodeName())) {
                b2.setUnparsedTexts(getUnparsedTextsFromNode(n));
            }
        }

        return b2;
    }

    /**
     * Helper method for XML representation parsing.<br>
     * Given the &lt;block3&gt;, &lt;block4&gt;, &lt;block5&gt; or &lt;block&gt; (user block) node in
     * the XML tree, returns the corresponding SwiftTagListBlock object
     * populated with the given portion of the XML message.
     *
     * @param blockNode Node object of the &lt;block3&gt;, &lt;block4&gt;, &lt;block5&gt; or &lt;block&gt; tag in the XML message
     * @return SwiftTagListBlock object populated with the given portion of the XML message
     */
    private SwiftTagListBlock getTagListBlockFromNode(final Node blockNode) {
        final String blockName = blockNode.getNodeName();
        SwiftTagListBlock b;
        if ("block3".equalsIgnoreCase(blockName)) {
            b = new SwiftBlock3();
        } else if ("block4".equalsIgnoreCase(blockName)) {
            b = new SwiftBlock4();
        } else if ("block5".equalsIgnoreCase(blockName)) {
            b = new SwiftBlock5();
        } else if ("block".equalsIgnoreCase(blockName)) {
            final String name = getNodeAttribute(blockNode, "name");
            if (name != null) {
                b = new SwiftBlockUser(name);
            } else {
                b = new SwiftBlockUser();
            }
        } else {
            return null;
        }

        final NodeList fields = blockNode.getChildNodes();
        if (log.isLoggable(Level.FINE)) {
            log.fine(fields.getLength() + " children in tag list " + blockName);
        }

        for (int j = 0; j < fields.getLength(); j++) {
            final Node t = fields.item(j);
            if ("tag".equalsIgnoreCase(t.getNodeName())) {
                final Tag tag = getTag(t);
                b.append(tag);
            } else if ("field".equalsIgnoreCase(t.getNodeName())) {
                final Field field = getField(t);
                b.append(field);
            } else if (UNPARSEDTEXTS.equalsIgnoreCase(t.getNodeName())) {
                b.setUnparsedTexts(getUnparsedTextsFromNode(t));
            }
        }

        return b;
    }

    /**
     * Helper method for XML representation parsing.<br>
     * Parses the given &lt;tag&gt; Node and returns a Tag object containing data from
     * the expected &lt;name&gt; and &lt;value&gt; tags. If name or value are not found as
     * children of the given node, the Tag object is returned with empty values.
     *
     * @param t the XML node to parse for name-value pair
     * @return a Tag object containing the name and value of the given XML node.
     */
    private Tag getTag(final Node t) {
        final Tag tag = new Tag();
        final NodeList children = t.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            final Node n = children.item(i);
            if ("name".equalsIgnoreCase(n.getNodeName())) {
                tag.setName(getText(n));
            }
            if ("value".equalsIgnoreCase(n.getNodeName())) {
                String text = getText(n);
                // normalize line feeds (DOM parser removes carriage return characters from original XML file)
                text = StringUtils.replace(text, "\n", FINWriterVisitor.SWIFT_EOL);
                tag.setValue(text);
            } else if (UNPARSEDTEXTS.equalsIgnoreCase(n.getNodeName())) {
                tag.setUnparsedTexts(getUnparsedTextsFromNode(n));
            }
        }
        return tag;
    }

    /**
     * Helper method for XML representation parsing.<br>
     * Parses the given &gt;field&lt; Node and returns a Field object containing data from
     * the expected &lt;name&gt; and &lt;component&gt; inner elements.
     * If &lt;name&gt; element is not set it will return null. Otherwise it will return a Field
     * instance filled with content from &lt;component&gt; elements.
     *
     * @param t the XML node to parse for name-value pair
     * @return a Field object or null if "name" element is not present
     */
    private Field getField(final Node t) {
        final NodeList children = t.getChildNodes();
        String name = null;
        for (int i = 0; i < children.getLength(); i++) {
            final Node n = children.item(i);
            if ("name".equalsIgnoreCase(n.getNodeName())) {
                name = getText(n);
                break;
            }
        }
        if (name != null) {
            Field field = Field.getField(name, null);
            for (int i = 0; i < children.getLength(); i++) {
                final Node n = children.item(i);
                if ("component".equalsIgnoreCase(n.getNodeName())) {
                    final String number = getNodeAttribute(n, "number");
                    if (StringUtils.isNumeric(number)) {
                        String text = getText(n);
                        // normalize line feeds (DOM parser removes carriage return characters from original XML file)
                        text = StringUtils.replace(text, "\n", FINWriterVisitor.SWIFT_EOL);
                        field.setComponent(Integer.parseInt(number), text);
                    }
                }
            }
            return field;
        }
        return null;
    }

    /**
     * Helper method for XML representation parsing.<br>
     * Given the &lt;unparsedtexts&gt; node in the XML tree, returns an
     * UnparsedTextList object populated with the contents of the <text> child
     * tags of &lt;unparsedtexts&gt;.
     *
     * @param blockNode Node object of the &lt;unparsedtexts&gt; tag in the XML message
     * @return UnparsedTextList object populated with the given &lt;text&gt; tags content of the &lt;unparsedtexts&gt;
     */
    private UnparsedTextList getUnparsedTextsFromNode(final Node blockNode) {
        final UnparsedTextList unparsedTexts = new UnparsedTextList();

        final NodeList texts = blockNode.getChildNodes();
        if (log.isLoggable(Level.FINE)) {
            log.fine(texts.getLength() + " children in <unparsedtexts>");
        }
        for (int j = 0; j < texts.getLength(); j++) {
            final Node t = texts.item(j);
            if ("text".equalsIgnoreCase(t.getNodeName())) {
                unparsedTexts.addText(getText(t));
            }
        }
        return unparsedTexts;
    }

    /**
     * Helper method for XML representation parsing.<br>
     * Gets the value of an expected attribute in a Node.
     *
     * @param n             Node to analyze to find the attribute
     * @param attributeName the attribute name expected in the analyzed Node n
     * @return the value of the attribute expected, or null if the attribute was not found
     */
    private String getNodeAttribute(final Node n, final String attributeName) {
        final Node attr = n.getAttributes().getNamedItem(attributeName);
        if (attr == null || !attr.getNodeName().equals(attributeName)) {
            return null;
        } else {
            return attr.getNodeValue();
        }
    }
}