All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.solab.iso8583.parse.ConfigParser Maven / Gradle / Ivy

Go to download

Java implementation of the ISO 8583 protocol, focused on making the creation, edition and reading of ISO8583 messages as simple and flexible as possible.

There is a newer version: 1.22.5
Show newest version
/*
j8583 A Java implementation of the ISO8583 protocol
Copyright (C) 2007 Enrique Zamudio Lopez

This library 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 library 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.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
*/
package com.solab.iso8583.parse;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.TimeZone;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import com.solab.iso8583.*;
import com.solab.iso8583.codecs.CompositeField;
import com.solab.iso8583.util.HexCodec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import static com.solab.iso8583.IsoMessage.MAX_AMOUNT_OF_FIELDS;

/**
 * This class is used to parse a XML configuration file and configure
 * a MessageFactory with the values from it.
 *
 * @author Enrique Zamudio
 */
public class ConfigParser {

	private static final Logger log = LoggerFactory.getLogger(ConfigParser.class);

    /**
     * Creates a message factory configured from the default file, which is j8583.xml
     * located in the root of the classpath, using the specified ClassLoader.  
     *
     * @param loader the loader
     * @return the message factory
     * @throws IOException the io exception
     */
    public static MessageFactory createDefault(
               ClassLoader loader) throws IOException {
   		if (loader.getResource("j8583.xml") == null) {
   			log.warn("ISO8583 ConfigParser cannot find j8583.xml, returning empty message factory");
   			return new MessageFactory<>();
   		} else {
   			return createFromClasspathConfig(loader, "j8583.xml");
   		}
   	}

    /**
     * Creates a message factory configured from the default file, which is j8583.xml
     * located in the root of the classpath, using the MessageFactory's
     * ClassLoader.  
     *
     * @return the message factory
     * @throws IOException the io exception
     */
    public static MessageFactory createDefault() throws IOException {
        return createDefault(MessageFactory.class.getClassLoader());
	}

    /**
     * Creates a message factory from the specified path inside the classpath,
     * using the specified ClassLoader.  
     *
     * @param path the path
     * @return the message factory
     * @throws IOException the io exception
     */
    public static MessageFactory createFromClasspathConfig(
            String path) throws IOException {
        return createFromClasspathConfig(MessageFactory.class.getClassLoader(), path);
    }

    /**
     * Creates a message factory from the specified path inside the classpath,
     * using MessageFactory's ClassLoader.  
     *
     * @param loader the loader
     * @param path the path
     * @return the message factory
     * @throws IOException the io exception
     */
    public static MessageFactory createFromClasspathConfig(
               ClassLoader loader, String path) throws IOException {
		MessageFactory mfact = new MessageFactory<>();
        try (InputStream ins = loader.getResourceAsStream(path)) {
            if (ins != null) {
                log.debug("ISO8583 Parsing config from classpath file {}", path);
                parse(mfact, new InputSource(ins));
            } else {
                log.error("ISO8583 File not found in classpath: {}", path);
            }
        }
		return mfact;
	}

    /**
     * Creates a message factory from the file located at the specified URL.  
     *
     * @param url the url
     * @return the message factory
     * @throws IOException the io exception
     */
    public static MessageFactory createFromUrl(URL url) throws IOException {
		MessageFactory mfact = new MessageFactory<>();
		try (InputStream stream = url.openStream()) {
			parse(mfact, new InputSource(stream));
		}
		return mfact;
	}

    /**
     * Creates a messageFactory from the XML contained in the specified Reader.  
     *
     * @param reader the reader
     * @return the message factory
     * @throws IOException the io exception
     */
    public static MessageFactory createFromReader(Reader reader) throws IOException {
        MessageFactory mfact = new MessageFactory<>();
        parse(mfact, new InputSource(reader));
        return mfact;
    }

