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

org.jpos.util.FSDMsg Maven / Gradle / Ivy

Go to download

jPOS is an ISO-8583 based financial transaction library/framework that can be customized and extended in order to implement financial interchanges.

There is a newer version: 2.1.9
Show newest version
/*
 * jPOS Project [http://jpos.org]
 * Copyright (C) 2000-2016 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 .
 */

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.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.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.
 * 
 * 

How to use

*

* 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: *

*
id
*
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.
*
length
*
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.
*
type
*
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. *
*
key
*
If this optional attribute has a value of "true", then fields from another schema, specified by the value, are appended to this schema.
*
separator
*
An optional attribute containing the separator for the field. This is the preferred method of specifying the separator. See the list of optional
*
*

* Possible types are: *

*
A
Alphanumeric. Padding if any is done with spaces to the right.
*
B
Binary. Padding, if any, is done with zeros to the left.
*
K
Constant. The value is specified by the field content. No padding is done.
*
N
Numeric. Padding, if any, is done with zeros to the left.
*
*

*

* Supported field separators are: *

*
FS
Field separator using '034' as the separator.
*
US
Field separator using '037' as the separator.
*
GS
Group separator using '035' as the separator.
*
RS
Row separator using '036' as the separator.
*
PIPE
Field separator using '|' as the separator.
*
EOF
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.
*
DS
A dummy separator. This is similar to EOF, but the message stream must not end before it is allowed.
*
EOM
End of message separator. This reads all bytes available in the stream. *
*

*

* 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. *

* * @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 DUMMY_SEPARATORS = new HashSet(Arrays.asList("DS", "EOM")); private static final String EOM_SEPARATOR = "EOM"; private static final int READ_BUFFER = 8192; Map fields; Map 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(); separators = new LinkedHashMap(); 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)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)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 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)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 fields.get (fieldName); } public String get (String fieldName, String def) { String s = 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 + ""); if (header != null) { append (p, "header", getHexHeader(), inner); } for (String f :fields.keySet()) append (p, f, fields.get (f), inner); p.println (indent + ""); } 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 entry: m.fields.entrySet()) set (entry.getKey(), entry.getValue()); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy