Files
Java-8583-Util/src/main/java/com/solab/iso8583/IsoMessage.java
T

374 lines
12 KiB
Java

/*
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<Integer,IsoValue<?>> fields = new ConcurrentHashMap<Integer,IsoValue<?>>();
/** 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<Integer> keys = new ArrayList<Integer>();
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());
}
}
}
}