org.jpos.util.FSDMsg Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jpos Show documentation
Show all versions of jpos Show documentation
jPOS is an ISO-8583 based financial transaction
library/framework that can be customized and
extended in order to implement financial interchanges.
/*
* jPOS Project [http://jpos.org]
* Copyright (C) 2000-2015 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.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.
*
* 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,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)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 + "");
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());
}
}