Package org.jpos.util

Source Code of org.jpos.util.FSDMsg

/*
* jPOS Project [http://jpos.org]
* Copyright (C) 2000-2014 Alejandro P. Revilla
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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.  See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.jpos.util;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jpos.iso.ISOException;
import org.jpos.iso.ISOUtil;
import org.jpos.space.Space;
import org.jpos.space.SpaceFactory;

/**
* General purpose, Field Separator delimited message.
*
* <h1>How to use</h1>
* <p>
* The message format (or schema) is defined in xml files containing a schema element, with an optional id attribute, and multiple
* field elements. A field element is made up of the following attributes:
* <dl>
* <dt>id</dt>
* <dd>The name of the field. This is used in calls to {@link FSDMsg#set(String, String)}. It should be unique amongst the fields in an FSDMsg.</dd>
* <dt>length</dt>
* <dd>The maximum length of the data allowed in this field. Fixed length fields will be padded to this length. A zero length is allowed, and can
* be useful to define extra separator characters in the message.</dd>
* <dt>type</dt>
* <dd>The type of the included data, including an optional separator for marking the end of the field and the beginning of the next one. The data type
* is defined by the first char of the type, and the separator is defined by the following chars. If a field separator is specified, then no
* padding is done on values for this field.
* </dd>
* <dt>key</dt>
* <dd>If this optional attribute has a value of "true", then fields from another schema, specified by the value, are appended to this schema.</dd>
* <dt>separator</dt>
* <dd>An optional attribute containing the separator for the field. This is the preferred method of specifying the separator. See the list of optional</dd>
* </dl>
* <p>
* Possible types are:
* <dl>
* <dt>A</dt><dd>Alphanumeric. Padding if any is done with spaces to the right.</dd>
* <dt>B</dt><dd>Binary. Padding, if any, is done with zeros to the left.</dd>
* <dt>K</dt><dd>Constant. The value is specified by the field content. No padding is done.</dd>
* <dt>N</dt><dd>Numeric. Padding, if any, is done with zeros to the left.</dd>
* </dl>
* </p>
* <p>
* Supported field separators are:
* <dl>
* <dt>FS</dt><dd>Field separator using '034' as the separator.</dd>
* <dt>US</dt><dd>Field separator using '037' as the separator.</dd>
* <dt>GS</dt><dd>Group separator using '035' as the separator.</dd>
* <dt>RS</dt><dd>Row separator using '036' as the separator.</dd>
* <dt>PIPE</dt><dd>Field separator using '|' as the separator.</dd>
* <dt>EOF</dt><dd>End of File - no separator character is emitted, but also no padding is done. Also if the end of file is reached
* parsing a message, then no exception is thrown.</dd>
* <dt>DS</dt><dd>A dummy separator. This is similar to EOF, but the message stream must not end before it is allowed.</dd>
* <dt>EOM</dt><dd>End of message separator. This reads all bytes available in the stream. 
* </dl>
* </p>
* <p>
* Key fields allow you to specify a tree of possible message formats. The key fields are the fork points of the tree.
* Multiple key fields are supported. It is also possible to have more key fields specified in appended schemas.
* </p>
*
* @author Alejandro Revila
* @author Mark Salter
* @author Dave Bergert
* @since 1.4.7
*/
@SuppressWarnings("unchecked")
public class FSDMsg implements Loggeable, Cloneable {
    public static char FS = '\034';
    public static char US = '\037';
    public static char GS = '\035';
    public static char RS = '\036';
    public static char EOF = '\000';
    public static char PIPE = '\u007C';
    public static char EOM = '\000';
   
    private static final Set<String> DUMMY_SEPARATORS = new HashSet<String>(Arrays.asList("DS", "EOM"));
    private static final String EOM_SEPARATOR = "EOM";
    private static final int READ_BUFFER = 8192;
   
    Map<String,String> fields;
    Map<String, Character> separators;

    String baseSchema;
    String basePath;
    byte[] header;
    Charset charset;
    int readCount;

