Skip to main content

ISOComponent: Composite Pattern

The entire jPOS message model is built on a single abstract class: ISOComponent.

Historical context

The Composite pattern was chosen in the late 1990s when jPOS was first written. Java's Collections framework didn't exist in 1.0 and was rudimentary in 1.1. A typed tree of objects with a uniform interface was the right design choice at the time. Twenty-five years and thousands of production payment systems later, its extensibility has proven worth keeping.

The pattern

The Composite pattern lets you treat individual objects (Leafs) and groups of objects (Composites) through the same interface. ISOComponent provides both sides:

public abstract class ISOComponent implements Cloneable {

// --- Composite API (valid on ISOMsg, throw on Leafs) ---
public void set(ISOComponent c) throws ISOException { throw new ISOException("Can't add to Leaf"); }
public void unset(int fldno) throws ISOException { throw new ISOException("Can't remove from Leaf"); }
public ISOComponent getComposite() { return null; } // Composites override → return this
public int getMaxField() { return 0; }
public Map getChildren() { return Collections.emptyMap(); }

// --- Leaf API (valid on ISOField/ISOBinaryField/ISOBitMap, throw on ISOMsg) ---
public Object getKey() throws ISOException { throw new ISOException("N/A in Composite"); }
public Object getValue() throws ISOException { throw new ISOException("N/A in Composite"); }
public byte[] getBytes() throws ISOException { throw new ISOException("N/A in Composite"); }

// --- Shared (abstract) ---
public abstract void setFieldNumber(int fieldNumber);
public abstract int getFieldNumber();
public abstract void setValue(Object obj) throws ISOException;
public abstract byte[] pack() throws ISOException;
public abstract int unpack(byte[] b) throws ISOException;
public abstract void dump(PrintStream p, String indent);
public abstract void unpack(InputStream in) throws IOException, ISOException;
}

The getComposite() method is the key discriminator. Packager code that receives an ISOComponent calls getComposite() to determine which side it's on:

// From ISOBasePackager.pack():
if (m.getComposite() != m)
throw new ISOException("Can't call packager on non Composite");

Leaf: ISOField

ISOField represents a standard string-valued data element:

public class ISOField extends ISOComponent implements Cloneable, Externalizable {
protected int fieldNumber;
protected String value;

public Object getKey() { return fieldNumber; }
public Object getValue() { return value; }
public byte[] getBytes() { return value.getBytes(ISOUtil.CHARSET); }
}
  • getKey() returns Integer (the field number). ISOMsg uses this as the map key.
  • getValue() returns the String value. Field packagers cast to String during pack.
  • pack() and unpack() throw ISOException("Not available on Leaf") — Leafs don't know how to pack themselves. Only ISOFieldPackager does.

Leaf: ISOBinaryField

ISOBinaryField holds raw bytes — used for binary data elements such as PINs, keys, and MAC values:

public class ISOBinaryField extends ISOComponent {
protected int fieldNumber;
protected byte[] value;

public Object getValue() { return value; } // returns byte[]
public byte[] getBytes() { return value; }
}

The distinction matters to field packagers: ISOStringFieldPackager checks whether c.getValue() is a byte[] and handles it transparently, but dedicated binary packagers like IFB_BINARY always expect byte[].

Leaf: ISOBitMap

ISOBitMap wraps java.util.BitSet and represents the field presence bitmap. It is always stored at field number -1 inside ISOMsg:

public class ISOBitMap extends ISOComponent {
protected int fieldNumber; // always -1 in practice
protected BitSet value;
}

The −1 key is a deliberate choice: it sits below all valid field numbers (1–192), so it never appears when participants call msg.getString(n) or iterate over normal fields. It's invisible to application code but always present for the packager.

Composite: ISOMsg

ISOMsg is the Composite. It holds a TreeMap<Object, ISOComponent> called fields:

public class ISOMsg extends ISOComponent implements ... {
private TreeMap fields = new TreeMap();

@Override
public ISOComponent getComposite() { return this; } // ← Composite returns itself

@Override
public void set(ISOComponent c) throws ISOException {
if (c.getKey() != null)
fields.put(c.getKey(), c);
}
@Override
public Map getChildren() {
return (Map) ((TreeMap) fields).clone();
}
@Override
public int getMaxField() {
// returns highest set field number (excluding MTI and bitmap)
...
}
}

The fields map key space

KeyHolds
Integer(0)MTI — ISOField("0200")
Integer(-1)Bitmap — ISOBitMap (written/read by packager only)
Integer(2) .. Integer(192)Data elements — ISOField or ISOBinaryField
Any non-integerSub-message fields in structured field packagers

Convenience API

ISOMsg provides a rich convenience layer on top of the raw Composite API:

// Setting fields
msg.setMTI("0200");
msg.set(2, "4111111111111111"); // ISOField
msg.set(52, new byte[]{...}); // ISOBinaryField

// Reading fields
String mti = msg.getMTI(); // "0200"
String pan = msg.getString(2); // "4111111111111111"
byte[] pin = msg.getBytes(52); // raw bytes
boolean has = msg.hasField(4);
boolean hasAll = msg.hasFields(new int[]{2, 3, 4});

// Response helpers
ISOMsg resp = (ISOMsg) req.clone();
resp.setResponseMTI(); // 0200 → 0210
resp.set(39, "0000");

Nested messages (sub-messages)

ISOMsg can be nested inside another ISOMsg. This is how field 55 (EMV data) or field 48 (additional data with sub-fields) are modelled — an inner ISOMsg with its own field numbering and its own packager.

ISOMsg inner = new ISOMsg(48);        // field number 48 in the outer message
inner.set(1, "value-of-subfield-1");
inner.set(2, "value-of-subfield-2");
outer.set(inner);

The ISOMsgFieldPackager handles the two-level packing: it first uses an inner packager to pack the sub-message, then wraps the result with an outer field packager (e.g. IFA_LLLCHAR) to prefix the total length.

The Externalizable contract

Both ISOField and ISOBinaryField implement Externalizable. This supports jPOS Space persistence (writing ISOMsg objects to JDBM or JE spaces for crash recovery). The TM's persistent context uses this path.

// ISOField serialization (compact binary form)
public void writeExternal(ObjectOutput out) throws IOException {
out.writeShort(fieldNumber);
out.writeUTF(value);
}

ISOMsg itself implements Externalizable and delegates to each child's serialization. This is independent of the ISO-8583 wire format — it's purely a Java persistence mechanism.