    /**
     * Parse headers.
     *
     * @param    the type parameter
     * @param nodes the nodes
     * @param mfact the mfact
     * @throws IOException the io exception
     */
    protected static  void parseHeaders(
            final NodeList nodes, final MessageFactory mfact) throws IOException {
        ArrayList refs = null;
        for (int i = 0; i < nodes.getLength(); i++) {
            Element elem = (Element)nodes.item(i);
            int type = parseType(elem.getAttribute("type"));
            if (type == -1) {
                throw new IOException("Invalid type for ISO8583 header: " + elem.getAttribute("type"));
            }
            if (elem.getChildNodes() == null || elem.getChildNodes().getLength() == 0) {
                if (elem.getAttribute("ref") != null && !elem.getAttribute("ref").isEmpty()) {
                    if (refs == null) {
                        refs = new ArrayList<>(nodes.getLength()-i);
                    }
                    refs.add(elem);
                }
                else throw new IOException("Invalid ISO8583 header element");
            } else {
                String header = elem.getChildNodes().item(0).getNodeValue();
                boolean binHeader = "true".equals(elem.getAttribute("binary"));
                if (log.isTraceEnabled()) {
                    log.trace("Adding {}ISO8583 header for type {}: {}",
                            binHeader?"binary ":"", elem.getAttribute("type"), header);
                }
                if (binHeader) {
                    mfact.setBinaryIsoHeader(type, HexCodec.hexDecode(header));
                } else {
                    mfact.setIsoHeader(type, header);
                }
            }
        }
        if (refs != null) {
            for (Element elem : refs) {
                int type = parseType(elem.getAttribute("type"));
                if (type == -1) {
                    throw new IOException("Invalid type for ISO8583 header: "
                            + elem.getAttribute("type"));
                }
                if (elem.getAttribute("ref") != null && !elem.getAttribute("ref").isEmpty()) {
                    int t2 = parseType(elem.getAttribute("ref"));
                    if (t2 == -1) {
                        throw new IOException("Invalid type reference "
                                + elem.getAttribute("ref") + " for ISO8583 header " + type);
                    }
                    String h = mfact.getIsoHeader(t2);
                    if (h == null) {
                        throw new IllegalArgumentException("Header def " + type +
                                " refers to nonexistent header " + t2);
                    }
                    if (log.isTraceEnabled()) {
                        log.trace("Adding ISO8583 header for type {}: {} (copied from {})",
                                elem.getAttribute("type"), h, elem.getAttribute("ref"));
                    }
                    mfact.setIsoHeader(type, h);
                }
            }
        }
    }

    /**
     * Parse templates.
     *
     * @param    the type parameter
     * @param nodes the nodes
     * @param mfact the mfact
     * @throws IOException the io exception
     */
    protected static  void parseTemplates(
            final NodeList nodes, final MessageFactory mfact) throws IOException {
        ArrayList subs = null;
        for (int i = 0; i < nodes.getLength(); i++) {
            Element elem = (Element)nodes.item(i);
            int type = parseType(elem.getAttribute("type"));
            if (type == -1) {
                throw new IOException("Invalid ISO8583 type for template: " + elem.getAttribute("type"));
            }
            if (elem.getAttribute("extends") != null && !elem.getAttribute("extends").isEmpty()) {
                if (subs == null) {
                    subs = new ArrayList<>(nodes.getLength()-i);
                }
                subs.add(elem);
                continue;
            }
            @SuppressWarnings("unchecked")
            T m = (T)new IsoMessage();
            m.setType(type);
            m.setCharacterEncoding(mfact.getCharacterEncoding());
            NodeList fields = elem.getElementsByTagName("field");
            for (int j = 0; j < fields.getLength(); j++) {
                Element f = (Element)fields.item(j);
                if (f.getParentNode()==elem) {
                    final int num = Integer.parseInt(f.getAttribute("num"));
                    IsoValue v = getTemplateField(f, mfact, true);
                    if (v != null) {
                        v.setCharacterEncoding(mfact.getCharacterEncoding());
                    }
                    m.setField(num, v);
                }
            }
            mfact.addMessageTemplate(m);
        }
        if (subs != null) {
            for (Element elem : subs) {
                int type = parseType(elem.getAttribute("type"));
                int ref = parseType(elem.getAttribute("extends"));
                if (ref == -1) {
                    throw new IllegalArgumentException("Message template "
                            + elem.getAttribute("type") + " extends invalid template "
                            + elem.getAttribute("extends"));
                }
                IsoMessage tref = mfact.getMessageTemplate(ref);
                if (tref == null) {
                    throw new IllegalArgumentException("Message template "
                            + elem.getAttribute("type") + " extends nonexistent template "
                            + elem.getAttribute("extends"));
                }
                @SuppressWarnings("unchecked")
                T m = (T)new IsoMessage();
                m.setType(type);
                m.setCharacterEncoding(mfact.getCharacterEncoding());
                for (int i = 2; i <= MAX_AMOUNT_OF_FIELDS; i++) {
                    if (tref.hasField(i)) {
                        m.setField(i, new IsoValue<>(tref.getField(i)));
                    }
                }
                NodeList fields = elem.getElementsByTagName("field");
                for (int j = 0; j < fields.getLength(); j++) {
                    Element f = (Element)fields.item(j);
                    int num = Integer.parseInt(f.getAttribute("num"));
                    if (f.getParentNode()==elem) {
                        IsoValue v = getTemplateField(f, mfact, true);
                        if (v != null) {
                            v.setCharacterEncoding(mfact.getCharacterEncoding());
                        }
                        m.setField(num, v);
                    }
                }
                mfact.addMessageTemplate(m);
            }
        }
    }