    /**
     * Creates a FSDMsg with a specific base path for the message format schema.
     * @param basePath   schema path, for example: "file:src/data/NDC-" looks for a file src/data/NDC-base.xml
     */
    public FSDMsg (String basePath) {
        this (basePath, "base");
    }
   
    /**
     * Creates a FSDMsg with a specific base path for the message format schema, and a base schema name. For instance,
     * FSDMsg("file:src/data/NDC-", "root") will look for a file: src/data/NDC-root.xml
     * @param basePath   schema path
     * @param baseSchema schema name
     */
    public FSDMsg (String basePath, String baseSchema) {
        super();
        fields = new LinkedHashMap<String,String>();
        separators = new LinkedHashMap<String,Character>();
        this.basePath   = basePath;
        this.baseSchema = baseSchema;
        charset = ISOUtil.CHARSET;
        readCount = 0;
       
        setSeparator("FS", FS);
        setSeparator("US", US);
        setSeparator("GS", GS);
        setSeparator("RS", RS);
        setSeparator("EOF", EOF);
        setSeparator("PIPE", PIPE);
    }
    public String getBasePath() {
        return basePath;
    }
    public String getBaseSchema() {
        return baseSchema;
    }
  
    public void setCharset(Charset charset) {
        this.charset = charset;
    }

    /*
     * add a new or override an existing separator type/char pair.
     *
     *  @param separatorName   string of type used in definition (FS, US etc)
     *  @param separator       char representing type
     */
    public void setSeparator(String separatorName, char separator) {
        separators.put(separatorName, separator);
    }
   
    /*
     * add a new or override an existing separator type/char pair.
     *
     *  @param separatorName   string of type used in definition (FS, US etc)
     *  @param separator       char representing type
     */
    public void unsetSeparator(String separatorName) {
        if (!separators.containsKey(separatorName))
            throw new IllegalArgumentException("unsetSeparator was attempted for "+
                      separatorName+" which was not previously defined.");

        separators.remove(separatorName);
    }
   
    /**
     * parse message. If the stream ends before the message is completely read, then the method adds an EOF field.
     *
     * @param is input stream
     *
     * @throws IOException
     * @throws JDOMException
     */
    public void unpack (InputStream is)
        throws IOException, JDOMException {
        try {
            if (is.markSupported())
                is.mark(READ_BUFFER);
            unpack (new InputStreamReader(is, charset), getSchema (baseSchema));
            if (is.markSupported()) {
                is.reset();
                is.skip (readCount);
                readCount = 0;
            }
        } catch (EOFException e) {
            fields.put ("EOF", "true");
        }
    }
    /**
     * parse message. If the stream ends before the message is completely read, then the method adds an EOF field.
     *
     * @param b message image
     *
     * @throws IOException
     * @throws JDOMException
     */
    public void unpack (byte[] b)
        throws IOException, JDOMException {
        unpack (new ByteArrayInputStream (b));
    }

    /**
     * @return message string
     * @throws org.jdom.JDOMException
     * @throws java.io.IOException
     * @throws ISOException
     */
    public String pack ()
        throws JDOMException, IOException, ISOException
    {
        StringBuilder sb = new StringBuilder ();
        pack (getSchema (baseSchema), sb);
        return sb.toString ();
    }
    public byte[] packToBytes ()
        throws JDOMException, IOException, ISOException
    {
        return pack().getBytes(charset);
    }

    protected String get (String id, String type, int length, String defValue, String separator)
        throws ISOException
    {
        String value = fields.get (id);
        if (value == null)
            value = defValue == null ? "" : defValue;

        type   = type.toUpperCase ();

        switch (type.charAt (0)) {
            case 'N':
                if (!isSeparated(separator)) {
                    value = ISOUtil.zeropad (value, length);
                } // else Leave value unpadded.
                break;
            case 'A':
                if (!isSeparated(separator)) {
                    value = ISOUtil.strpad (value, length);
                } // else Leave value unpadded.
                if (value.length() > length)
                    value = value.substring(0,length);
                break;
            case 'K':
                if (defValue != null)
                    value = defValue;
                break;
            case 'B':
                if ((length << 1) < value.length())
                    throw new IllegalArgumentException("field content=" + value
                            + " is too long to fit in field " + id
                            + " whose length is " + length);

                if (isSeparated(separator)) {
                    // Convert but do not pad if this field ends with a
                    // separator
                    value = new String(ISOUtil.hex2byte(value), charset);
                } else {
                    value = new String(ISOUtil.hex2byte(ISOUtil.zeropad(
                            value, length << 1).substring(0, length << 1)), charset);
                }
                break;
        }

        if (!isSeparated(separator) || isBinary(type) || EOM_SEPARATOR.equals(separator))
          return value;

        return ISOUtil.blankUnPad(value);
    }
   
