commit 1b07cf9a32f5dcea926d8c11a3fa4947ea7461c2 Author: chochos Date: Fri Nov 26 00:30:40 2010 +0000 This is the mavenized version of the project, and it's the one I'll be working with from now on. git-svn-id: https://j8583.svn.sourceforge.net/svnroot/j8583/j8583@52 db60a039-f72e-0410-b1d2-baa000846441 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..898a61f --- /dev/null +++ b/pom.xml @@ -0,0 +1,122 @@ + + + 4.0.0 + com.solab + j8583 + 1.3.4 + jar + j8583 + Java implementation of the ISO 8583 protocol, focused on making the creation, edition and reading of ISO8583 messages as simple and flexible as possible. + http://j8583.sourceforge.net/ + + + GNU Lesser General Public License, version 2.1 + http://www.gnu.org/licenses/lgpl-2.1.html + repo + + + + scm:svn:https://j8583.svn.sourceforge.net/svnroot/j8583 + scm:svn:https://j8583.svn.sourceforge.net/svnroot/j8583 + http://j8583.svn.sourceforge.net/viewvc/j8583/ + + + UTF-8 + + + + org.slf4j + slf4j-api + 1.6.1 + + + junit + junit + 4.8.2 + jar + test + + + + Java Mexico + http://javamexico.org + + + + chochos + Enrique Zamudio Lopez + http://javamexico.org/blogs/ezamudio + + architect + developer + + + chochosmx + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.8 + + true + true + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.1 + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-site-plugin + 2.0.1 + + en,es + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.7 + + + http://download.oracle.com/javase/6/docs/api/ + http://static.springsource.org/spring/docs/3.0.x/javadoc-api/ + http://slf4j.org/apidocs/ + http://jml.blathersource.org/javadocs/ + http://dustin.github.com/java-memcached-client/apidocs/ + + + + + + + + j8583.sf.net + scp://chochos,j8583@shell.sourceforge.net:/home/groups/j/j8/j8583/htdocs + + + diff --git a/src/main/java/com/solab/iso8583/CustomField.java b/src/main/java/com/solab/iso8583/CustomField.java new file mode 100644 index 0000000..c9de1df --- /dev/null +++ b/src/main/java/com/solab/iso8583/CustomField.java @@ -0,0 +1,31 @@ +/* +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 2.1 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; + +/** Defines the behavior for a custom field value encoder/decoder + * + * @author Enrique Zamudio + */ +public interface CustomField { + + public T decodeField(String value); + + public String encodeField(T value); + +} diff --git a/src/main/java/com/solab/iso8583/IsoMessage.java b/src/main/java/com/solab/iso8583/IsoMessage.java new file mode 100644 index 0000000..0a6cf5a --- /dev/null +++ b/src/main/java/com/solab/iso8583/IsoMessage.java @@ -0,0 +1,373 @@ +/* +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 2.1 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; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** Represents an ISO8583 message. This is the core class of the framework. + * Contains the bitmap which is modified as fields are added/removed. + * This class makes no assumptions as to what types belong in each field, + * nor what fields should each different message type have; that is left + * for the developer, since the different ISO8583 implementations can vary + * greatly. + * + * @author Enrique Zamudio + */ +public class IsoMessage { + + static final byte[] HEX = new byte[]{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + /** The message type. */ + private int type; + /** Indicates if the message is binary-coded. */ + private boolean binary; + /** This is where the values are stored. */ + private Map> fields = new ConcurrentHashMap>(); + /** Stores the optional ISO header. */ + private String isoHeader; + private int etx = -1; + /** Flag to enforce secondary bitmap even if empty. */ + private boolean forceb2; + + /** Creates a new empty message with no values set. */ + public IsoMessage() { + } + + /** Creates a new message with the specified ISO header. This will be prepended to the message. */ + IsoMessage(String header) { + isoHeader = header; + } + + /** If set, this flag will cause the secondary bitmap to be written even if it's not needed. */ + public void setForceSecondaryBitmap(boolean flag) { + forceb2 = flag; + } + public boolean getForceSecondaryBitmap() { + return forceb2; + } + + /** Sets the string to be sent as ISO header, that is, after the length header but before the message type. + * This is useful in case an application needs some custom data in the ISO header of each message (very rare). */ + public void setIsoHeader(String value) { + isoHeader = value; + } + /** Returns the ISO header that this message was created with. */ + public String getIsoHeader() { + return isoHeader; + } + + /** Sets the ISO message type. Common values are 0x200, 0x210, 0x400, 0x410, 0x800, 0x810. */ + public void setType(int value) { + type = value; + } + /** Returns the ISO message type. */ + public int getType() { + return type; + } + + /** Indicates whether the message should be binary. Default is false. */ + public void setBinary(boolean flag) { + binary = flag; + } + /** Returns true if the message is binary coded; default is false. */ + public boolean isBinary() { + return binary; + } + + /** Sets the ETX character, which is sent at the end of the message as a terminator. + * Default is -1, which means no terminator is sent. */ + public void setEtx(int value) { + etx = value; + } + + /** Returns the stored value in the field, without converting or formatting it. + * @param field The field number. 1 is the secondary bitmap and is not returned as such; + * real fields go from 2 to 128. */ + public Object getObjectValue(int field) { + IsoValue v = fields.get(field); + if (v == null) { + return null; + } + return v.getValue(); + } + + /** Returns the IsoValue for the specified field. First real field is 2. */ + public IsoValue getField(int field) { + return fields.get(field); + } + + /** Stored the field in the specified index. The first field is the secondary bitmap and has index 1, + * so the first valid value for index must be 2. */ + public void setField(int index, IsoValue field) { + if (index < 2 || index > 128) { + throw new IndexOutOfBoundsException("Field index must be between 2 and 128"); + } + if (field == null) { + fields.remove(index); + } else { + fields.put(index, field); + } + } + + /** Sets the specified value in the specified field, creating an IsoValue internally. + * @param index The field number (2 to 128) + * @param value The value to be stored. + * @param t The ISO type. + * @param length The length of the field, used for ALPHA and NUMERIC values only, ignored + * with any other type. */ + public void setValue(int index, Object value, IsoType t, int length) { + setValue(index, value, null, t, length); + } + + /** Sets the specified value in the specified field, creating an IsoValue internally. + * @param index The field number (2 to 128) + * @param value The value to be stored. + * @param encoder An optional CustomField to encode/decode the value. + * @param t The ISO type. + * @param length The length of the field, used for ALPHA and NUMERIC values only, ignored + * with any other type. */ + @SuppressWarnings("unchecked") + public void setValue(int index, Object value, CustomField encoder, IsoType t, int length) { + if (index < 2 || index > 128) { + throw new IndexOutOfBoundsException("Field index must be between 2 and 128"); + } + if (value == null) { + fields.remove(index); + } else { + IsoValue v = null; + if (t.needsLength()) { + v = new IsoValue(t, value, length, encoder); + } else { + v = new IsoValue(t, value, encoder); + } + fields.put(index, v); + } + } + + /** Returns true is the message has a value in the specified field. + * @param idx The field number. */ + public boolean hasField(int idx) { + return fields.get(idx) != null; + } + + /** Writes a message to a stream, after writing the specified number of bytes indicating + * the message's length. The message will first be written to an internal memory stream + * which will then be dumped into the specified stream. This method flushes the stream + * after the write. There are at most three write operations to the stream: one for the + * length header, one for the message, and the last one with for the ETX. + * @param outs The stream to write the message to. + * @param lengthBytes The size of the message length header. Valid ranges are 0 to 4. + * @throws IllegalArgumentException if the specified length header is more than 4 bytes. + * @throws IOException if there is a problem writing to the stream. */ + public void write(OutputStream outs, int lengthBytes) throws IOException { + if (lengthBytes > 4) { + throw new IllegalArgumentException("The length header can have at most 4 bytes"); + } + byte[] data = writeInternal(); + + if (lengthBytes > 0) { + int l = data.length; + if (etx > -1) { + l++; + } + byte[] buf = new byte[lengthBytes]; + int pos = 0; + if (lengthBytes == 4) { + buf[0] = (byte)((l & 0xff000000) >> 24); + pos++; + } + if (lengthBytes > 2) { + buf[pos] = (byte)((l & 0xff0000) >> 16); + pos++; + } + if (lengthBytes > 1) { + buf[pos] = (byte)((l & 0xff00) >> 8); + pos++; + } + buf[pos] = (byte)(l & 0xff); + outs.write(buf); + } + outs.write(data); + //ETX + if (etx > -1) { + outs.write(etx); + } + outs.flush(); + } + + /** Creates and returns a ByteBuffer with the data of the message, including the length header. + * The returned buffer is already flipped, so it is ready to be written to a Channel. */ + public ByteBuffer writeToBuffer(int lengthBytes) { + if (lengthBytes > 4) { + throw new IllegalArgumentException("The length header can have at most 4 bytes"); + } + + byte[] data = writeInternal(); + ByteBuffer buf = ByteBuffer.allocate(lengthBytes + data.length + (etx > -1 ? 1 : 0)); + if (lengthBytes > 0) { + int l = data.length; + if (etx > -1) { + l++; + } + byte[] bbuf = new byte[lengthBytes]; + int pos = 0; + if (lengthBytes == 4) { + bbuf[0] = (byte)((l & 0xff000000) >> 24); + pos++; + } + if (lengthBytes > 2) { + bbuf[pos] = (byte)((l & 0xff0000) >> 16); + pos++; + } + if (lengthBytes > 1) { + bbuf[pos] = (byte)((l & 0xff00) >> 8); + pos++; + } + bbuf[pos] = (byte)(l & 0xff); + buf.put(bbuf); + } + buf.put(data); + //ETX + if (etx > -1) { + buf.put((byte)etx); + } + buf.flip(); + return buf; + } + + /** This calls writeInternal(), allowing applications to get the byte buffer containing the + * message data, without the length header. */ + public byte[] writeData() { + return writeInternal(); + } + + /** Writes the message to a memory buffer and returns it. The message does not include + * the ETX character or the header length. */ + protected byte[] writeInternal() { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + if (isoHeader != null) { + try { + bout.write(isoHeader.getBytes()); + } catch (IOException ex) { + //should never happen, writing to a ByteArrayOutputStream + } + } + //Message Type + if (binary) { + bout.write((type & 0xff00) >> 8); + bout.write(type & 0xff); + } else { + try { + bout.write(String.format("%04x", type).getBytes()); + } catch (IOException ex) { + //should never happen, writing to a ByteArrayOutputStream + } + } + + //Bitmap + ArrayList keys = new ArrayList(); + keys.addAll(fields.keySet()); + Collections.sort(keys); + BitSet bs = new BitSet(forceb2 ? 128 : 64); + for (Integer i : keys) { + bs.set(i - 1); + } + if (forceb2) { + bs.set(0); + } else if (bs.length() > 64) { + //Extend to 128 if needed + BitSet b2 = new BitSet(128); + b2.or(bs); + bs = b2; + bs.set(0); + } + //Write bitmap to stream + if (binary) { + int pos = 128; + int b = 0; + for (int i = 0; i < bs.size(); i++) { + if (bs.get(i)) { + b |= pos; + } + pos >>= 1; + if (pos == 0) { + bout.write(b); + pos = 128; + b = 0; + } + } + } else { + int pos = 0; + int lim = bs.size() / 4; + for (int i = 0; i < lim; i++) { + int nibble = 0; + if (bs.get(pos++)) + nibble |= 8; + if (bs.get(pos++)) + nibble |= 4; + if (bs.get(pos++)) + nibble |= 2; + if (bs.get(pos++)) + nibble |= 1; + bout.write(HEX[nibble]); + } + } + + //Fields + for (Integer i : keys) { + IsoValue v = fields.get(i); + try { + v.write(bout, binary); + } catch (IOException ex) { + //should never happen, writing to a ByteArrayOutputStream + } + } + return bout.toByteArray(); + } + + //These are for Groovy compat + /** Sets the specified value in the specified field, just like {@link #setField(int, IsoValue)}. */ + public void putAt(int i, IsoValue v) { + setField(i, v); + } + /** Returns the IsoValue in the specified field, just like {@link #getField(int)}. */ + public IsoValue getAt(int i) { + return getField(i); + } + + /** Copies the specified fields from the other message into the recipient. If a specified field is + * not present in the source message it is simply ignored. */ + public void copyFieldsFrom(IsoMessage src, int...idx) { + for (int i : idx) { + IsoValue v = src.getField(i); + if (v != null) { + setValue(i, v.getValue(), v.getType(), v.getLength()); + } + } + } + +} diff --git a/src/main/java/com/solab/iso8583/IsoType.java b/src/main/java/com/solab/iso8583/IsoType.java new file mode 100644 index 0000000..133d5ff --- /dev/null +++ b/src/main/java/com/solab/iso8583/IsoType.java @@ -0,0 +1,179 @@ +/* +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 2.1 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; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** Defines the possible values types that can be used in the fields. + * Some types required the length of the value to be specified (NUMERIC + * and ALPHA). Other types have a fixed length, like dates and times. + * Other types do not require a length to be specified, like LLVAR + * and LLLVAR. + * + * @author Enrique Zamudio + */ +public enum IsoType { + + /** A fixed-length numeric value. It is zero-filled to the left. */ + NUMERIC(true, 0), + /** A fixed-length alphanumeric value. It is filled with spaces to the right. */ + ALPHA(true, 0), + /** A variable length alphanumeric value with a 2-digit header length. */ + LLVAR(false, 0), + /** A variable length alphanumeric value with a 3-digit header length. */ + LLLVAR(false, 0), + /** A date in format MMddHHmmss */ + DATE10(false, 10), + /** A date in format MMdd */ + DATE4(false, 4), + /** A date in format yyMM */ + DATE_EXP(false, 4), + /** Time of day in format HHmmss */ + TIME(false, 6), + /** An amount, expressed in cents with a fixed length of 12. */ + AMOUNT(false, 12); + + private boolean needsLen; + private int length; + + IsoType(boolean flag, int l) { + needsLen = flag; + length = l; + } + + /** Returns true if the type needs a specified length. */ + public boolean needsLength() { + return needsLen; + } + + /** Returns the length of the type if it's always fixed, or 0 if it's variable. */ + public int getLength() { + return length; + } + + /** Formats a Date if the receiver is DATE10, DATE4, DATE_EXP or TIME; throws an exception + * otherwise. */ + public String format(Date value) { + if (this == DATE10) { + return new SimpleDateFormat("MMddHHmmss").format(value); + } else if (this == DATE4) { + return new SimpleDateFormat("MMdd").format(value); + } else if (this == DATE_EXP) { + return new SimpleDateFormat("yyMM").format(value); + } else if (this == TIME) { + return new SimpleDateFormat("HHmmss").format(value); + } + throw new IllegalArgumentException("Cannot format date as " + this); + } + + /** Formats the string to the given length (length is only useful if type is ALPHA). */ + public String format(String value, int length) { + if (this == ALPHA) { + if (value == null) { + value = ""; + } + if (value.length() > length) { + return value.substring(0, length); + } + char[] c = new char[length]; + System.arraycopy(value.toCharArray(), 0, c, 0, value.length()); + for (int i = value.length(); i < c.length; i++) { + c[i] = ' '; + } + return new String(c); + } else if (this == LLVAR || this == LLLVAR) { + return value; + } else if (this == NUMERIC) { + char[] c = new char[length]; + char[] x = value.toCharArray(); + if (x.length > length) { + throw new IllegalArgumentException("Numeric value is larger than intended length: " + value + " LEN " + length); + } + int lim = c.length - x.length; + for (int i = 0; i < lim; i++) { + c[i] = '0'; + } + System.arraycopy(x, 0, c, lim, x.length); + return new String(c); + } + throw new IllegalArgumentException("Cannot format String as " + this); + } + + /** Formats the integer value as a NUMERIC, an AMOUNT, or a String. */ + public String format(long value, int length) { + if (this == NUMERIC) { + char[] c = new char[length]; + char[] x = Long.toString(value).toCharArray(); + if (x.length > length) { + throw new IllegalArgumentException("Numeric value is larger than intended length: " + value + " LEN " + length); + } + int lim = c.length - x.length; + for (int i = 0; i < lim; i++) { + c[i] = '0'; + } + System.arraycopy(x, 0, c, lim, x.length); + return new String(c); + } else if (this == ALPHA || this == LLVAR || this == LLLVAR) { + return format(Long.toString(value), length); + } else if (this == AMOUNT) { + String v = Long.toString(value); + char[] digits = new char[12]; + for (int i = 0; i < 12; i++) { + digits[i] = '0'; + } + //No hay decimales asi que dejamos los dos ultimos digitos como 0 + System.arraycopy(v.toCharArray(), 0, digits, 10 - v.length(), v.length()); + return new String(digits); + } + throw new IllegalArgumentException("Cannot format number as " + this); + } + + /** Formats the BigDecimal as an AMOUNT, NUMERIC, or a String. */ + public String format(BigDecimal value, int length) { + if (this == AMOUNT) { + String v = new DecimalFormat(value.signum() >= 0 ? "0000000000.00" : "000000000.00").format(value); + return String.format("%s%s", v.substring(0, 10), v.substring(11)); + } else if (this == NUMERIC) { + return format(value.longValue(), length); + } else if (this == ALPHA || this == LLVAR || this == LLLVAR) { + return format(value.toString(), length); + } + throw new IllegalArgumentException("Cannot format BigDecimal as " + this); + } + + public IsoValue value(Object val, int len) { + return new IsoValue(this, val, len); + } + + public IsoValue value(Object val) { + return new IsoValue(this, val); + } + + public IsoValue call(Object val, int len) { + return new IsoValue(this, val, len); + } + + public IsoValue call(Object val) { + return new IsoValue(this, val); + } + +} diff --git a/src/main/java/com/solab/iso8583/IsoValue.java b/src/main/java/com/solab/iso8583/IsoValue.java new file mode 100644 index 0000000..96ef805 --- /dev/null +++ b/src/main/java/com/solab/iso8583/IsoValue.java @@ -0,0 +1,237 @@ +/* +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 2.1 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; + +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigDecimal; +import java.util.Date; + +/** Represents a value that is stored in a field inside an ISO8583 message. + * It can format the value when the message is generated. + * Some values have a fixed length, other values require a length to be specified + * so that the value can be padded to the specified length. LLVAR and LLLVAR + * values do not need a length specification because the length is calculated + * from the stored value. + * + * @author Enrique Zamudio + */ +public class IsoValue implements Cloneable { + + private IsoType type; + private T value; + private CustomField encoder; + private int length; + + public IsoValue(IsoType t, T value) { + this(t, value, null); + } + + /** Creates a new instance that stores the specified value as the specified type. + * Useful for storing LLVAR or LLLVAR types, as well as fixed-length value types + * like DATE10, DATE4, AMOUNT, etc. + * @param t the ISO type. + * @param value The value to be stored. + * @param custom An optional CustomField to encode/decode a custom value. + */ + public IsoValue(IsoType t, T value, CustomField custom) { + if (t.needsLength()) { + throw new IllegalArgumentException("Fixed-value types must use constructor that specifies length"); + } + encoder = custom; + type = t; + this.value = value; + if (type == IsoType.LLVAR || type == IsoType.LLLVAR) { + if (custom == null) { + length = value.toString().length(); + } else { + String enc = custom.encodeField(value); + if (enc == null) { + enc = value == null ? "" : value.toString(); + } + length = enc.length(); + } + if (t == IsoType.LLVAR && length > 99) { + throw new IllegalArgumentException("LLVAR can only hold values up to 99 chars"); + } else if (t == IsoType.LLLVAR && length > 999) { + throw new IllegalArgumentException("LLLVAR can only hold values up to 999 chars"); + } + } else { + length = type.getLength(); + } + } + + public IsoValue(IsoType t, T val, int len) { + this(t, val, len, null); + } + + /** Creates a new instance that stores the specified value as the specified type. + * Useful for storing fixed-length value types. + * @param t The ISO8583 type for this field. + * @param val The value to store in the field. + * @param len The length for the value. + * @param custom An optional CustomField to encode/decode a custom value. + */ + public IsoValue(IsoType t, T val, int len, CustomField custom) { + type = t; + value = val; + length = len; + encoder = custom; + if (length == 0 && t.needsLength()) { + throw new IllegalArgumentException(String.format("Length must be greater than zero for type %s (value '%s')", t, val)); + } else if (t == IsoType.LLVAR || t == IsoType.LLLVAR) { + length = custom == null ? val.toString().length() : custom.encodeField(value).length(); + if (t == IsoType.LLVAR && length > 99) { + throw new IllegalArgumentException("LLVAR can only hold values up to 99 chars"); + } else if (t == IsoType.LLLVAR && length > 999) { + throw new IllegalArgumentException("LLLVAR can only hold values up to 999 chars"); + } + } + } + + /** Returns the ISO type to which the value must be formatted. */ + public IsoType getType() { + return type; + } + + /** Returns the length of the stored value, of the length of the formatted value + * in case of NUMERIC or ALPHA. It doesn't include the field length header in case + * of LLVAR or LLLVAR. */ + public int getLength() { + return length; + } + + /** Returns the stored value without any conversion or formatting. */ + public T getValue() { + return value; + } + + /** Returns the formatted value as a String. The formatting depends on the type of the + * receiver. */ + public String toString() { + if (value == null) { + return "ISOValue"; + } + if (type == IsoType.NUMERIC || type == IsoType.AMOUNT) { + if (type == IsoType.AMOUNT) { + return type.format((BigDecimal)value, 12); + } else if (value instanceof Number) { + return type.format(((Number)value).longValue(), length); + } else { + return type.format(encoder == null ? value.toString() : encoder.encodeField(value), length); + } + } else if (type == IsoType.ALPHA) { + return type.format(encoder == null ? value.toString() : encoder.encodeField(value), length); + } else if (type == IsoType.LLLVAR || type == IsoType.LLLVAR) { + return encoder == null ? value.toString() : encoder.encodeField(value); + } else if (value instanceof Date) { + return type.format((Date)value); + } + return encoder == null ? value.toString() : encoder.encodeField(value); + } + + /** Returns a copy of the receiver that references the same value object. */ + @SuppressWarnings("unchecked") + public IsoValue clone() { + try { + return (IsoValue)super.clone(); + } catch (CloneNotSupportedException ex) { + return null; + } + } + + /** Returns true of the other object is also an IsoValue and has the same type and length, + * and if other.getValue().equals(getValue()) returns true. */ + public boolean equals(Object other) { + if (other == null || !(other instanceof IsoValue)) { + return false; + } + IsoValue comp = (IsoValue)other; + return (comp.getType() == getType() && comp.getValue().equals(getValue()) && comp.getLength() == getLength()); + } + + @Override + public int hashCode() { + return value == null ? 0 : toString().hashCode(); + } + + /** Writes the formatted value to a stream, with the length header + * if it's a variable length type. */ + public void write(OutputStream outs, boolean binary) throws IOException { + if (type == IsoType.LLLVAR || type == IsoType.LLVAR) { + if (binary) { + if (type == IsoType.LLLVAR) { + outs.write(length / 100); //00 to 09 automatically in BCD + } + //BCD encode the rest of the length + outs.write((((length % 100) / 10) << 4) | (length % 10)); + } else { + //write the length in ASCII + if (type == IsoType.LLLVAR) { + outs.write((length / 100) + 48); + } + if (length >= 10) { + outs.write(((length % 100) / 10) + 48); + } else { + outs.write(48); + } + outs.write((length % 10) + 48); + } + } else if (binary) { + //numeric types in binary are coded like this + byte[] buf = null; + if (type == IsoType.NUMERIC) { + buf = new byte[(length / 2) + (length % 2)]; + } else if (type == IsoType.AMOUNT) { + buf = new byte[6]; + } else if (type == IsoType.DATE10 || type == IsoType.DATE4 || type == IsoType.DATE_EXP || type == IsoType.TIME) { + buf = new byte[length / 2]; + } + //Encode in BCD if it's one of these types + if (buf != null) { + toBcd(toString(), buf); + outs.write(buf); + return; + } + } + //Just write the value as text + outs.write(toString().getBytes()); + } + + /** Encode the value as BCD and put it in the buffer. The buffer must be big enough + * to store the digits in the original value (half the length of the string). */ + private void toBcd(String value, byte[] buf) { + int charpos = 0; //char where we start + int bufpos = 0; + if (value.length() % 2 == 1) { + //for odd lengths we encode just the first digit in the first byte + buf[0] = (byte)(value.charAt(0) - 48); + charpos = 1; + bufpos = 1; + } + //encode the rest of the string + while (charpos < value.length()) { + buf[bufpos] = (byte)(((value.charAt(charpos) - 48) << 4) + | (value.charAt(charpos + 1) - 48)); + charpos += 2; + bufpos++; + } + } + +} diff --git a/src/main/java/com/solab/iso8583/MessageFactory.java b/src/main/java/com/solab/iso8583/MessageFactory.java new file mode 100644 index 0000000..4e8ec8f --- /dev/null +++ b/src/main/java/com/solab/iso8583/MessageFactory.java @@ -0,0 +1,421 @@ +/* +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 2.1 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; + +import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.solab.iso8583.parse.ConfigParser; +import com.solab.iso8583.parse.FieldParseInfo; + +/** This class is used to create messages, either from scratch or from an existing String or byte + * buffer. It can be configured to put default values on newly created messages, and also to know + * what to expect when reading messages from an InputStream. + *