    /**
     * Creates an IsoValue from the XML definition in a message template.
     * If it's for a toplevel field and the message factory has a codec for this field,
     * that codec is assigned to that field. For nested fields, a CompositeField is
     * created and populated.
     *
     * @param   the type parameter
     * @param f        the f
     * @param mfact    the mfact
     * @param toplevel the toplevel
     * @return the template field
     */
    protected static  IsoValue getTemplateField(
            Element f, MessageFactory mfact, boolean toplevel) {
        final int num = Integer.parseInt(f.getAttribute("num"));
        final String typedef = f.getAttribute("type");
        if ("exclude".equals(typedef)) {
            return null;
        }
        int length = 0;
        if (f.getAttribute("length").length() > 0) {
            length = Integer.parseInt(f.getAttribute("length"));
        }
        final IsoType itype = IsoType.valueOf(typedef);
        final NodeList subs = f.getElementsByTagName("field");
        if (subs != null && subs.getLength() > 0) {
            //Composite field
            final CompositeField cf = new CompositeField();
            for (int j = 0; j < subs.getLength(); j++) {
                Element sub = (Element)subs.item(j);
                if (sub.getParentNode()==f) {
                    IsoValue sv = getTemplateField(sub, mfact, false);
                    if (sv != null) {
                        sv.setCharacterEncoding(mfact.getCharacterEncoding());
                        cf.addValue(sv);
                    }
                }
            }
            IsoValue rv = itype.needsLength() ? new IsoValue<>(itype, cf, length, cf) :
                    new IsoValue<>(itype, cf, cf);
            if (f.hasAttribute("tz")) {
                TimeZone tz = TimeZone.getTimeZone(f.getAttribute("tz"));
                rv.setTimeZone(tz);
            }
            return rv;
        }
        final String v;
        if (f.getChildNodes().getLength() == 0) {
            v = "";
        } else {
            v = f.getChildNodes().item(0).getNodeValue();
        }
        final CustomField cf = toplevel ? mfact.getCustomField(num) : null;
        IsoValue rv;
        if (cf == null) {
            rv = itype.needsLength() ? new IsoValue<>(itype, v, length) : new IsoValue<>(itype, v);
        } else {
            rv = itype.needsLength() ? new IsoValue<>(itype, cf.decodeField(v), length, cf) :
                    new IsoValue<>(itype, cf.decodeField(v), cf);
        }
        if (f.hasAttribute("tz")) {
            TimeZone tz = TimeZone.getTimeZone(f.getAttribute("tz"));
            rv.setTimeZone(tz);
        }
        return rv;
    }