    private boolean isSeparated(String separator) {
        /*
         * if type's last two characters appear in our Map of separators,
         * return true
         */
        if (separator == null)
            return false;
        else if (separators.containsKey (separator))
            return true;
        else if (isDummySeparator (separator))
            return true;
        else
            try {
                if (Character.isDefined(Integer.parseInt(separator,16))) {
                    setSeparator(separator, (char)Long.parseLong(separator,16));
                    return true;
                }
            } catch (NumberFormatException ignored) {
                throw new IllegalArgumentException("Invalid separator '"+ separator + "'");
            }
        throw new IllegalArgumentException("isSeparated called on separator="+
                      separator+" which was not previously defined.");
    }

    private boolean isDummySeparator(String separator) {
        return DUMMY_SEPARATORS.contains(separator);
    }
   
    private boolean isBinary(String type) {
        /*
         * if type's first digit is a 'B' return true
         */
        return type.startsWith("B");
    }
   
    public boolean isSeparator(byte b) {
        return separators.containsValue((char) b);
    }
   
    private String getSeparatorType(String type) {
        if (type.length() > 2) {
            return type.substring(1);
        }
        return null;
    }
   
    private char getSeparator(String separator) {
        if (separators.containsKey(separator))
            return separators.get(separator);
        else if (isDummySeparator (separator)) {
            // Dummy separator type, return 0 to indicate nothing to add.
            return 0;
        }

        throw new IllegalArgumentException("getSeparator called on separator="+
                      separator+" which was not previously defined.");
    }

    protected void pack (Element schema, StringBuilder sb)
        throws JDOMException, IOException, ISOException
    {
        String keyOff = "";
        String defaultKey = "";
        for (Element elem : (List<Element>)schema.getChildren("field")) {
            String id    = elem.getAttributeValue ("id");
            int length   = Integer.parseInt (elem.getAttributeValue ("length"));
            String type  = elem.getAttributeValue ("type");
            // For backward compatibility, look for a separator at the end of the type attribute, if no separator has been defined.
            String separator = elem.getAttributeValue ("separator");
            if (type != null && separator == null) {
              separator = getSeparatorType (type);
            }
            boolean key  = "true".equals (elem.getAttributeValue ("key"));
            Map properties = key ? loadProperties(elem) : Collections.EMPTY_MAP;
            String defValue = elem.getText();
            // If properties were specified, then the defValue contains lots of \n and \t in it. It should just be set to the empty string, or null.
            if (!properties.isEmpty()) {
              defValue = defValue.replace("\n", "").replace("\t", "").replace("\r", "");
            }
            String value = get (id, type, length, defValue, separator);
            sb.append (value);
           
            if (isSeparated(separator)) {
                char c = getSeparator(separator);
                if (c > 0)
                    sb.append(c);
            }
            if (key) {
                String v = isBinary(type) ? ISOUtil.hexString(value.getBytes(charset)) : value;
                keyOff = keyOff + normalizeKeyValue(v, properties);
                defaultKey += elem.getAttributeValue ("default-key");
            }
        }
        if (keyOff.length() > 0)
            pack (getSchema (getId (schema), keyOff, defaultKey), sb);
    }

    private Map loadProperties(Element elem) {
      Map props = new HashMap ();
        for (Element prop : (List<Element>)elem.getChildren ("property")) {
        String name = prop.getAttributeValue ("name");
        String value = prop.getAttributeValue ("value");
        props.put (name, value);
      }
      return props;
    }