+ * The factory can be configured to know what values to set for newly created messages, both from + * a template (useful for fields that must be set with the same value for EVERY message created) + * and individually (for trace [field 11] and message date [field 7]). + *

+ * It can also be configured to know what fields to expect in incoming messages (all possible values + * must be stated, indicating the date type for each). This way the messages can be parsed from + * a byte buffer. + * + * @author Enrique Zamudio + */ +public class MessageFactory { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + /** This map stores the message template for each message type. */ + private Map typeTemplates = new HashMap(); + /** Stores the information needed to parse messages sorted by type. */ + private Map> parseMap = new HashMap>(); + /** Stores the field numbers to be parsed, in order of appearance. */ + private Map> parseOrder = new HashMap>(); + + private TraceNumberGenerator traceGen; + /** The ISO header to be included in each message type. */ + private Map isoHeaders = new HashMap(); + /** A map for the custom field encoder/decoders, keyed by field number. */ + private Map> customFields = new HashMap>(); + /** Indicates if the current date should be set on new messages (field 7). */ + private boolean setDate; + /** Indicates if the factory should create binary messages and also parse binary messages. */ + private boolean useBinary; + private int etx = -1; + /** Flag to specify if missing fields should be ignored as long as they're at the end of the message. */ + private boolean ignoreLast; + private boolean forceb2; + + /** Sets or clears the flag to pass to new messages, to include a secondary bitmap even if it's not needed. */ + public void setForceSecondaryBitmap(boolean flag) { + forceb2 = flag; + } + public boolean getForceSecondaryBitmap() { + return forceb2; + } + + /** Setting this property to true avoids getting a ParseException when parsing messages that don't have + * the last field specified in the bitmap. This is common with certain providers where field 128 is + * specified in the bitmap but not actually included in the messages. Default is false, which has + * been the behavior in previous versions when this option didn't exist. */ + public void setIgnoreLastMissingField(boolean flag) { + ignoreLast = flag; + } + /** This flag indicates if the MessageFactory throws an exception if the last field of a message + * is not really present even though it's specified in the bitmap. Default is false which means + * an exception is thrown. */ + public boolean getIgnoreLastMissingField() { + return ignoreLast; + } + + /** Specifies a map for custom field encoder/decoders. The keys are the field numbers. */ + public void setCustomFields(Map> value) { + customFields = value; + } + + /** Sets the CustomField encoder for the specified field number. */ + public void setCustomField(int index, CustomField value) { + customFields.put(index, value); + } + /** Returns a custom field encoder/decoder for the specified field number, if one is available. */ + public CustomField getCustomField(int index) { + return customFields.get(index); + } + /** Returns a custom field encoder/decoder for the specified field number, if one is available. */ + public CustomField getCustomField(Integer index) { + return customFields.get(index); + } + + /** Tells the receiver to read the configuration at the specified path. This just calls + * ConfigParser.configureFromClasspathConfig() with itself and the specified path at arguments, + * but is really convenient in case the MessageFactory is being configured from within, say, Spring. */ + public void setConfigPath(String path) throws IOException { + ConfigParser.configureFromClasspathConfig(this, path); + } + + /** Tells the receiver to create and parse binary messages if the flag is true. + * Default is false, that is, create and parse ASCII messages. */ + public void setUseBinaryMessages(boolean flag) { + useBinary = flag; + } + /** Returns true is the factory is set to create and parse binary messages, + * false if it uses ASCII messages. Default is false. */ + public boolean getUseBinaryMessages() { + return useBinary; + } + + /** Sets the ETX character to be sent at the end of the message. This is optional and the + * default is -1, which means nothing should be sent as terminator. + * @param value The ASCII value of the ETX character or -1 to indicate no terminator should be used. */ + public void setEtx(int value) { + etx = value; + } + public int getEtx() { + return etx; + } + + /** Creates a new message of the specified type, with optional trace and date values as well + * as any other values specified in a message template. If the factory is set to use binary + * messages, then the returned message will be written using binary coding. + * @param type The message type, for example 0x200, 0x400, etc. */ + public IsoMessage newMessage(int type) { + IsoMessage m = new IsoMessage(isoHeaders.get(type)); + m.setType(type); + m.setEtx(etx); + m.setBinary(useBinary); + m.setForceSecondaryBitmap(forceb2); + + //Copy the values from the template + IsoMessage templ = typeTemplates.get(type); + if (templ != null) { + for (int i = 2; i <= 128; i++) { + if (templ.hasField(i)) { + m.setField(i, templ.getField(i).clone()); + } + } + } + if (traceGen != null) { + m.setValue(11, traceGen.nextTrace(), IsoType.NUMERIC, 6); + } + if (setDate) { + m.setValue(7, new Date(), IsoType.DATE10, 10); + } + return m; + } + + /** Creates a message to respond to a request. Increments the message type by 16, + * sets all fields from the template if there is one, and copies all values from the request, + * overwriting fields from the template if they overlap. + * @param request An ISO8583 message with a request type (ending in 00). */ + public IsoMessage createResponse(IsoMessage request) { + IsoMessage resp = new IsoMessage(isoHeaders.get(request.getType() + 16)); + resp.setBinary(request.isBinary()); + resp.setType(request.getType() + 16); + resp.setEtx(etx); + resp.setForceSecondaryBitmap(forceb2); + //Copy the values from the template or the request (request has preference) + IsoMessage templ = typeTemplates.get(resp.getType()); + if (templ == null) { + for (int i = 2; i < 128; i++) { + if (request.hasField(i)) { + resp.setField(i, request.getField(i).clone()); + } + } + } else { + for (int i = 2; i < 128; i++) { + if (request.hasField(i)) { + resp.setField(i, request.getField(i).clone()); + } else if (templ.hasField(i)) { + resp.setField(i, templ.getField(i).clone()); + } + } + } + return resp; + } + + /** Creates a new message instance from the buffer, which must contain a valid ISO8583 + * message. If the factory is set to use binary messages then it will try to parse + * a binary message. + * @param buf The byte buffer containing the message. Must not include the length header. + * @param isoHeaderLength The expected length of the ISO header, after which the message type + * and the rest of the message must come. */ + public IsoMessage parseMessage(byte[] buf, int isoHeaderLength) throws ParseException { + IsoMessage m = new IsoMessage(isoHeaderLength > 0 ? new String(buf, 0, isoHeaderLength) : null); + //TODO it only parses ASCII messages for now + int type = 0; + if (useBinary) { + type = ((buf[isoHeaderLength] & 0xff) << 8) | (buf[isoHeaderLength + 1] & 0xff); + } else { + type = ((buf[isoHeaderLength] - 48) << 12) + | ((buf[isoHeaderLength + 1] - 48) << 8) + | ((buf[isoHeaderLength + 2] - 48) << 4) + | (buf[isoHeaderLength + 3] - 48); + } + m.setType(type); + //Parse the bitmap (primary first) + BitSet bs = new BitSet(64); + int pos = 0; + if (useBinary) { + for (int i = isoHeaderLength + 2; i < isoHeaderLength + 10; i++) { + int bit = 128; + for (int b = 0; b < 8; b++) { + bs.set(pos++, (buf[i] & bit) != 0); + bit >>= 1; + } + } + //Check for secondary bitmap and parse if necessary + if (bs.get(0)) { + for (int i = isoHeaderLength + 10; i < isoHeaderLength + 18; i++) { + int bit = 128; + for (int b = 0; b < 8; b++) { + bs.set(pos++, (buf[i] & bit) != 0); + bit >>= 1; + } + } + pos = 18 + isoHeaderLength; + } else { + pos = 10 + isoHeaderLength; + } + } else { + try { + for (int i = isoHeaderLength + 4; i < isoHeaderLength + 20; i++) { + if (buf[i] >= '0' && buf[i] <= '9') { + bs.set(pos++, ((buf[i] - 48) & 8) > 0); + bs.set(pos++, ((buf[i] - 48) & 4) > 0); + bs.set(pos++, ((buf[i] - 48) & 2) > 0); + bs.set(pos++, ((buf[i] - 48) & 1) > 0); + } else if (buf[i] >= 'A' && buf[i] <= 'F') { + bs.set(pos++, ((buf[i] - 55) & 8) > 0); + bs.set(pos++, ((buf[i] - 55) & 4) > 0); + bs.set(pos++, ((buf[i] - 55) & 2) > 0); + bs.set(pos++, ((buf[i] - 5) & 1) > 0); + } else if (buf[i] >= 'a' && buf[i] <= 'f') { + bs.set(pos++, ((buf[i] - 87) & 8) > 0); + bs.set(pos++, ((buf[i] - 87) & 4) > 0); + bs.set(pos++, ((buf[i] - 87) & 2) > 0); + bs.set(pos++, ((buf[i] - 87) & 1) > 0); + } + } + //Check for secondary bitmap and parse it if necessary + if (bs.get(0)) { + for (int i = isoHeaderLength + 20; i < isoHeaderLength + 36; i++) { + if (buf[i] >= '0' && buf[i] <= '9') { + bs.set(pos++, ((buf[i] - 48) & 8) > 0); + bs.set(pos++, ((buf[i] - 48) & 4) > 0); + bs.set(pos++, ((buf[i] - 48) & 2) > 0); + bs.set(pos++, ((buf[i] - 48) & 1) > 0); + } else if (buf[i] >= 'A' && buf[i] <= 'F') { + bs.set(pos++, ((buf[i] - 55) & 8) > 0); + bs.set(pos++, ((buf[i] - 55) & 4) > 0); + bs.set(pos++, ((buf[i] - 55) & 2) > 0); + bs.set(pos++, ((buf[i] - 5) & 1) > 0); + } else if (buf[i] >= 'a' && buf[i] <= 'f') { + bs.set(pos++, ((buf[i] - 87) & 8) > 0); + bs.set(pos++, ((buf[i] - 87) & 4) > 0); + bs.set(pos++, ((buf[i] - 87) & 2) > 0); + bs.set(pos++, ((buf[i] - 87) & 1) > 0); + } + } + pos = 36 + isoHeaderLength; + } else { + pos = 20 + isoHeaderLength; + } + } catch (NumberFormatException ex) { + ParseException _e = new ParseException("Invalid ISO8583 bitmap", pos); + _e.initCause(ex); + throw _e; + } + } + //Parse each field + Map parseGuide = parseMap.get(type); + List index = parseOrder.get(type); + if (index == null) { + log.error(String.format("ISO8583 MessageFactory has no parsing guide for message type %04x [%s]", + type, new String(buf))); + return null; + } + //First we check if the message contains fields not specified in the parsing template + boolean abandon = false; + for (int i = 1; i < bs.length(); i++) { + if (bs.get(i) && !index.contains(i+1)) { + log.warn("ISO8583 MessageFactory cannot parse field {}: unspecified in parsing guide", i+1); + abandon = true; + } + } + if (abandon) { + return m; + } + //Now we parse each field + for (Integer i : index) { + FieldParseInfo fpi = parseGuide.get(i); + if (bs.get(i - 1)) { + if (ignoreLast && pos >= buf.length && i == index.get(index.size() -1)) { + log.warn("Field {} is not really in the message even though it's in the bitmap", i); + bs.clear(i - 1); + } else { + IsoValue val = useBinary ? fpi.parseBinary(buf, pos, getCustomField(i)) : fpi.parse(buf, pos, getCustomField(i)); + m.setField(i, val); + if (useBinary && !(val.getType() == IsoType.ALPHA || val.getType() == IsoType.LLVAR + || val.getType() == IsoType.LLLVAR)) { + pos += (val.getLength() / 2) + (val.getLength() % 2); + } else { + pos += val.getLength(); + } + if (val.getType() == IsoType.LLVAR) { + pos += useBinary ? 1 : 2; + } else if (val.getType() == IsoType.LLLVAR) { + pos += useBinary ? 2 : 3; + } + } + } + } + m.setBinary(useBinary); + return m; + } + + /** Sets whether the factory should set the current date on newly created messages, + * in field 7. Default is false. */ + public void setAssignDate(boolean flag) { + setDate = flag; + } + /** Returns true if the factory is assigning the current date to newly created messages + * (field 7). Default is false. */ + public boolean getAssignDate() { + return setDate; + } + + /** Sets the generator that this factory will get new trace numbers from. There is no + * default generator. */ + public void setTraceNumberGenerator(TraceNumberGenerator value) { + traceGen = value; + } + /** Returns the generator used to assign trace numbers to new messages. */ + public TraceNumberGenerator getTraceNumberGenerator() { + return traceGen; + } + + /** Sets the ISO header to be used in each message type. + * @param value A map where the keys are the message types and the values are the ISO headers. + */ + public void setIsoHeaders(Map value) { + isoHeaders.clear(); + isoHeaders.putAll(value); + } + + /** Sets the ISO header for a specific message type. + * @param type The message type, for example 0x200. + * @param value The ISO header, or NULL to remove any headers for this message type. */ + public void setIsoHeader(int type, String value) { + if (value == null) { + isoHeaders.remove(type); + } else { + isoHeaders.put(type, value); + } + } + + /** Returns the ISO header used for the specified type. */ + public String getIsoHeader(int type) { + return isoHeaders.get(type); + } + + /** Adds a message template to the factory. If there was a template for the same + * message type as the new one, it is overwritten. */ + public void addMessageTemplate(IsoMessage templ) { + if (templ != null) { + typeTemplates.put(templ.getType(), templ); + } + } + + /** Removes the message template for the specified type. */ + public void removeMessageTemplate(int type) { + typeTemplates.remove(type); + } + + /** Returns the template for the specified message type. This allows templates to be modified + * programatically. */ + public IsoMessage getMessageTemplate(int type) { + return typeTemplates.get(type); + } + + /** Sets a map with the fields that are to be expected when parsing a certain type of + * message. + * @param type The message type. + * @param map A map of FieldParseInfo instances, each of which define what type and length + * of field to expect. The keys will be the field numbers. */ + public void setParseMap(int type, Map map) { + parseMap.put(type, map); + ArrayList index = new ArrayList(); + index.addAll(map.keySet()); + Collections.sort(index); + log.trace(String.format("ISO8583 MessageFactory adding parse map for type %04x with fields %s", type, index)); + parseOrder.put(type, index); + } + +} diff --git a/src/main/java/com/solab/iso8583/TraceNumberGenerator.java b/src/main/java/com/solab/iso8583/TraceNumberGenerator.java new file mode 100644 index 0000000..cc1a103 --- /dev/null +++ b/src/main/java/com/solab/iso8583/TraceNumberGenerator.java @@ -0,0 +1,36 @@ +/* +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 2.1 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; + +/** This interface defines the behavior needed to provide sequence numbers for newly created + * messages. It must provide sequence numbers between 1 and 999999, as per the ISO standard. + * This value is put in field 11. + * A default version that simply iterates through an int in memory is provided. + * + * @author Enrique Zamudio + */ +public interface TraceNumberGenerator { + + /** Returns the next trace number. */ + public int nextTrace(); + + /** Returns the last number that was generated. */ + public int getLastTrace(); + +} diff --git a/src/main/java/com/solab/iso8583/impl/SimpleTraceGenerator.java b/src/main/java/com/solab/iso8583/impl/SimpleTraceGenerator.java new file mode 100644 index 0000000..baf4821 --- /dev/null +++ b/src/main/java/com/solab/iso8583/impl/SimpleTraceGenerator.java @@ -0,0 +1,57 @@ +/* +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 2.1 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.impl; + +import com.solab.iso8583.TraceNumberGenerator; + +/** Simple implementation of a TraceNumberGenerator with an internal + * number that is increased in memory but is not stored anywhere. + * + * @author Enrique Zamudio + */ +public class SimpleTraceGenerator implements TraceNumberGenerator { + + private int value = 0; + + /** Creates a new instance that will use the specified initial value. This means + * the first nextTrace() call will return this number. + * @param initialValue a number between 1 and 999999. + * @throws IllegalArgumentException if the number is less than 1 or greater than 999999. */ + public SimpleTraceGenerator(int initialValue) { + if (initialValue < 1 || initialValue > 999999) { + throw new IllegalArgumentException("Initial value must be between 1 and 999999"); + } + value = initialValue - 1; + } + + public int getLastTrace() { + return value; + } + + /** Returns the next number in the sequence. This method is synchronized, because the counter + * is incremented in memory only. */ + public synchronized int nextTrace() { + value++; + if (value > 999999) { + value = 1; + } + return value; + } + +} diff --git a/src/main/java/com/solab/iso8583/package.html b/src/main/java/com/solab/iso8583/package.html new file mode 100644 index 0000000..56baf12 --- /dev/null +++ b/src/main/java/com/solab/iso8583/package.html @@ -0,0 +1 @@ +This package contains the main framework classes. The one that are used directly are MessageFactory and IsoMessage. IsoType is used to indicate value types in the fields and IsoValue can be used to create values directly but is not necessary since there are convenience methods in IsoMessage. \ No newline at end of file diff --git a/src/main/java/com/solab/iso8583/parse/ConfigParser.java b/src/main/java/com/solab/iso8583/parse/ConfigParser.java new file mode 100644 index 0000000..1e79ed6 --- /dev/null +++ b/src/main/java/com/solab/iso8583/parse/ConfigParser.java @@ -0,0 +1,224 @@ +/* +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 2.1 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.net.URL; +import java.util.HashMap; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import com.solab.iso8583.CustomField; +import com.solab.iso8583.IsoMessage; +import com.solab.iso8583.IsoType; +import com.solab.iso8583.MessageFactory; + +/** 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 final static 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. */ + public static MessageFactory createDefault() throws IOException { + if (MessageFactory.class.getClassLoader().getResource("j8583.xml") == null) { + log.warn("ISO8583 ConfigParser cannot find j8583.xml, returning empty message factory"); + return new MessageFactory(); + } else { + return createFromClasspathConfig("j8583.xml"); + } + } + + /** Creates a message factory from the specified path inside the classpath. */ + public static MessageFactory createFromClasspathConfig(String path) throws IOException { + InputStream ins = MessageFactory.class.getClassLoader().getResourceAsStream(path); + MessageFactory mfact = new MessageFactory(); + if (ins != null) { + if (log.isDebugEnabled()) { + log.debug("ISO8583 Parsing config from classpath file {}", path); + } + try { + parse(mfact, ins); + } finally { + ins.close(); + } + } else { + log.warn("ISO8583 File not found in classpath: {}", path); + } + return mfact; + } + + /** Creates a message factory from the file located at the specified URL. */ + public static MessageFactory createFromUrl(URL url) throws IOException { + MessageFactory mfact = new MessageFactory(); + InputStream stream = url.openStream(); + try { + parse(mfact, stream); + } finally { + stream.close(); + } + return mfact; + } + + /** Reads the XML from the stream and configures the message factory with its values. + * @param mfact The message factory to be configured with the values read from the XML. + * @param stream The InputStream containing the XML configuration. */ + protected static void parse(MessageFactory mfact, InputStream stream) throws IOException { + final DocumentBuilderFactory docfact = DocumentBuilderFactory.newInstance(); + DocumentBuilder docb = null; + Document doc = null; + try { + docb = docfact.newDocumentBuilder(); + doc = docb.parse(stream); + } catch (ParserConfigurationException ex) { + log.error("ISO8583 Cannot parse XML configuration", ex); + return; + } catch (SAXException ex) { + log.error("ISO8583 Parsing XML configuration", ex); + return; + } + final Element root = doc.getDocumentElement(); + + //Read the ISO headers + NodeList nodes = root.getElementsByTagName("header"); + Element elem = null; + for (int i = 0; i < nodes.getLength(); i++) { + 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) { + throw new IOException("Invalid ISO8583 header element"); + } + String header = elem.getChildNodes().item(0).getNodeValue(); + if (log.isTraceEnabled()) { + log.trace("Adding ISO8583 header for type {}: {}", elem.getAttribute("type"), header); + } + mfact.setIsoHeader(type, header); + } + + //Read the message templates + nodes = root.getElementsByTagName("template"); + for (int i = 0; i < nodes.getLength(); i++) { + 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")); + } + NodeList fields = elem.getElementsByTagName("field"); + IsoMessage m = new IsoMessage(); + m.setType(type); + for (int j = 0; j < fields.getLength(); j++) { + Element f = (Element)fields.item(j); + int num = Integer.parseInt(f.getAttribute("num")); + IsoType itype = IsoType.valueOf(f.getAttribute("type")); + int length = 0; + if (f.getAttribute("length").length() > 0) { + length = Integer.parseInt(f.getAttribute("length")); + } + String v = f.getChildNodes().item(0).getNodeValue(); + CustomField _cf = mfact.getCustomField(num); + m.setValue(num, _cf == null ? v : _cf.decodeField(v), _cf, itype, length); + } + mfact.addMessageTemplate(m); + } + + //Read the parsing guides + nodes = root.getElementsByTagName("parse"); + for (int i = 0; i < nodes.getLength(); i++) { + 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")); + } + NodeList fields = elem.getElementsByTagName("field"); + HashMap parseMap = new HashMap(); + for (int j = 0; j < fields.getLength(); j++) { + Element f = (Element)fields.item(j); + int num = Integer.parseInt(f.getAttribute("num")); + IsoType itype = IsoType.valueOf(f.getAttribute("type")); + int length = 0; + if (f.getAttribute("length").length() > 0) { + length = Integer.parseInt(f.getAttribute("length")); + } + parseMap.put(num, new FieldParseInfo(itype, length)); + } + mfact.setParseMap(type, parseMap); + } + + } + + /** Configures a MessageFactory using the default configuration file j8583.xml. This is useful + * if you have a MessageFactory created using Spring for example. */ + public static void configureFromDefault(MessageFactory mfact) throws IOException { + if (MessageFactory.class.getClassLoader().getResource("j8583.xml") == null) { + log.warn("ISO8583 config file j8583.xml not found!"); + } else { + configureFromClasspathConfig(mfact, "j8583.xml"); + } + } + + /** 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. */ + public static void configureFromClasspathConfig(MessageFactory mfact, String path) throws IOException { + InputStream ins = MessageFactory.class.getClassLoader().getResourceAsStream(path); + if (ins != null) { + if (log.isDebugEnabled()) { + log.debug("ISO8583 Parsing config from classpath file {}", path); + } + try { + parse(mfact, ins); + } finally { + ins.close(); + } + } else { + log.warn("ISO8583 File not found in classpath: {}", path); + } + } + + /** 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); + } + +} diff --git a/src/main/java/com/solab/iso8583/parse/FieldParseInfo.java b/src/main/java/com/solab/iso8583/parse/FieldParseInfo.java new file mode 100644 index 0000000..0326c4c --- /dev/null +++ b/src/main/java/com/solab/iso8583/parse/FieldParseInfo.java @@ -0,0 +1,318 @@ +/* +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 2.1 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.math.BigDecimal; +import java.math.BigInteger; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; + +import com.solab.iso8583.CustomField; +import com.solab.iso8583.IsoType; +import com.solab.iso8583.IsoValue; + +/** This class contains the information needed to parse a field from a message buffer. + * + * @author Enrique Zamudio + */ +public class FieldParseInfo { + + private IsoType type; + private int length; + + /** Creates a new instance that parses a value of the specified type, with the specified length. + * The length is only useful for ALPHA and NUMERIC types. + * @param t The ISO type to be parsed. + * @param len The length of the data to be read (useful only for ALPHA and NUMERIC types). */ + public FieldParseInfo(IsoType t, int len) { + if (t == null) { + throw new IllegalArgumentException("IsoType cannot be null"); + } + type = t; + length = len; + } + + /** Returns the specified length for the data to be parsed. */ + public int getLength() { + return length; + } + + /** Returns the data type for the data to be parsed. */ + public IsoType getType() { + return type; + } + + /** Parses the character data from the buffer and returns the + * IsoValue with the correct data type in it. */ + public IsoValue parse(byte[] buf, int pos, CustomField custom) throws ParseException { + if (pos < 0) { + throw new ParseException(String.format("Invalid position %d", pos), pos); + } + if (type == IsoType.NUMERIC || type == IsoType.ALPHA) { + if (pos+length > buf.length) { + throw new ParseException(String.format("Insufficient data for ALPHA or NUMERIC field of length %d, pos %d", + length, pos), pos); + } + if (custom == null) { + return new IsoValue(type, new String(buf, pos, length), length, null); + } else { + IsoValue v = new IsoValue(type, custom.decodeField(new String(buf, pos, length)), length, custom); + if (v.getValue() == null) { + return new IsoValue(type, new String(buf, pos, length), length, null); + } + return v; + } + } else if (type == IsoType.LLVAR) { + length = ((buf[pos] - 48) * 10) + (buf[pos + 1] - 48); + if (length < 0) { + throw new ParseException(String.format("Invalid LLVAR length %d pos %d", length, pos), pos); + } + if (pos+2 > buf.length || length+pos+2 > buf.length) { + throw new ParseException(String.format("Insufficient data for LLVAR field, pos %d", pos), pos); + } + if (custom == null) { + return new IsoValue(type, new String(buf, pos + 2, length), null); + } else { + IsoValue v = new IsoValue(type, custom.decodeField(new String(buf, pos + 2, length)), custom); + if (v.getValue() == null) { + return new IsoValue(type, new String(buf, pos + 2, length), null); + } + return v; + } + } else if (type == IsoType.LLLVAR) { + length = ((buf[pos] - 48) * 100) + ((buf[pos + 1] - 48) * 10) + (buf[pos + 2] - 48); + if (length < 0) { + throw new ParseException(String.format("Invalid LLLVAR length %d pos %d", length, pos), pos); + } + if (pos+3 > buf.length || length+pos+3 > buf.length) { + throw new ParseException(String.format("Insufficient data for LLLVAR field, pos %d", pos), pos); + } + if (custom == null) { + return new IsoValue(type, new String(buf, pos + 3, length), null); + } else { + IsoValue v = new IsoValue(type, custom.decodeField(new String(buf, pos + 3, length)), custom); + if (v.getValue() == null) { + //problems decoding? return the string + return new IsoValue(type, new String(buf, pos + 3, length), null); + } + return v; + } + } else if (type == IsoType.AMOUNT) { + if (pos+12 > buf.length) { + throw new ParseException(String.format("Insufficient data for AMOUNT field of length %d, pos %d", + length, pos), pos); + } + byte[] c = new byte[13]; + System.arraycopy(buf, pos, c, 0, 10); + System.arraycopy(buf, pos + 10, c, 11, 2); + c[10] = '.'; + try { + return new IsoValue(type, new BigDecimal(new String(c)), null); + } catch (NumberFormatException ex) { + throw new ParseException(String.format("Cannot read amount '%s' pos %d", new String(c), pos), pos); + } + } else if (type == IsoType.DATE10) { + if (pos+10 > buf.length) { + throw new ParseException(String.format("Insufficient data for DATE10 field of length %d, pos %d", + length, pos), pos); + } + //A SimpleDateFormat in the case of dates won't help because of the missing data + //we have to use the current date for reference and change what comes in the buffer + Calendar cal = Calendar.getInstance(); + //Set the month in the date + cal.set(Calendar.MONTH, ((buf[pos] - 48) * 10) + buf[pos + 1] - 49); + cal.set(Calendar.DATE, ((buf[pos + 2] - 48) * 10) + buf[pos + 3] - 48); + cal.set(Calendar.HOUR_OF_DAY, ((buf[pos + 4] - 48) * 10) + buf[pos + 5] - 48); + cal.set(Calendar.MINUTE, ((buf[pos + 6] - 48) * 10) + buf[pos + 7] - 48); + cal.set(Calendar.SECOND, ((buf[pos + 8] - 48) * 10) + buf[pos + 9] - 48); + if (cal.getTime().after(new Date())) { + cal.add(Calendar.YEAR, -1); + } + return new IsoValue(type, cal.getTime(), null); + } else if (type == IsoType.DATE4) { + if (pos+4 > buf.length) { + throw new ParseException(String.format("Insufficient data for DATE4 field of length %d, pos %d", + length, pos), pos); + } + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + //Set the month in the date + cal.set(Calendar.MONTH, ((buf[pos] - 48) * 10) + buf[pos + 1] - 49); + cal.set(Calendar.DATE, ((buf[pos + 2] - 48) * 10) + buf[pos + 3] - 48); + if (cal.getTime().after(new Date())) { + cal.add(Calendar.YEAR, -1); + } + return new IsoValue(type, cal.getTime(), null); + } else if (type == IsoType.DATE_EXP) { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.DATE, 1); + //Set the month in the date + cal.set(Calendar.YEAR, cal.get(Calendar.YEAR) - (cal.get(Calendar.YEAR) % 100) + + ((buf[pos] - 48) * 10) + buf[pos + 1] - 48); + cal.set(Calendar.MONTH, ((buf[pos + 2] - 48) * 10) + buf[pos + 3] - 49); + return new IsoValue(type, cal.getTime(), null); + } else if (type == IsoType.TIME) { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, ((buf[pos] - 48) * 10) + buf[pos + 1] - 48); + cal.set(Calendar.MINUTE, ((buf[pos + 2] - 48) * 10) + buf[pos + 3] - 48); + cal.set(Calendar.SECOND, ((buf[pos + 4] - 48) * 10) + buf[pos + 5] - 48); + return new IsoValue(type, cal.getTime(), null); + } + return null; + } + + /** Parses binary data from the buffer, creating and returning an IsoValue of the configured + * type and length. */ + public IsoValue parseBinary(byte[] buf, int pos, CustomField custom) throws ParseException { + if (type == IsoType.ALPHA) { + + if (custom == null) { + return new IsoValue(type, new String(buf, pos, length), length, null); + } else { + IsoValue v = new IsoValue(type, custom.decodeField(new String(buf, pos, length)), length, custom); + if (v.getValue() == null) { + return new IsoValue(type, new String(buf, pos, length), length, null); + } + return v; + } + + } else if (type == IsoType.NUMERIC) { + + //A long covers up to 18 digits + if (length < 19) { + long l = 0; + long power = 1L; + for (int i = pos + (length / 2) + (length % 2) - 1; i >= pos; i--) { + l += (buf[i] & 0x0f) * power; + power *= 10L; + l += ((buf[i] & 0xf0) >> 4) * power; + power *= 10L; + } + return new IsoValue(IsoType.NUMERIC, l, length, null); + } else { + //Use a BigInteger + char[] digits = new char[length]; + int start = 0; + for (int i = pos; i < pos + (length / 2) + (length % 2); i++) { + digits[start++] = (char)(((buf[i] & 0xf0) >> 4) + 48); + digits[start++] = (char)((buf[i] & 0x0f) + 48); + } + return new IsoValue(IsoType.NUMERIC, new BigInteger(new String(digits)), length, null); + } + + } else if (type == IsoType.LLVAR) { + + length = (((buf[pos] & 0xf0) >> 4) * 10) + (buf[pos] & 0x0f); + if (custom == null) { + return new IsoValue(type, new String(buf, pos + 1, length), null); + } else { + IsoValue v = new IsoValue(type, custom.decodeField(new String(buf, pos + 1, length)), custom); + if (v.getValue() == null) { + return new IsoValue(type, new String(buf, pos + 1, length), null); + } + return v; + } + + } else if (type == IsoType.LLLVAR) { + + length = ((buf[pos] & 0x0f) * 100) + (((buf[pos + 1] & 0xf0) >> 4) * 10) + (buf[pos + 1] & 0x0f); + if (custom == null) { + return new IsoValue(type, new String(buf, pos + 2, length), null); + } else { + IsoValue v = new IsoValue(type, custom.decodeField(new String(buf, pos + 2, length)), custom); + if (v.getValue() == null) { + return new IsoValue(type, new String(buf, pos + 2, length), null); + } + return v; + } + + } else if (type == IsoType.AMOUNT) { + + char[] digits = new char[13]; + digits[10] = '.'; + int start = 0; + for (int i = pos; i < pos + 6; i++) { + digits[start++] = (char)(((buf[i] & 0xf0) >> 4) + 48); + digits[start++] = (char)((buf[i] & 0x0f) + 48); + if (start == 10) { + start++; + } + } + return new IsoValue(IsoType.AMOUNT, new BigDecimal(new String(digits)), null); + } else if (type == IsoType.DATE10 || type == IsoType.DATE4 || type == IsoType.DATE_EXP + || type == IsoType.TIME) { + + int[] tens = new int[(type.getLength() / 2) + (type.getLength() % 2)]; + int start = 0; + for (int i = pos; i < pos + tens.length; i++) { + tens[start++] = (((buf[i] & 0xf0) >> 4) * 10) + (buf[i] & 0x0f); + } + Calendar cal = Calendar.getInstance(); + if (type == IsoType.DATE10) { + //A SimpleDateFormat in the case of dates won't help because of the missing data + //we have to use the current date for reference and change what comes in the buffer + //Set the month in the date + cal.set(Calendar.MONTH, tens[0] - 1); + cal.set(Calendar.DATE, tens[1]); + cal.set(Calendar.HOUR_OF_DAY, tens[2]); + cal.set(Calendar.MINUTE, tens[3]); + cal.set(Calendar.SECOND, tens[4]); + if (cal.getTime().after(new Date())) { + cal.add(Calendar.YEAR, -1); + } + return new IsoValue(type, cal.getTime(), null); + } else if (type == IsoType.DATE4) { + cal.set(Calendar.HOUR, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + //Set the month in the date + cal.set(Calendar.MONTH, tens[0] - 1); + cal.set(Calendar.DATE, tens[1]); + if (cal.getTime().after(new Date())) { + cal.add(Calendar.YEAR, -1); + } + return new IsoValue(type, cal.getTime(), null); + } else if (type == IsoType.DATE_EXP) { + cal.set(Calendar.HOUR, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.DATE, 1); + //Set the month in the date + cal.set(Calendar.YEAR, cal.get(Calendar.YEAR) + - (cal.get(Calendar.YEAR) % 100) + tens[0]); + cal.set(Calendar.MONTH, tens[1] - 1); + return new IsoValue(type, cal.getTime(), null); + } else if (type == IsoType.TIME) { + cal.set(Calendar.HOUR_OF_DAY, tens[0]); + cal.set(Calendar.MINUTE, tens[1]); + cal.set(Calendar.SECOND, tens[2]); + return new IsoValue(type, cal.getTime(), null); + } + return new IsoValue(type, cal.getTime(), null); + } + return null; + } + +} diff --git a/src/main/java/com/solab/iso8583/parse/j8583.dtd b/src/main/java/com/solab/iso8583/parse/j8583.dtd new file mode 100644 index 0000000..79bf080 --- /dev/null +++ b/src/main/java/com/solab/iso8583/parse/j8583.dtd @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/main/java/com/solab/iso8583/parse/package.html b/src/main/java/com/solab/iso8583/parse/package.html new file mode 100644 index 0000000..a5e0225 --- /dev/null +++ b/src/main/java/com/solab/iso8583/parse/package.html @@ -0,0 +1 @@ +This package contains the classes used for parsing the XML configuration files to set up a MessageFactory, and also for storing the parse information for each field depending on the type of message. \ No newline at end of file diff --git a/src/main/resources/PLACEHOLDER b/src/main/resources/PLACEHOLDER new file mode 100644 index 0000000..e69de29 diff --git a/src/site/apt/groovy.apt b/src/site/apt/groovy.apt new file mode 100644 index 0000000..e69de29 diff --git a/src/site/apt/guide.apt b/src/site/apt/guide.apt new file mode 100644 index 0000000..e69de29 diff --git a/src/site/apt/index.apt b/src/site/apt/index.apt new file mode 100644 index 0000000..e69de29 diff --git a/src/site/apt/spring.apt b/src/site/apt/spring.apt new file mode 100644 index 0000000..e69de29 diff --git a/src/site/site.xml b/src/site/site.xml new file mode 100644 index 0000000..346d561 --- /dev/null +++ b/src/site/site.xml @@ -0,0 +1,30 @@ + + + + j8583 + http://sflogo.sourceforge.net/sflogo.php?group_id=194767&type=7 + http://j8583.sourceforge.net/ + + + + + + + + + +

+ + + + + + + + + + + + + diff --git a/src/site/site_es.xml b/src/site/site_es.xml new file mode 100644 index 0000000..2fcffda --- /dev/null +++ b/src/site/site_es.xml @@ -0,0 +1,30 @@ + + + + j8583 + http://sflogo.sourceforge.net/sflogo.php?group_id=194767&type=7 + http://j8583.sourceforge.net/ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/PLACEHOLDER b/src/test/resources/PLACEHOLDER new file mode 100644 index 0000000..e69de29