    /**
     * Gets parser.
     *
     * @param    the type parameter
     * @param f     the f
     * @param mfact the mfact
     * @return the parser
     */
    protected static  FieldParseInfo getParser(
            Element f, MessageFactory mfact) {
        IsoType itype = IsoType.valueOf(f.getAttribute("type"));
        int length = 0;
        if (f.getAttribute("length").length() > 0) {
            length = Integer.parseInt(f.getAttribute("length"));
        }
        FieldParseInfo fpi = FieldParseInfo.getInstance(itype, length, mfact.getCharacterEncoding());
        NodeList subs = f.getElementsByTagName("field");
        if (subs != null && subs.getLength() > 0) {
            final CompositeField combo = new CompositeField();
            for (int i=0; i   the type parameter
     * @param nodes the nodes
     * @param mfact the mfact
     * @throws IOException the io exception
     */
    protected static  void parseGuides(
            final NodeList nodes, final MessageFactory mfact) throws IOException {
        ArrayList subs = null;
        HashMap> guides = new HashMap<>();
        for (int i = 0; i < nodes.getLength(); i++) {
            Element elem = (Element)nodes.item(i);
            int type = parseType(elem.getAttribute("type"));
            if (type == -1) {
                throw new IOException("Invalid ISO8583 type for parse guide: " + elem.getAttribute("type"));
            }
            if (elem.getAttribute("extends") != null && !elem.getAttribute("extends").isEmpty()) {
                if (subs == null) {
                    subs = new ArrayList<>(nodes.getLength()-i);
                }
                subs.add(elem);
                continue;
            }
            HashMap parseMap = new HashMap<>();
            NodeList fields = elem.getElementsByTagName("field");
            for (int j = 0; j < fields.getLength(); j++) {
                Element f = (Element)fields.item(j);
                if (f.getParentNode()==elem) {
                    int num = Integer.parseInt(f.getAttribute("num"));
                    parseMap.put(num, getParser(f, mfact));
                }
            }
            mfact.setParseMap(type, parseMap);
            guides.put(type, parseMap);
        }
        if (subs != null) {
            for (Element elem : subs) {
                int type = parseType(elem.getAttribute("type"));
                int ref = parseType(elem.getAttribute("extends"));
                if (ref == -1) {
                    throw new IllegalArgumentException("Message template "
                            + elem.getAttribute("type") + " extends invalid template "
                            + elem.getAttribute("extends"));
                }
                HashMap parent = guides.get(ref);
                if (parent == null) {
                    throw new IllegalArgumentException("Parsing guide "
                            + elem.getAttribute("type") + " extends nonexistent guide "
                            + elem.getAttribute("extends"));
                }
                HashMap child = new HashMap<>();
                child.putAll(parent);
                List fields = getDirectChildrenByTagName(elem, "field");
                for (Element f : fields) {
                    int num = Integer.parseInt(f.getAttribute("num"));
                    String typedef = f.getAttribute("type");
                    if ("exclude".equals(typedef)) {
                        child.remove(num);
                    } else {
                        child.put(num, getParser(f, mfact));
                    }
                }
                mfact.setParseMap(type, child);
                guides.put(type, child);
            }
        }
    }
    
	private static List getDirectChildrenByTagName(Element elem, String tagName) {
		List childElementsByTagName = new ArrayList();
		NodeList childNodes = elem.getChildNodes();
		for (int i = 0; i < childNodes.getLength(); i++) {
			if (childNodes.item(i).getNodeType() == Node.ELEMENT_NODE) {
				Element childElem = (Element)childNodes.item(i);
				if (childElem.getTagName().equals(tagName)) {
					childElementsByTagName.add(childElem);
				}
			}
		}
		return childElementsByTagName;
	}

    /**
     * Reads the XML from the stream and configures the message factory with its values.
     *
     * @param     the type parameter
     * @param mfact  The message factory to be configured with the values read from the XML.
     * @param source The InputSource containing the XML configuration.
     * @throws IOException the io exception
     */
    protected static  void parse(
            MessageFactory mfact, InputSource source) throws IOException {
		final DocumentBuilderFactory docfact = DocumentBuilderFactory.newInstance();
		DocumentBuilder docb = null;
		Document doc = null;
		try {
			docb = docfact.newDocumentBuilder();
			docb.setEntityResolver(new EntityResolver() {
				@Override
				public InputSource resolveEntity(String publicId, String systemId)
						throws SAXException, IOException {
					if (systemId.contains("j8583.dtd")) {
						URL dtd = getClass().getResource("j8583.dtd");
						if (dtd == null) {
							log.warn("Cannot find j8583.dtd in classpath. j8583 config files will not be validated.");
						} else {
							return new InputSource(dtd.toString());
						}
					}
					return null;
				}
			});
			doc = docb.parse(source);
		} catch (ParserConfigurationException | SAXException ex) {
			log.error("ISO8583 Cannot parse XML configuration", ex);
			return;
		}
		final Element root = doc.getDocumentElement();

        parseHeaders(root.getElementsByTagName("header"), mfact);
        parseTemplates(root.getElementsByTagName("template"), mfact);
		//Read the parsing guides
        parseGuides(root.getElementsByTagName("parse"), mfact);
	}

    /**
     * Configures a MessageFactory using the default configuration file j8583.xml. This is useful
     * if you have a MessageFactory created using Spring for example.
     *
     * @param   the type parameter
     * @param mfact the mfact
     * @throws IOException the io exception
     */
    public static  void configureFromDefault(
            MessageFactory mfact) throws IOException {
		if (mfact.getClass().getClassLoader().getResource("j8583.xml") == null) {
			log.warn("ISO8583 config file j8583.xml not found!");
		} else {
			configureFromClasspathConfig(mfact, "j8583.xml");
		}
	}

    /**
     * This method attempts to open a stream from the XML configuration in the specified URL and
     * configure the message factory from that config.
     *
     * @param   the type parameter
     * @param mfact the mfact
     * @param url   the url
     * @throws IOException the io exception
     */
    public static  void configureFromUrl(
            MessageFactory mfact, URL url) throws IOException {
		try (InputStream stream = url.openStream()) {
			parse(mfact, new InputSource(stream));
		}
	}

    /**
     * Configures a MessageFactory using the configuration file at the path specified (will be searched
     * within the classpath using the MessageFactory's ClassLoader). This is useful for configuring
     * Spring-bound instances of MessageFactory for example.
     *
     * @param   the type parameter
     * @param mfact the mfact
     * @param path  the path
     * @throws IOException the io exception
     */
    public static  void configureFromClasspathConfig(
            MessageFactory mfact, String path) throws IOException {
        try (InputStream ins = mfact.getClass().getClassLoader().getResourceAsStream(path)) {
            if (ins != null) {
                log.debug("ISO8583 Parsing config from classpath file {}", path);
                parse(mfact, new InputSource(ins));
            } else {
                log.warn("ISO8583 File not found in classpath: {}", path);
            }
        }
	}

    /**
     * Configures a MessageFactory using the XML data obtained from the specified Reader.
     *
     * @param   the type parameter
     * @param mfact  the mfact
     * @param reader the reader
     * @throws IOException the io exception
     */
    public static  void configureFromReader(MessageFactory mfact, Reader reader)
            throws IOException {
        parse(mfact, new InputSource(reader));
    }

	/** Parses a message type expressed as a hex string and returns the integer number.
	 * For example, "0200" or "200" return the number 512 (0x200) */
	private static int parseType(String type) throws IOException {
		if (type.length() % 2 == 1) {
			type = "0" + type;
		}
		if (type.length() != 4) {
			return -1;
		}
		return ((type.charAt(0) - 48) << 12) | ((type.charAt(1) - 48) << 8)
			| ((type.charAt(2) - 48) << 4) | (type.charAt(3) - 48);
	}

}