  private String normalizeKeyValue(String value, Map<?,String> properties) {
      if (properties.containsKey(value)) {
            return properties.get(value);
      }
      return ISOUtil.normalize(value);
    }

    protected void unpack (InputStreamReader r, Element schema)
        throws IOException, JDOMException {

        String keyOff = "";
        String defaultKey = "";
        for (Element elem :(List<Element>)schema.getChildren("field")) {

            String id    = elem.getAttributeValue ("id");
            int length   = Integer.parseInt (elem.getAttributeValue ("length"));
            String type  = elem.getAttributeValue ("type").toUpperCase();
            String separator = elem.getAttributeValue ("separator");
            if (type != null && separator == null) {
              separator = getSeparatorType (type);
            }
            boolean key  = "true".equals (elem.getAttributeValue ("key"));
            Map properties = key ? loadProperties(elem) : Collections.EMPTY_MAP;
            String value = readField(r, id, length, type, separator);
           
            if (key) {
                keyOff = keyOff + normalizeKeyValue(value, properties);
                defaultKey += elem.getAttributeValue ("default-key");
            }
            if ("K".equals(type) && !value.equals (elem.getText()))
                throw new IllegalArgumentException (
                    "Field "+id
                       + " value='"     +value
                       + "' expected='" + elem.getText () + "'"
                );
        }
        if (keyOff.length() > 0) {
            unpack(r, getSchema (getId (schema), keyOff, defaultKey));
        }
    }
    private String getId (Element e) {
        String s = e.getAttributeValue ("id");
        return s == null ? "" : s;
    }
    protected String read (InputStreamReader r, int len, String type, String separator)
        throws IOException
    {
        StringBuilder sb = new StringBuilder();
        char[] c = new char[1];
        boolean expectSeparator = isSeparated(separator);
        boolean separated = expectSeparator;

        if (EOM_SEPARATOR.equals(separator)) {
            // Grab what's left.
            char[] rest = new char[32];
            int con = 0;
            while ((con = r.read(rest, 0, rest.length)) >= 0) {
              if (rest.length == con)
                sb.append(rest);
              else
                sb.append(Arrays.copyOf(rest, con));
            }
        } else if (isDummySeparator(separator)) {
            /*
             * No need to look for a seperator, that is not there! Try and take
             * len bytes from the is.
             */
            for (int i = 0; i < len; i++) {
                if (r.read(c) < 0) {
                    break; // end of stream indicates end of field?
                }
                sb.append(c[0]);
            }
        } else {
            for (int i = 0; i < len; i++) {
                if (r.read(c) < 0) {
                    if (!"EOF".equals(separator))
                        throw new EOFException();
                    else {
                        separated = false;
                        break;
                    }
                }
                if (expectSeparator && (c[0] == getSeparator(separator))) {
                    separated = false;
                    break;
                }
                sb.append(c[0]);
            }

            if (separated && !"EOF".equals(separator) && r.read(c) < 0) {
                throw new EOFException();
            }
        }
        readCount += sb.length();
        return sb.toString();
    }
    protected String readField (InputStreamReader r, String fieldName, int len,
        String type, String separator) throws IOException
    {
        String fieldValue = read (r, len, type, separator);
       
        if (isBinary(type))
            fieldValue = ISOUtil.hexString (fieldValue.getBytes (charset));
        fields.put (fieldName, fieldValue);
        // System.out.println ("++++ "+fieldName + ":" + fieldValue + " " + type + "," + isBinary(type));
        return fieldValue;
    }
    public void set (String name, String value) {
        if (value != null)
            fields.put (name, value);
        else
            fields.remove (name);
    }
    public void setHeader (byte[] h) {
        this.header = h;
    }
    public byte[] getHeader () {
        return header;
    }
    public String getHexHeader () {
        return header != null ? ISOUtil.hexString (header).substring (2) : "";
    }
    public String get (String fieldName) {
        return (String) fields.get (fieldName);
    }
    public String get (String fieldName, String def) {
        String s = (String) fields.get (fieldName);
        return s != null ? s : def;
    }
    public void copy (String fieldName, FSDMsg msg) {
        fields.put (fieldName, msg.get (fieldName));
    }
    public byte[] getHexBytes (String name) {
        String s = get (name);
        return s == null ? null : ISOUtil.hex2byte (s);
    }
    @SuppressWarnings("PMD.EmptyCatchBlock")
    public int getInt (String name) {
        int i = 0;
        try {
            i = Integer.parseInt (get (name));
        } catch (Exception ignored) { }
        return i;
    }
    @SuppressWarnings("PMD.EmptyCatchBlock")
    public int getInt (String name, int def) {
        int i = def;
        try {
            i = Integer.parseInt (get (name));
        } catch (Exception ignored) { }
        return i;
    }
    public Element toXML () {
        Element e = new Element ("message");
        if (header != null) {
            e.addContent (
                new Element ("header")
                    .setText (getHexHeader ())
            );
        }
        for (String fieldName :fields.keySet()) {
            Element inner = new Element (fieldName);
            inner.addContent (ISOUtil.normalize (fields.get (fieldName)));
            e.addContent (inner);
        }
        return e;
    }
    protected Element getSchema ()
        throws JDOMException, IOException {
        return getSchema (baseSchema);
    }
    protected Element getSchema (String message)
        throws JDOMException, IOException {
        return getSchema (message, "", null);
    }
    protected Element getSchema (String prefix, String suffix, String defSuffix)
        throws JDOMException, IOException {
        StringBuilder sb = new StringBuilder (basePath);
        sb.append (prefix);
        prefix = sb.toString(); // little hack, we'll reuse later with defSuffix
        sb.append (suffix);
        sb.append (".xml");
        String uri = sb.toString ();

        Space sp = SpaceFactory.getSpace();
        Element schema = (Element) sp.rdp (uri);
        if (schema == null) {
            schema = loadSchema(uri, defSuffix == null);
            if (schema == null && defSuffix != null) {
                sb = new StringBuilder (prefix);
                sb.append (defSuffix);
                sb.append (".xml");
                schema = loadSchema(sb.toString(), true);
            }
            sp.out (uri, schema);
        }
        return schema;
    }

    protected Element loadSchema(String uri, boolean throwex)
        throws JDOMException, IOException {
        SAXBuilder builder = new SAXBuilder();
        if (uri.startsWith("jar:") && uri.length()>4) {
            InputStream is = schemaResouceInputStream(uri.substring(4));
            if (is == null && throwex)
                throw new FileNotFoundException(uri + " not found");
            else if (is != null)
                return builder.build(is).getRootElement();
            else
                return null;
        }

        URL url = new URL(uri);
        try {
            return builder.build(url).getRootElement();
        } catch (FileNotFoundException ex) {
            if (throwex)
                throw ex;
            return null;
        }
    }

    protected InputStream schemaResouceInputStream(String resource)
        throws JDOMException, IOException {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        cl = cl==null ? ClassLoader.getSystemClassLoader() : cl;
        return cl.getResourceAsStream(resource);
    }

    /**
     * @return message's Map
     */
    public Map getMap () {
        return fields;
    }
    public void setMap (Map fields) {
        this.fields = fields;
    }

    @Override
    public void dump (PrintStream p, String indent) {
        String inner = indent + "  ";
        p.println (indent + "<fsdmsg schema='" + basePath + baseSchema  + "'>");
        if (header != null) {
            append (p, "header", getHexHeader(), inner);
        }
        for (String f :fields.keySet())
            append (p, f, fields.get (f), inner);
        p.println (indent + "</fsdmsg>");
    }
    private void append (PrintStream p, String f, String v, String indent) {
        p.println (indent + f + ": '" + v + "'");
    }
    public boolean hasField(String fieldName) {
        return fields.containsKey(fieldName);
    }

    @Override
    public Object clone() {
        try {             
            FSDMsg m = (FSDMsg) super.clone();
            m.fields = (Map) ((LinkedHashMap) fields).clone();
            return m;
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
    }
    public void merge (FSDMsg m) {
        for (Entry<String,String> entry: m.fields.entrySet())
             set (entry.getKey(), entry.getValue());
    }
}
TOP

Related Classes of org.jpos.util.FSDMsg

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