diff --git a/jpos/src/main/java/org/jpos/iso/DatasetElement.java b/jpos/src/main/java/org/jpos/iso/DatasetElement.java
index 8219203527..2172705cd5 100644
--- a/jpos/src/main/java/org/jpos/iso/DatasetElement.java
+++ b/jpos/src/main/java/org/jpos/iso/DatasetElement.java
@@ -81,6 +81,21 @@ public boolean isConstructed() {
return constructed;
}
+ /**
+ * Indicates whether a BER-TLV tag is constructed, that is, whether bit 6
+ * of its first byte is set.
+ *
+ * @param tag TLV tag, for example {@code 0x71} or {@code 0x9F26}
+ * @return {@code true} for constructed tags
+ */
+ public static boolean isConstructedTag(int tag) {
+ String hexTag = Integer.toHexString(tag);
+ if ((hexTag.length() & 0x01) == 1)
+ hexTag = "0" + hexTag;
+ byte[] tagBytes = ISOUtil.hex2byte(hexTag);
+ return (tagBytes[0] & 0x20) == 0x20;
+ }
+
/**
* Returns the component value.
*
diff --git a/jpos/src/main/java/org/jpos/iso/ISOMsg.java b/jpos/src/main/java/org/jpos/iso/ISOMsg.java
index ce6ae284a4..5d9a336643 100644
--- a/jpos/src/main/java/org/jpos/iso/ISOMsg.java
+++ b/jpos/src/main/java/org/jpos/iso/ISOMsg.java
@@ -302,6 +302,8 @@ public ISOMsg with(int fldno, String value) {
*
* @param fpath dot-separated field path (i.e. 63.2)
* @param value field value
+ * @throws IllegalArgumentException if fpath addresses a dataset field and does not
+ * match the field packager's dataset envelope, or the value cannot be stored there
*/
public void set(String fpath, String value) {
try {
@@ -354,7 +356,8 @@ public ISOMsg with(String fpath, String value) {
* Creates an ISOField associated with fldno within this ISOMsg
* @param fpath dot-separated field path (i.e. 63.2)
* @param c component
- * @throws ISOException on error
+ * @throws ISOException on error, including a path that addresses a dataset field
+ * and does not match the field packager's dataset envelope
*/
public void set(String fpath, ISOComponent c) throws ISOException {
if (setDatasetPath(fpath, c))
@@ -404,6 +407,8 @@ public ISOMsg with(String fpath, ISOComponent c) throws ISOException {
*
* @param fpath dot-separated field path (i.e. 63.2)
* @param value binary field value
+ * @throws IllegalArgumentException if fpath addresses a dataset field and does not
+ * match the field packager's dataset envelope, or the value cannot be stored there
*/
public void set(String fpath, byte[] value) {
try {
@@ -496,6 +501,8 @@ public void unset (int ... flds) {
* Unset a field referenced by a fpath if it exists, otherwise ignore.
*
* @param fpath dot-separated field path (i.e. 63.2)
+ * @throws IllegalArgumentException if fpath addresses a dataset field and does not
+ * match the field packager's dataset envelope
*/
public void unset(String fpath) {
try {
@@ -565,6 +572,115 @@ public ISOMsg without(String ... fpaths) {
unset(fpaths);
return this;
}
+
+ /**
+ * Sets an element in a dataset field without a dataset envelope (such as DE 55
+ * ICC data), where the dataset identifier is the field number.
+ *
+ *
Unlike {@link #set(String, String)}, no packager is needed: the dataset
+ * field is created if absent, along with any intermediate composite fields.
+ * New datasets use {@link DatasetFormat#TLV}. The value replaces every existing
+ * element with the same identifier in the field's first dataset; TLV elements
+ * record whether the tag is constructed.
+ *
+ * @param fpath dot-separated path of the dataset field (i.e. 55, 127.55)
+ * @param elementId element identifier (TLV tag)
+ * @param value a {@code String}, {@code byte[]} or {@link ISOComponent};
+ * {@code null} removes the element, as {@link #unsetDatasetElement(String, int)}
+ * @throws IllegalArgumentException if fpath is malformed or elementId is negative
+ * @throws ISOException if the path runs through, or ends at, a field of the wrong
+ * type, or the value type is not supported
+ */
+ public void setDatasetElement(String fpath, int elementId, Object value) throws ISOException {
+ writeDatasetElement(fpath, false, 0, elementId, value);
+ }
+
+ /**
+ * Sets an element in a dataset field with a dataset envelope (ISO 8583:2023
+ * composite data elements such as DE 104).
+ *
+ * Unlike {@link #set(String, String)}, no packager is needed: the dataset
+ * field is created if absent, along with any intermediate composite fields.
+ * New datasets use {@link DatasetFormat#TLV} for identifiers up to {@code 0x70}
+ * and {@link DatasetFormat#DBM} above. The value replaces every existing element
+ * with the same identifier in the first dataset carrying {@code datasetId};
+ * TLV elements record whether the tag is constructed.
+ *
+ * @param fpath dot-separated path of the dataset field (i.e. 104)
+ * @param datasetId dataset identifier
+ * @param elementId element identifier (TLV tag or DBM bit number)
+ * @param value a {@code String}, {@code byte[]} or {@link ISOComponent};
+ * {@code null} removes the element, as {@link #unsetDatasetElement(String, int, int)}
+ * @throws IllegalArgumentException if fpath is malformed or an identifier is negative
+ * @throws ISOException if the path runs through, or ends at, a field of the wrong
+ * type, or the value type is not supported
+ */
+ public void setDatasetElement(String fpath, int datasetId, int elementId, Object value) throws ISOException {
+ writeDatasetElement(fpath, true, datasetId, elementId, value);
+ }
+
+ /**
+ * Sets an element in a dataset field without a dataset envelope and returns
+ * this message for fluent chaining.
+ *
+ * @param fpath dot-separated path of the dataset field (i.e. 55)
+ * @param elementId element identifier (TLV tag)
+ * @param value element value
+ * @return this message
+ * @throws ISOException on errors, see {@link #setDatasetElement(String, int, Object)}
+ */
+ public ISOMsg withDatasetElement(String fpath, int elementId, Object value) throws ISOException {
+ setDatasetElement(fpath, elementId, value);
+ return this;
+ }
+
+ /**
+ * Sets an element in a dataset field with a dataset envelope and returns this
+ * message for fluent chaining.
+ *
+ * @param fpath dot-separated path of the dataset field (i.e. 104)
+ * @param datasetId dataset identifier
+ * @param elementId element identifier (TLV tag or DBM bit number)
+ * @param value element value
+ * @return this message
+ * @throws ISOException on errors, see {@link #setDatasetElement(String, int, int, Object)}
+ */
+ public ISOMsg withDatasetElement(String fpath, int datasetId, int elementId, Object value) throws ISOException {
+ setDatasetElement(fpath, datasetId, elementId, value);
+ return this;
+ }
+
+ /**
+ * Removes an element from a dataset field without a dataset envelope.
+ *
+ * Every occurrence is removed. Datasets left empty are removed, and so is
+ * the dataset field once it has no datasets. An absent path is ignored.
+ *
+ * @param fpath dot-separated path of the dataset field (i.e. 55)
+ * @param elementId element identifier (TLV tag)
+ * @throws IllegalArgumentException if fpath is malformed or elementId is negative
+ * @throws ISOException if the path ends at a field that is not a dataset field
+ */
+ public void unsetDatasetElement(String fpath, int elementId) throws ISOException {
+ writeDatasetElement(fpath, false, 0, elementId, null);
+ }
+
+ /**
+ * Removes an element from a dataset field with a dataset envelope.
+ *
+ * Every occurrence is removed from every dataset carrying {@code datasetId}.
+ * Datasets left empty are removed, and so is the dataset field once it has no
+ * datasets. An absent path is ignored.
+ *
+ * @param fpath dot-separated path of the dataset field (i.e. 104)
+ * @param datasetId dataset identifier
+ * @param elementId element identifier (TLV tag or DBM bit number)
+ * @throws IllegalArgumentException if fpath is malformed or an identifier is negative
+ * @throws ISOException if the path ends at a field that is not a dataset field
+ */
+ public void unsetDatasetElement(String fpath, int datasetId, int elementId) throws ISOException {
+ writeDatasetElement(fpath, true, datasetId, elementId, null);
+ }
/**
* In order to interchange Composites and Leafs we use
* getComposite(). A Composite component returns itself and
@@ -754,6 +870,80 @@ public ISOComponent getComponent (String fpath) throws ISOException {
}
return obj;
}
+
+ /**
+ * Strict variant of {@link #getValue(String)}.
+ *
+ * The whole path is validated before the message is read, so a malformed
+ * path always throws {@link IllegalArgumentException}, regardless of the
+ * message contents. Each segment is a decimal number ({@code 62}) or a
+ * {@code 0x}-prefixed hexadecimal number ({@code 0x9F26}) in the range
+ * {@code 0..Integer.MAX_VALUE}; signs, whitespace and empty segments are invalid.
+ *
+ * Every segment but the last must address a sub-{@link ISOMsg}. A path
+ * through a leaf field or an absent field returns {@link Optional#empty()}.
+ * The value is a {@code String}, a {@code byte[]}, the sub-{@code ISOMsg} for
+ * a composite field, or the {@code List} of a dataset field.
+ *
+ * When the path reaches an {@link ISODatasetField} with segments left, it
+ * addresses a dataset element, using the same layout as {@link #set(String, String)}:
+ * {@code 55.0x9F26} (no envelope, the dataset identifier is the field number) or
+ * {@code 104.0x71.0x01} (envelope: dataset identifier, then element identifier).
+ * If the field packager of the message holding the dataset field is a
+ * {@link DatasetFieldPackager}, its {@link ISODatasetPackager#hasDatasetEnvelope()}
+ * decides the layout and a path that does not match it returns empty. Otherwise
+ * the layout is inferred from the number of remaining segments. More than two
+ * segments past a dataset field return empty.
+ *
+ * A dataset element path returns the value of the first element in
+ * {@link #findDatasetElements(String)}, so {@code findValue(p).isPresent()} is
+ * equivalent to {@code !findDatasetElements(p).isEmpty()}. Constructed TLV
+ * elements return their encoded value; decode their children with
+ * {@link org.jpos.tlv.TLVList}.
+ *
+ * Without a packager, {@code set("55.0x9F26", value)} creates a plain
+ * composite field rather than a dataset field; this method reads it back
+ * through the normal composite walk. Use
+ * {@link #setDatasetElement(String, int, Object)} to create dataset fields
+ * without a packager.
+ *
+ * @param fpath dot-separated field path (i.e. 62.1, 55.0x9F26)
+ * @return the value, or empty if the path is not present
+ * @throws IllegalArgumentException if fpath is malformed
+ * @throws ISOException if a dataset element value cannot be read
+ */
+ public Optional findValue(String fpath) throws ISOException {
+ int[] path = parsePath(fpath);
+ PathTarget target = locate(path);
+ if (target == null)
+ return Optional.empty();
+ if (target.datasetField() == null)
+ return Optional.ofNullable(target.msg().getValue(path[target.index()]));
+ List elements = target.msg().datasetElements(target.datasetField(), path, target.index());
+ return elements.isEmpty() ? Optional.empty() : Optional.ofNullable(elements.get(0).getValue());
+ }
+
+ /**
+ * Returns every occurrence of the dataset element addressed by a path.
+ *
+ * Uses the same path grammar and dataset layout rules as {@link #findValue(String)}.
+ * Elements are collected from every dataset carrying the addressed dataset
+ * identifier, in dataset order and then element order, so repeated tags (such
+ * as EMV issuer scripts) are all returned with their constructed flag.
+ *
+ * @param fpath dot-separated dataset element path (i.e. 55.0x71, 104.0x71.0x01)
+ * @return unmodifiable list of matching elements; empty if the path is absent or
+ * does not address a dataset element
+ * @throws IllegalArgumentException if fpath is malformed
+ * @throws ISOException on dataset access errors
+ */
+ public List findDatasetElements(String fpath) throws ISOException {
+ int[] path = parsePath(fpath);
+ PathTarget target = locate(path);
+ if (target == null || target.datasetField() == null)
+ return Collections.emptyList();
+ return target.msg().datasetElements(target.datasetField(), path, target.index());
+ }
/**
* Return the String value associated with the given ISOField number
* @param fldno the Field Number
@@ -962,17 +1152,34 @@ public Object clone(int ... fields) {
}
/**
- * Partially clone an ISOMsg by field paths
+ * Partially clone an ISOMsg by field paths.
+ *
+ * Paths use the grammar of {@link #findValue(String)}. Absent paths are
+ * skipped. A dataset element path (i.e. 55.0x9F26, 104.0x71.0x01) copies every
+ * occurrence of the element, keeping dataset identifiers, formats and constructed
+ * flags; elements are added in path order, and a path repeated or covered by an
+ * earlier path replaces the copied elements instead of duplicating them.
+ *
* @param fpaths string array of field paths to copy
* @return new ISOMsg instance
+ * @throws IllegalArgumentException if a path is malformed
*/
public ISOMsg clone(String ... fpaths) {
try {
ISOMsg m = (ISOMsg) super.clone();
m.fields = new TreeMap();
+ Map createdDatasets = new IdentityHashMap<>();
for (String fpath : fpaths) {
+ int[] path = parsePath(fpath);
try {
- ISOComponent component = getComponent(fpath);
+ PathTarget target = locate(path);
+ if (target == null)
+ continue;
+ if (target.datasetField() != null) {
+ m.cloneDatasetElements(target, path, createdDatasets);
+ continue;
+ }
+ ISOComponent component = target.msg().getComponent(path[target.index()]);
if (component instanceof ISOMsg || component instanceof ISODatasetField) {
m.set(fpath, cloneComponent(component));
} else if (component != null) {
@@ -982,6 +1189,11 @@ public ISOMsg clone(String ... fpaths) {
//should never happen
}
}
+ // datasets created only to keep repeated dataset identifiers aligned
+ for (Map.Entry e : createdDatasets.entrySet()) {
+ if (e.getKey().isEmpty())
+ e.getValue().removeDataset(e.getKey());
+ }
return m;
} catch (CloneNotSupportedException e) {
throw new InternalError();
@@ -1619,105 +1831,276 @@ private int parseInt (String s) {
}
private boolean setDatasetPath(String fpath, Object value) throws ISOException {
+ DatasetAddress address = datasetAddress(fpath);
+ if (address == null)
+ return false;
+ putDatasetElement(address, value);
+ return true;
+ }
+
+ private boolean unsetDatasetPath(String fpath) throws ISOException {
+ DatasetAddress address = datasetAddress(fpath);
+ if (address == null)
+ return false;
+ removeDatasetElement(address);
+ return true;
+ }
+
+ /**
+ * Resolves a lenient top-level dataset path against this message's packager.
+ *
+ * @return the address, or {@code null} if fpath is not a dataset path
+ * @throws ISOException if the field packager is a dataset packager and the
+ * number of segments does not match its dataset envelope
+ */
+ private DatasetAddress datasetAddress(String fpath) throws ISOException {
StringTokenizer st = new StringTokenizer(fpath, ".");
if (st.countTokens() < 2)
- return false;
+ return null;
int fieldNo = parseInt(st.nextToken());
- ISOFieldPackager fp = null;
- if (packager instanceof ISOBasePackager) {
- fp = ((ISOBasePackager) packager).getFieldPackager(fieldNo);
- }
- if (!(fp instanceof DatasetFieldPackager))
- return false;
+ Boolean envelope = datasetEnvelope(fieldNo);
+ if (envelope == null)
+ return null;
+ int expected = envelope ? 2 : 1;
+ if (st.countTokens() != expected)
+ throw new ISOException(
+ "Path '" + fpath + "' does not match the dataset " + (envelope ? "envelope" : "layout")
+ + " of field " + fieldNo + " (expected " + expected + " segment" + (expected > 1 ? "s" : "")
+ + " after the field)"
+ );
+ int datasetId = envelope ? parseInt(st.nextToken()) : fieldNo;
+ int elementId = parseInt(st.nextToken());
+ return new DatasetAddress(fieldNo, envelope, datasetId, elementId);
+ }
- DatasetFieldPackager dfp = (DatasetFieldPackager) fp;
- ISODatasetPackager datasetPackager = dfp.getISODatasetPackager();
- int datasetId;
- int elementId;
+ /**
+ * Resolves the dataset element addressed by the segments of a strict path that
+ * follow the dataset field at {@code path[index]}, held by this message.
+ *
+ * @return the address, or {@code null} if the segments do not address an element
+ */
+ private DatasetAddress datasetAddress(int[] path, int index) {
+ int fieldNo = path[index];
+ int remaining = path.length - index - 1;
+ Boolean envelope = datasetEnvelope(fieldNo);
+ if (envelope == null)
+ envelope = remaining == 2;
+ if (envelope)
+ return remaining == 2 ? new DatasetAddress(fieldNo, true, path[index + 1], path[index + 2]) : null;
+ return remaining == 1 ? new DatasetAddress(fieldNo, false, fieldNo, path[index + 1]) : null;
+ }
- if (!datasetPackager.hasDatasetEnvelope()) {
- if (st.countTokens() != 1)
- return false;
- datasetId = fieldNo;
- elementId = parseInt(st.nextToken());
- } else {
- if (st.countTokens() != 2)
- return false;
- datasetId = parseInt(st.nextToken());
- elementId = parseInt(st.nextToken());
- }
+ /**
+ * @return whether this message's packager defines a dataset envelope for
+ * the field, or {@code null} if it does not define a dataset field there
+ */
+ private Boolean datasetEnvelope(int fieldNo) {
+ if (packager instanceof ISOBasePackager bp
+ && bp.getFieldPackager(fieldNo) instanceof DatasetFieldPackager dfp)
+ return dfp.getISODatasetPackager().hasDatasetEnvelope();
+ return null;
+ }
+ private void putDatasetElement(DatasetAddress address, Object value) throws ISOException {
+ int elementId = address.elementId();
+ ISOComponent elementComponent = toDatasetComponent(elementId, value);
ISODatasetField field;
- ISOComponent component = getComponent(fieldNo);
+ ISOComponent component = getComponent(address.fieldNo());
if (component == null) {
- field = new ISODatasetField(fieldNo);
+ field = new ISODatasetField(address.fieldNo());
set(field);
- } else if (component instanceof ISODatasetField) {
- field = (ISODatasetField) component;
+ } else if (component instanceof ISODatasetField f) {
+ field = f;
} else {
- throw new ISOException("Field " + fieldNo + " is not a dataset field");
+ throw new ISOException("Field " + address.fieldNo() + " is not a dataset field");
}
- ISODataset dataset = (ISODataset) field.getDataset(datasetId);
+ ISODataset dataset = (ISODataset) field.getDataset(address.datasetId());
if (dataset == null) {
- dataset = new ISODataset(datasetId, datasetId <= 0x70 ? DatasetFormat.TLV : DatasetFormat.DBM);
+ // without an envelope the dataset is raw TLV (i.e. ICC data);
+ // otherwise ISO 8583:2023 identifiers above 0x70 are DBM
+ DatasetFormat format = address.envelope() && address.datasetId() > 0x70 ? DatasetFormat.DBM : DatasetFormat.TLV;
+ dataset = new ISODataset(address.datasetId(), format);
field.addDataset(dataset);
}
- dataset.putElement(elementId, toDatasetComponent(elementId, value));
- return true;
+ dataset.putElement(elementId, elementComponent,
+ dataset.getFormat() == DatasetFormat.TLV && DatasetElement.isConstructedTag(elementId));
}
- private boolean unsetDatasetPath(String fpath) throws ISOException {
- StringTokenizer st = new StringTokenizer(fpath, ".");
- if (st.countTokens() < 2)
- return false;
+ private void removeDatasetElement(DatasetAddress address) throws ISOException {
+ ISOComponent component = getComponent(address.fieldNo());
+ if (component == null)
+ return;
+ if (!(component instanceof ISODatasetField field))
+ throw new ISOException("Field " + address.fieldNo() + " is not a dataset field");
+
+ for (Dataset dataset : field.getDatasets(address.datasetId())) {
+ if (dataset instanceof ISODataset isoDataset) {
+ isoDataset.removeElement(address.elementId());
+ if (isoDataset.isEmpty())
+ field.removeDataset(isoDataset);
+ }
+ }
+ if (!field.hasDatasets())
+ unset(address.fieldNo());
+ }
- int fieldNo = parseInt(st.nextToken());
- ISOFieldPackager fp = null;
- if (packager instanceof ISOBasePackager) {
- fp = ((ISOBasePackager) packager).getFieldPackager(fieldNo);
+ private void writeDatasetElement(String fpath, boolean envelope, int datasetId, int elementId, Object value)
+ throws ISOException {
+ if (datasetId < 0 || elementId < 0)
+ throw new IllegalArgumentException("Invalid dataset element " + datasetId + "/" + elementId + " for '" + fpath + "'");
+ int[] path = parsePath(fpath);
+ ISOMsg m = this;
+ for (int i = 0; i < path.length - 1; i++) {
+ ISOComponent c = m.getComponent(path[i]);
+ if (c instanceof ISOMsg sub) {
+ m = sub;
+ } else if (value == null) {
+ return; // nothing to remove
+ } else if (c != null) {
+ throw new ISOException("Field " + path[i] + " in path '" + fpath + "' is not a composite field");
+ } else {
+ ISOMsg sub = new ISOMsg(path[i]);
+ m.set(sub);
+ m = sub;
+ }
}
- if (!(fp instanceof DatasetFieldPackager))
- return false;
+ int fieldNo = path[path.length - 1];
+ DatasetAddress address = new DatasetAddress(fieldNo, envelope, envelope ? datasetId : fieldNo, elementId);
+ if (value == null)
+ m.removeDatasetElement(address);
+ else
+ m.putDatasetElement(address, value);
+ }
- DatasetFieldPackager dfp = (DatasetFieldPackager) fp;
- ISODatasetPackager datasetPackager = dfp.getISODatasetPackager();
- int datasetId;
- int elementId;
+ private List datasetElements(ISODatasetField field, int[] path, int index) {
+ DatasetAddress address = datasetAddress(path, index);
+ if (address == null)
+ return Collections.emptyList();
+ List elements = new ArrayList<>();
+ for (Dataset dataset : field.getDatasets(address.datasetId()))
+ elements.addAll(dataset.getElements(address.elementId()));
+ return Collections.unmodifiableList(elements);
+ }
- if (!datasetPackager.hasDatasetEnvelope()) {
- if (st.countTokens() != 1)
- return false;
- datasetId = fieldNo;
- elementId = parseInt(st.nextToken());
+ /**
+ * Copies the dataset element addressed by {@code source} into this (cloned) message.
+ * Same-identifier datasets are matched by position, so repeated datasets stay apart.
+ */
+ private void cloneDatasetElements(PathTarget source, int[] path, Map createdDatasets)
+ throws ISOException {
+ DatasetAddress address = source.msg().datasetAddress(path, source.index());
+ if (address == null)
+ return;
+ int elementId = address.elementId();
+ List sourceDatasets = source.datasetField().getDatasets(address.datasetId());
+ if (sourceDatasets.stream().allMatch(d -> d.getElements(elementId).isEmpty()))
+ return;
+
+ ISOMsg m = this;
+ for (int i = 0; i < source.index(); i++) {
+ ISOComponent c = m.getComponent(path[i]);
+ ISOMsg sub;
+ if (c instanceof ISOMsg msg) {
+ sub = msg;
+ } else {
+ sub = new ISOMsg(path[i]);
+ m.set(sub);
+ }
+ m = sub;
+ }
+ ISODatasetField field;
+ ISOComponent c = m.getComponent(address.fieldNo());
+ if (c instanceof ISODatasetField f) {
+ field = f;
+ } else if (c == null) {
+ field = new ISODatasetField(address.fieldNo());
+ m.set(field);
} else {
- if (st.countTokens() != 2)
- return false;
- datasetId = parseInt(st.nextToken());
- elementId = parseInt(st.nextToken());
+ return; // cannot happen: the clone mirrors the source hierarchy
}
- ISOComponent component = getComponent(fieldNo);
- if (component == null)
- return true;
- if (!(component instanceof ISODatasetField))
- throw new ISOException("Field " + fieldNo + " is not a dataset field");
-
- ISODatasetField field = (ISODatasetField) component;
- ISODataset dataset = (ISODataset) field.getDataset(datasetId);
- if (dataset == null)
- return true;
-
- dataset.removeElement(elementId);
- if (dataset.isEmpty()) {
- field.removeDataset(dataset);
- if (!field.hasDatasets())
- unset(fieldNo);
+ List targetDatasets = field.getDatasets(address.datasetId());
+ for (int k = 0; k < sourceDatasets.size(); k++) {
+ Dataset src = sourceDatasets.get(k);
+ ISODataset dst;
+ if (k < targetDatasets.size() && targetDatasets.get(k) instanceof ISODataset d) {
+ dst = d;
+ } else {
+ dst = new ISODataset(address.datasetId(), src.getFormat());
+ field.addDataset(dst);
+ createdDatasets.put(dst, field);
+ }
+ dst.removeElement(elementId);
+ for (DatasetElement element : src.getElements(elementId))
+ dst.addElement(elementId, cloneDatasetComponent(element.getComponent()), element.isConstructed());
}
- return true;
}
+ /**
+ * Walks a strict path.
+ *
+ * @return the message holding the component addressed by the last segment
+ * ({@code datasetField} is {@code null}), the message holding the dataset
+ * field at {@code path[index]} that the path descends into, or {@code null}
+ * if the path runs through an absent or leaf field
+ */
+ private PathTarget locate(int[] path) {
+ ISOMsg m = this;
+ for (int i = 0; i < path.length - 1; i++) {
+ ISOComponent c = m.getComponent(path[i]);
+ if (c instanceof ISODatasetField field)
+ return new PathTarget(m, i, field);
+ if (!(c instanceof ISOMsg sub))
+ return null;
+ m = sub;
+ }
+ return new PathTarget(m, path.length - 1, null);
+ }
+
+ /**
+ * Parses a strict field path. Segments are decimal ({@code 62}) or
+ * {@code 0x}-prefixed hexadecimal ({@code 0x9F26}) numbers in the range
+ * {@code 0..Integer.MAX_VALUE}.
+ *
+ * @throws IllegalArgumentException if fpath is malformed
+ */
+ private static int[] parsePath(String fpath) {
+ if (fpath == null || fpath.isEmpty())
+ throw new IllegalArgumentException("Invalid path '" + fpath + "': empty path");
+ String[] segments = fpath.split("\\.", -1);
+ int[] path = new int[segments.length];
+ for (int i = 0; i < segments.length; i++)
+ path[i] = parsePathSegment(fpath, segments[i], i + 1);
+ return path;
+ }
+
+ private static int parsePathSegment(String fpath, String segment, int position) {
+ if (segment.isEmpty())
+ throw new IllegalArgumentException("Invalid path '" + fpath + "': empty segment at position " + position);
+ boolean hex = segment.startsWith("0x");
+ String digits = hex ? segment.substring(2) : segment;
+ boolean valid = !digits.isEmpty();
+ for (int i = 0; valid && i < digits.length(); i++) {
+ char ch = digits.charAt(i);
+ valid = ch >= '0' && ch <= '9' || hex && (ch >= 'a' && ch <= 'f' || ch >= 'A' && ch <= 'F');
+ }
+ if (!valid)
+ throw new IllegalArgumentException(
+ "Invalid path '" + fpath + "': invalid segment '" + segment + "' at position " + position);
+ try {
+ return Integer.parseInt(digits, hex ? 16 : 10);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "Invalid path '" + fpath + "': segment '" + segment + "' at position " + position + " is out of range", e);
+ }
+ }
+
+ private record PathTarget(ISOMsg msg, int index, ISODatasetField datasetField) { }
+
+ private record DatasetAddress(int fieldNo, boolean envelope, int datasetId, int elementId) { }
+
private ISOComponent toDatasetComponent(int elementId, Object value) throws ISOException {
if (value instanceof ISOComponent) {
ISOComponent component = (ISOComponent) value;
diff --git a/jpos/src/main/java/org/jpos/iso/packager/DatasetPackager.java b/jpos/src/main/java/org/jpos/iso/packager/DatasetPackager.java
index b41fc8bf37..446d46e75a 100644
--- a/jpos/src/main/java/org/jpos/iso/packager/DatasetPackager.java
+++ b/jpos/src/main/java/org/jpos/iso/packager/DatasetPackager.java
@@ -247,7 +247,7 @@ protected ISODataset unpackTLV(int identifier, byte[] content) throws ISOExcepti
ISODataset dataset = new ISODataset(identifier, DatasetFormat.TLV);
for (TLVMsg tag : tlv.getTags()) {
ISOBinaryField field = new ISOBinaryField(tag.getTag(), tag.getValue());
- dataset.addElement(tag.getTag(), field, isConstructedTag(tag.getTag()));
+ dataset.addElement(tag.getTag(), field, DatasetElement.isConstructedTag(tag.getTag()));
}
return dataset;
}
@@ -450,7 +450,7 @@ private void unpackTLVContinuation(int identifier, ISODataset dataset, byte[] co
throw new ISOException(String.format("Invalid DBM TLV continuation in dataset %02X", identifier), e);
}
for (TLVMsg tag : tlv.getTags()) {
- dataset.addElement(tag.getTag(), new ISOBinaryField(tag.getTag(), tag.getValue()), isConstructedTag(tag.getTag()));
+ dataset.addElement(tag.getTag(), new ISOBinaryField(tag.getTag(), tag.getValue()), DatasetElement.isConstructedTag(tag.getTag()));
}
}
@@ -511,15 +511,6 @@ private boolean isBitSet(byte value, int bitNumber) {
return ((value >> (8 - bitNumber)) & 0x01) == 0x01;
}
- private boolean isConstructedTag(int tag) {
- String hexTag = Integer.toHexString(tag);
- if ((hexTag.length() & 0x01) == 1) {
- hexTag = '0' + hexTag;
- }
- byte[] tagBytes = ISOUtil.hex2byte(hexTag);
- return (tagBytes[0] & 0x20) == 0x20;
- }
-
private static class DBMBitmap {
private final List elements;
private final boolean tlvContinuation;
diff --git a/jpos/src/main/java/org/jpos/iso/packager/XMLPackager.java b/jpos/src/main/java/org/jpos/iso/packager/XMLPackager.java
index 7b20e8ed2c..6e0947a88f 100644
--- a/jpos/src/main/java/org/jpos/iso/packager/XMLPackager.java
+++ b/jpos/src/main/java/org/jpos/iso/packager/XMLPackager.java
@@ -280,7 +280,7 @@ else if (TYPE_AMOUNT.equals (type)) {
throw new SAXException("invalid dataset element");
int elementId = dataset.getFormat() == DatasetFormat.TLV ? parseHexId(id) : parseDecimalOrHexId(id);
byte[] bytes = value == null ? new byte[0] : ISOUtil.hex2byte(value.getBytes(), 0, value.length() / 2);
- dataset.addElement(elementId, new ISOBinaryField(elementId, bytes), dataset.getFormat() == DatasetFormat.TLV && isConstructedTag(elementId));
+ dataset.addElement(elementId, new ISOBinaryField(elementId, bytes), dataset.getFormat() == DatasetFormat.TLV && DatasetElement.isConstructedTag(elementId));
} else if (HEADER_TAG.equals (name)) {
BaseHeader bh = new BaseHeader();
bh.setAsciiEncoding (ASCII_ENCODING.equalsIgnoreCase(atts.getValue(ENCODING_ATTR)));
@@ -438,12 +438,4 @@ private int parseHexId(String id) throws SAXException {
throw new SAXException("Invalid hex id " + id, ex);
}
}
-
- private boolean isConstructedTag(int tag) {
- String hexTag = Integer.toHexString(tag);
- if ((hexTag.length() & 0x01) == 1)
- hexTag = "0" + hexTag;
- byte[] tagBytes = ISOUtil.hex2byte(hexTag);
- return (tagBytes[0] & 0x20) == 0x20;
- }
}
diff --git a/jpos/src/test/java/org/jpos/iso/ISOMsgPathTest.java b/jpos/src/test/java/org/jpos/iso/ISOMsgPathTest.java
new file mode 100644
index 0000000000..839586e7f5
--- /dev/null
+++ b/jpos/src/test/java/org/jpos/iso/ISOMsgPathTest.java
@@ -0,0 +1,554 @@
+/*
+ * jPOS Project [http://jpos.org]
+ * Copyright (C) 2000-2026 jPOS Software SRL
+ *
+ * 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.iso;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.jpos.iso.packager.GenericPackager;
+import org.jpos.iso.packager.XMLPackager;
+import org.jpos.tlv.TLVList;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Strict field path readers, dataset element writers and dataset-aware
+ * {@link ISOMsg#clone(String...)}.
+ */
+public class ISOMsgPathTest {
+ private static final byte[] AC = ISOUtil.hex2byte("1122334455667788");
+ private static final byte[] IAD = ISOUtil.hex2byte("06011203A0B800");
+ // issuer script templates: 9F18 script id + 86 command
+ private static final byte[] SCRIPT1 = ISOUtil.hex2byte("9F180411223344" + "86058418000000");
+ private static final byte[] SCRIPT2 = ISOUtil.hex2byte("9F180455667788" + "86058424000000");
+
+ // ---- path grammar
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "", ".", "62..1", ".62", "62.", "62.1.", "6a", "-1", "+1", " 1", "1 ", "62. 1",
+ "0X1F", "0x", "0x.1", "0xG1", "0x-1", "0x+1", "2147483648", "0x80000000", "99999999999",
+ "٣", "1"
+ })
+ public void malformedPathsThrow(String fpath) throws Exception {
+ for (ISOMsg m : List.of(new ISOMsg(), populated())) {
+ assertThrows(IllegalArgumentException.class, () -> m.findValue(fpath), fpath);
+ assertThrows(IllegalArgumentException.class, () -> m.findDatasetElements(fpath), fpath);
+ assertThrows(IllegalArgumentException.class, () -> m.clone(fpath), fpath);
+ assertThrows(IllegalArgumentException.class, () -> m.setDatasetElement(fpath, 0x9F26, AC), fpath);
+ assertThrows(IllegalArgumentException.class, () -> m.unsetDatasetElement(fpath, 0x9F26), fpath);
+ }
+ }
+
+ @Test
+ public void nullPathThrows() {
+ ISOMsg m = new ISOMsg();
+ assertThrows(IllegalArgumentException.class, () -> m.findValue(null));
+ assertThrows(IllegalArgumentException.class, () -> m.findDatasetElements(null));
+ }
+
+ @Test
+ public void malformedPathValidityDoesNotDependOnContents() {
+ ISOMsg m = new ISOMsg();
+ m.set(62, "leaf");
+ // hasField stops at the leaf and never parses "x"; findValue always does
+ assertFalse(m.hasField("62.x"));
+ assertThrows(IllegalArgumentException.class, () -> m.findValue("62.x"));
+ assertThrows(IllegalArgumentException.class, () -> m.findValue("64.x"));
+ }
+
+ @Test
+ public void malformedPathMessageNamesSegment() {
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new ISOMsg().findValue("62..1"));
+ assertEquals("Invalid path '62..1': empty segment at position 2", e.getMessage());
+ e = assertThrows(IllegalArgumentException.class, () -> new ISOMsg().findValue("62.0X1"));
+ assertEquals("Invalid path '62.0X1': invalid segment '0X1' at position 2", e.getMessage());
+ e = assertThrows(IllegalArgumentException.class, () -> new ISOMsg().findValue("0x80000000"));
+ assertEquals("Invalid path '0x80000000': segment '0x80000000' at position 1 is out of range", e.getMessage());
+ }
+
+ @Test
+ public void validPathForms() throws Exception {
+ ISOMsg m = new ISOMsg();
+ m.set(7, "0123456789");
+ m.set(10, "61000000");
+ assertEquals(Optional.of("0123456789"), m.findValue("007"));
+ assertEquals(Optional.of("61000000"), m.findValue("0x0A"));
+ assertEquals(Optional.of("61000000"), m.findValue("0x0a"));
+ assertEquals(Optional.empty(), m.findValue("2147483647"));
+ assertEquals(Optional.empty(), m.findValue("0x7FFFFFFF"));
+ }
+
+ // ---- strict walk
+
+ @Test
+ public void strictWalk() throws Exception {
+ ISOMsg m = populated();
+ assertEquals(Optional.of("0100"), m.findValue("0"));
+ assertEquals(Optional.of("000000"), m.findValue("3"));
+ assertArrayEquals(ISOUtil.hex2byte("CAFEBABE"), (byte[]) m.findValue("52").orElseThrow());
+ assertEquals(Optional.of("value63.2.3"), m.findValue("63.2.3"));
+ assertSame(m.getComponent(63), m.findValue("63").orElseThrow());
+ assertSame(m.getComponent("63.2"), m.findValue("63.2").orElseThrow());
+
+ assertEquals(Optional.empty(), m.findValue("4"), "absent");
+ assertEquals(Optional.empty(), m.findValue("64.1"), "absent first level");
+ assertEquals(Optional.empty(), m.findValue("63.9.1"), "absent second level");
+ assertEquals(Optional.empty(), m.findValue("63.2.9"), "absent leaf");
+ assertEquals(Optional.empty(), m.findValue("62.1"), "through a leaf");
+ assertEquals(Optional.empty(), m.findValue("63.2.3.1"), "through a nested leaf");
+ assertEquals(Optional.empty(), new ISOMsg().findValue("0"), "absent MTI");
+
+ // the lenient readers disagree on a path through a leaf
+ assertThrows(ISOException.class, () -> m.getValue("62.1"));
+ assertNull(m.getString("62.1"));
+ assertNull(m.getComponent("62.1"));
+ }
+
+ @Test
+ public void findDatasetElementsOnNonDatasetPaths() throws Exception {
+ ISOMsg m = populated();
+ assertTrue(m.findDatasetElements("3").isEmpty());
+ assertTrue(m.findDatasetElements("62.1").isEmpty());
+ assertTrue(m.findDatasetElements("63.2.3").isEmpty());
+ assertTrue(m.findDatasetElements("64.1").isEmpty());
+ assertTrue(m.findDatasetElements("55").isEmpty(), "bare dataset field");
+ }
+
+ // ---- dataset descent
+
+ @Test
+ public void datasetPathsWithPackager() throws Exception {
+ GenericPackager packager = cmfv3();
+ ISOMsg m = roundTrip(packager, cmfMsg(packager)
+ .with("55.0x9F26", AC)
+ .with("55.0x9F10", IAD)
+ .with("104.0x01.0xDF01", ISOUtil.hex2byte("CAFEBABE"))
+ .with("49.0x71.2", "1234"));
+
+ assertArrayEquals(AC, (byte[]) m.findValue("55.0x9F26").orElseThrow());
+ assertArrayEquals(IAD, (byte[]) m.findValue("55.0x9F10").orElseThrow());
+ assertArrayEquals(ISOUtil.hex2byte("CAFEBABE"), (byte[]) m.findValue("104.0x01.0xDF01").orElseThrow());
+ assertEquals(Optional.of("1234"), m.findValue("49.0x71.2"));
+
+ assertEquals(Optional.empty(), m.findValue("55.0x9F27"), "absent tag");
+ assertEquals(Optional.empty(), m.findValue("104.0x02.0xDF01"), "absent dataset");
+ assertEquals(Optional.empty(), m.findValue("104.0x01.0xDF01.1"), "more than two segments");
+ assertEquals(Optional.empty(), m.findValue("104.0xDF01"), "envelope field, one segment");
+ assertEquals(Optional.empty(), m.findValue("55.55.0x9F26"), "no-envelope field, two segments");
+ assertTrue(m.findDatasetElements("55.55.0x9F26").isEmpty());
+
+ // without the packager the layout is inferred from the segment count, so the
+ // dataset that DE 55 stores under its own field number is reachable both ways
+ m.setPackager(null);
+ assertArrayEquals(AC, (byte[]) m.findValue("55.0x9F26").orElseThrow());
+ assertArrayEquals(AC, (byte[]) m.findValue("55.55.0x9F26").orElseThrow());
+ assertEquals(Optional.empty(), m.findValue("104.0xDF01"));
+ }
+
+ @Test
+ public void bareDatasetFieldValue() throws Exception {
+ ISOMsg m = populated();
+ ISODatasetField f55 = (ISODatasetField) m.getComponent(55);
+ assertEquals(f55.getDatasets(), m.findValue("55").orElseThrow());
+ }
+
+ @Test
+ public void datasetPathsWithoutPackager() throws Exception {
+ ISOMsg m = populated();
+ ISODatasetField f104 = new ISODatasetField(104);
+ f104.addDataset(new ISODataset(0x01, DatasetFormat.TLV).with(0xDF01, "CAFE".getBytes()));
+ f104.addDataset(new ISODataset(0x71, DatasetFormat.DBM).with(2, "1234"));
+ m.set(f104);
+
+ assertArrayEquals(AC, (byte[]) m.findValue("55.0x9F26").orElseThrow());
+ assertArrayEquals("CAFE".getBytes(), (byte[]) m.findValue("104.0x01.0xDF01").orElseThrow());
+ assertEquals(Optional.of("1234"), m.findValue("104.0x71.2"));
+ assertEquals(Optional.empty(), m.findValue("104.0xDF01"), "inferred no-envelope: no dataset 104");
+ assertEquals(Optional.empty(), m.findValue("104.0x01.0xDF01.1"));
+ }
+
+ @Test
+ public void nestedDatasetField() throws Exception {
+ ISOMsg m = new ISOMsg("0100");
+ ISOMsg f127 = new ISOMsg(127);
+ ISODatasetField f55 = new ISODatasetField(55);
+ f55.addDataset(new ISODataset(55, DatasetFormat.TLV).with(0x9F26, AC));
+ f127.set(f55);
+ m.set(f127);
+
+ assertArrayEquals(AC, (byte[]) m.findValue("127.55.0x9F26").orElseThrow());
+ assertArrayEquals(AC, (byte[]) m.findValue("127.55.55.0x9F26").orElseThrow(), "inferred envelope");
+
+ // the sub-message's packager decides the layout of its dataset fields
+ f127.setPackager(cmfv3());
+ assertArrayEquals(AC, (byte[]) m.findValue("127.55.0x9F26").orElseThrow());
+ assertEquals(Optional.empty(), m.findValue("127.55.55.0x9F26"));
+ }
+
+ @Test
+ public void xmlRoundTrip() throws Exception {
+ ISOMsg m = populated();
+ m.setDatasetElement("104", 0x71, 2, "1234");
+ m.setPackager(new XMLPackager());
+ ISOMsg unpacked = new ISOMsg();
+ unpacked.setPackager(new XMLPackager());
+ unpacked.unpack(m.pack());
+
+ assertInstanceOf(ISODatasetField.class, unpacked.getComponent(55));
+ assertArrayEquals(AC, (byte[]) unpacked.findValue("55.0x9F26").orElseThrow());
+ assertArrayEquals("1234".getBytes(), (byte[]) unpacked.findValue("104.0x71.2").orElseThrow());
+ assertEquals(Optional.of("value63.2.3"), unpacked.findValue("63.2.3"));
+ }
+
+ @Test
+ public void setWithoutPackagerCreatesPlainComposite() throws Exception {
+ ISOMsg m = new ISOMsg("0100");
+ m.set("55.0x9F26", AC);
+ assertInstanceOf(ISOMsg.class, m.getComponent(55));
+ assertArrayEquals(AC, (byte[]) m.findValue("55.0x9F26").orElseThrow());
+ assertTrue(m.findDatasetElements("55.0x9F26").isEmpty());
+ }
+
+ // ---- repeats
+
+ @Test
+ public void repeatedTags() throws Exception {
+ ISOMsg m = new ISOMsg("0100");
+ ISODatasetField f55 = new ISODatasetField(55);
+ ISODataset icc = new ISODataset(55, DatasetFormat.TLV).with(0x9F26, AC);
+ icc.addElement(0x71, new ISOBinaryField(0x71, SCRIPT1), true);
+ icc.addElement(0x71, new ISOBinaryField(0x71, SCRIPT2), true);
+ f55.addDataset(icc);
+ m.set(f55);
+ m = roundTrip(cmfv3(), m);
+
+ List scripts = m.findDatasetElements("55.0x71");
+ assertEquals(2, scripts.size());
+ assertArrayEquals(SCRIPT1, scripts.get(0).getBytes());
+ assertArrayEquals(SCRIPT2, scripts.get(1).getBytes());
+ assertTrue(scripts.get(0).isConstructed());
+ assertTrue(scripts.get(1).isConstructed());
+ assertArrayEquals(SCRIPT1, (byte[]) m.findValue("55.0x71").orElseThrow(), "first occurrence");
+ assertThrows(UnsupportedOperationException.class, () -> scripts.add(scripts.get(0)));
+
+ // constructed elements carry their encoded children
+ TLVList children = new TLVList();
+ children.unpack(scripts.get(1).getBytes());
+ assertArrayEquals(ISOUtil.hex2byte("55667788"), children.find(0x9F18).getValue());
+ assertArrayEquals(ISOUtil.hex2byte("8424000000"), children.find(0x86).getValue());
+ }
+
+ @Test
+ public void repeatedDatasets() throws Exception {
+ ISOMsg m = new ISOMsg("0100");
+ ISODatasetField f104 = new ISODatasetField(104);
+ f104.addDataset(new ISODataset(0x01, DatasetFormat.TLV).with(0xDF02, "A".getBytes()));
+ f104.addDataset(new ISODataset(0x01, DatasetFormat.TLV).with(0xDF01, "B".getBytes()));
+ f104.addDataset(new ISODataset(0x01, DatasetFormat.TLV).with(0xDF01, "C".getBytes()));
+ m.set(f104);
+
+ assertNull(f104.getValue(0x01, 0xDF01), "ISODatasetField only reads the first dataset");
+ assertArrayEquals("B".getBytes(), (byte[]) m.findValue("104.0x01.0xDF01").orElseThrow());
+ List elements = m.findDatasetElements("104.0x01.0xDF01");
+ assertEquals(2, elements.size());
+ assertArrayEquals("B".getBytes(), elements.get(0).getBytes());
+ assertArrayEquals("C".getBytes(), elements.get(1).getBytes());
+ }
+
+ // ---- setDatasetElement / withDatasetElement / unsetDatasetElement
+
+ @Test
+ public void setDatasetElementWithoutPackager() throws Exception {
+ ISOMsg m = new ISOMsg("0100")
+ .withDatasetElement("55", 0x9F26, AC)
+ .withDatasetElement("55", 0x71, SCRIPT1)
+ .withDatasetElement("104", 0x01, 0xDF01, "CAFE".getBytes())
+ .withDatasetElement("49", 0x71, 2, "1234")
+ .withDatasetElement("49", 0x71, 1, new ISOField(99, "1"));
+
+ ISODatasetField f55 = (ISODatasetField) m.getComponent(55);
+ ISODataset icc = (ISODataset) f55.getDataset(55);
+ assertEquals(DatasetFormat.TLV, icc.getFormat());
+ assertFalse(icc.getElement(0x9F26).isConstructed());
+ assertTrue(icc.getElement(0x71).isConstructed());
+
+ ISODatasetField f49 = (ISODatasetField) m.getComponent(49);
+ ISODataset verification = (ISODataset) f49.getDataset(0x71);
+ assertEquals(DatasetFormat.DBM, verification.getFormat());
+ assertEquals(1, verification.getComponent(1).getFieldNumber());
+ assertFalse(verification.getElement(1).isConstructed(), "DBM elements are never constructed");
+ assertEquals(DatasetFormat.TLV, ((ISODatasetField) m.getComponent(104)).getDataset(0x01).getFormat());
+
+ // the result packs with a dataset-aware packager
+ ISOMsg unpacked = roundTrip(cmfv3(), m);
+ assertArrayEquals(AC, (byte[]) unpacked.findValue("55.0x9F26").orElseThrow());
+ assertArrayEquals(SCRIPT1, (byte[]) unpacked.findValue("55.0x71").orElseThrow());
+ assertArrayEquals("CAFE".getBytes(), (byte[]) unpacked.findValue("104.0x01.0xDF01").orElseThrow());
+ assertEquals(Optional.of("1"), unpacked.findValue("49.0x71.1"));
+ assertEquals(Optional.of("1234"), unpacked.findValue("49.0x71.2"));
+ }
+
+ @Test
+ public void setDatasetElementReplacesAndCreatesHierarchy() throws Exception {
+ ISOMsg m = new ISOMsg("0100");
+ m.setDatasetElement("127.55", 0x9F26, AC);
+ m.setDatasetElement("127.55", 0x9F26, IAD);
+ assertInstanceOf(ISOMsg.class, m.getComponent(127));
+ List elements = m.findDatasetElements("127.55.0x9F26");
+ assertEquals(1, elements.size());
+ assertArrayEquals(IAD, elements.get(0).getBytes());
+
+ // a no-envelope field above 0x70 still gets a TLV dataset
+ m.setDatasetElement("120", 0x9F26, AC);
+ assertEquals(DatasetFormat.TLV, ((ISODatasetField) m.getComponent(120)).getDataset(120).getFormat());
+ }
+
+ @Test
+ public void setDatasetElementErrors() throws Exception {
+ ISOMsg m = populated();
+ assertThrows(ISOException.class, () -> m.setDatasetElement("62", 0x9F26, AC), "leaf target");
+ assertThrows(ISOException.class, () -> m.setDatasetElement("63", 0x9F26, AC), "composite target");
+ assertThrows(ISOException.class, () -> m.setDatasetElement("62.55", 0x9F26, AC), "through a leaf");
+ assertThrows(ISOException.class, () -> m.setDatasetElement("55.1", 0x9F26, AC), "through a dataset field");
+ assertThrows(ISOException.class, () -> m.setDatasetElement("56", 0x9F26, 42), "unsupported value");
+ assertThrows(IllegalArgumentException.class, () -> m.setDatasetElement("56", -1, AC));
+ assertThrows(IllegalArgumentException.class, () -> m.setDatasetElement("104", -1, 1, AC));
+ assertEquals("leaf62", m.getString(62));
+ assertFalse(m.hasField(56));
+ }
+
+ @Test
+ public void unsetDatasetElement() throws Exception {
+ ISOMsg m = new ISOMsg("0100");
+ ISODatasetField f104 = new ISODatasetField(104);
+ f104.addDataset(new ISODataset(0x01, DatasetFormat.TLV).with(0xDF01, "A".getBytes()).with(0xDF02, "B".getBytes()));
+ f104.addDataset(new ISODataset(0x01, DatasetFormat.TLV).with(0xDF01, "C".getBytes()));
+ m.set(f104);
+
+ m.unsetDatasetElement("104", 0x01, 0xDF01);
+ assertTrue(m.findDatasetElements("104.0x01.0xDF01").isEmpty(), "every occurrence removed");
+ assertEquals(1, f104.getDatasets().size(), "empty dataset removed");
+ m.unsetDatasetElement("104", 0x01, 0xDF02);
+ assertFalse(m.hasField(104), "field removed with its last dataset");
+
+ m.setDatasetElement("55", 0x9F26, AC);
+ m.setDatasetElement("55", 0x9F26, null);
+ assertFalse(m.hasField(55), "null value removes");
+
+ m.set(62, "leaf62");
+ m.unsetDatasetElement("56", 0x9F26);
+ m.unsetDatasetElement("64.55", 0x9F26);
+ m.unsetDatasetElement("62.55", 0x9F26);
+ assertEquals("leaf62", m.getString(62));
+ assertThrows(ISOException.class, () -> m.unsetDatasetElement("62", 0x9F26));
+ }
+
+ // ---- set/unset by dataset path
+
+ @Test
+ public void mismatchedDatasetPathDoesNotReplaceField() throws Exception {
+ ISOMsg m = new ISOMsg("0100");
+ m.setPackager(cmfv3());
+ m.set("104.0x01.0xDF01", "CAFE".getBytes());
+ m.set("55.0x9F26", AC);
+
+ assertThrows(IllegalArgumentException.class, () -> m.set("104.0xDF02", "BABE".getBytes()));
+ assertThrows(IllegalArgumentException.class, () -> m.set("104.0x01.0xDF02.1", "BABE"));
+ assertThrows(ISOException.class, () -> m.set("104.0xDF02", new ISOBinaryField(0, "BABE".getBytes())));
+ assertThrows(IllegalArgumentException.class, () -> m.set("55.0x01.0x9F27", AC));
+ assertThrows(IllegalArgumentException.class, () -> m.unset("104.0xDF01"));
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> m.set("55.0x01.0x9F27", AC));
+ assertEquals("Path '55.0x01.0x9F27' does not match the dataset layout of field 55 (expected 1 segment after the field)", e.getMessage());
+
+ assertInstanceOf(ISODatasetField.class, m.getComponent(104));
+ assertInstanceOf(ISODatasetField.class, m.getComponent(55));
+ assertArrayEquals("CAFE".getBytes(), (byte[]) m.findValue("104.0x01.0xDF01").orElseThrow());
+ assertArrayEquals(AC, (byte[]) m.findValue("55.0x9F26").orElseThrow());
+ }
+
+ @Test
+ public void setByDatasetPathOnHighNoEnvelopeFieldUsesTLV() throws Exception {
+ GenericPackager packager = cmfv3();
+ packager.setFieldPackager(120, packager.getFieldPackager(55));
+ ISOMsg m = new ISOMsg("0100");
+ m.setPackager(packager);
+ m.set("120.0x9F26", AC);
+ m.set("120.0x71", SCRIPT1);
+ ISODataset dataset = (ISODataset) ((ISODatasetField) m.getComponent(120)).getDataset(120);
+ assertEquals(DatasetFormat.TLV, dataset.getFormat());
+ assertTrue(dataset.getElement(0x71).isConstructed());
+ }
+
+ @Test
+ public void unsetByDatasetPathRemovesFromRepeatedDatasets() throws Exception {
+ ISOMsg m = new ISOMsg("0100");
+ m.setPackager(cmfv3());
+ ISODatasetField f104 = new ISODatasetField(104);
+ f104.addDataset(new ISODataset(0x01, DatasetFormat.TLV).with(0xDF01, "A".getBytes()).with(0xDF02, "B".getBytes()));
+ f104.addDataset(new ISODataset(0x01, DatasetFormat.TLV).with(0xDF01, "C".getBytes()));
+ m.set(f104);
+
+ m.unset("104.0x01.0xDF01");
+ assertTrue(m.findDatasetElements("104.0x01.0xDF01").isEmpty());
+ assertArrayEquals("B".getBytes(), (byte[]) m.findValue("104.0x01.0xDF02").orElseThrow());
+ }
+
+ // ---- clone
+
+ @Test
+ public void cloneDatasetElementPath() throws Exception {
+ ISOMsg m = populated();
+ ISOMsg c = m.clone("0", "55.0x9F26");
+
+ assertEquals("0100", c.getMTI());
+ ISODatasetField f55 = (ISODatasetField) c.getComponent(55);
+ assertNotSame(m.getComponent(55), f55);
+ ISODataset icc = (ISODataset) f55.getDataset(55);
+ assertEquals(DatasetFormat.TLV, icc.getFormat());
+ assertEquals(1, icc.getElements().size());
+ byte[] cloned = icc.getBytes(0x9F26);
+ assertArrayEquals(AC, cloned);
+
+ // deep copy: the source element shares its array with the caller
+ byte[] source = AC.clone();
+ m.setDatasetElement("55", 0x9F26, source);
+ c = m.clone("55.0x9F26");
+ source[0] = 0;
+ assertArrayEquals(AC, c.findValue("55.0x9F26").map(byte[].class::cast).orElseThrow());
+ }
+
+ @Test
+ public void cloneDatasetElementsOrderAndOverlap() throws Exception {
+ ISOMsg m = populated();
+
+ ISOMsg c = m.clone("55.0x9F10", "55.0x9F26");
+ List elements = ((ISODatasetField) c.getComponent(55)).getDataset(55).getElements();
+ assertEquals(0x9F10, elements.get(0).getId());
+ assertEquals(0x9F26, elements.get(1).getId());
+
+ c = m.clone("55", "55.0x9F26", "55.0x9F26");
+ assertEquals(1, c.findDatasetElements("55.0x9F26").size(), "overlapping paths replace");
+ assertEquals(2, ((ISODatasetField) c.getComponent(55)).getDataset(55).getElements().size());
+
+ c = m.clone("55.0x9F27", "62.1", "64.0x9F26");
+ assertFalse(c.hasFields(), "absent paths are skipped");
+ }
+
+ @Test
+ public void cloneRepeatedTagsAndDatasets() throws Exception {
+ ISOMsg m = new ISOMsg("0100");
+ ISODatasetField f55 = new ISODatasetField(55);
+ ISODataset icc = new ISODataset(55, DatasetFormat.TLV).with(0x9F26, AC);
+ icc.addElement(0x71, new ISOBinaryField(0x71, SCRIPT1), true);
+ icc.addElement(0x71, new ISOBinaryField(0x71, SCRIPT2), true);
+ f55.addDataset(icc);
+ m.set(f55);
+ ISODatasetField f104 = new ISODatasetField(104);
+ f104.addDataset(new ISODataset(0x01, DatasetFormat.TLV).with(0xDF02, "A".getBytes()));
+ f104.addDataset(new ISODataset(0x01, DatasetFormat.TLV).with(0xDF01, "B".getBytes()));
+ f104.addDataset(new ISODataset(0x71, DatasetFormat.DBM).with(2, "1234"));
+ m.set(f104);
+
+ ISOMsg c = m.clone("55.0x71");
+ List scripts = c.findDatasetElements("55.0x71");
+ assertEquals(2, scripts.size());
+ assertArrayEquals(SCRIPT1, scripts.get(0).getBytes());
+ assertArrayEquals(SCRIPT2, scripts.get(1).getBytes());
+ assertTrue(scripts.get(0).isConstructed() && scripts.get(1).isConstructed());
+
+ c = m.clone("104.0x01.0xDF01");
+ List datasets = ((ISODatasetField) c.getComponent(104)).getDatasets();
+ assertEquals(1, datasets.size(), "dataset without the element is not kept");
+ assertArrayEquals("B".getBytes(), datasets.get(0).getElement(0xDF01).getBytes());
+
+ c = m.clone("104.0x01.0xDF01", "104.0x01.0xDF02", "104.0x71.2");
+ datasets = ((ISODatasetField) c.getComponent(104)).getDatasets(0x01);
+ assertEquals(2, datasets.size(), "same-identifier datasets stay apart");
+ assertArrayEquals("A".getBytes(), datasets.get(0).getElement(0xDF02).getBytes());
+ assertArrayEquals("B".getBytes(), datasets.get(1).getElement(0xDF01).getBytes());
+ assertEquals(1, datasets.get(0).getElements().size());
+ assertEquals(1, datasets.get(1).getElements().size());
+ assertEquals(DatasetFormat.DBM, ((ISODatasetField) c.getComponent(104)).getDataset(0x71).getFormat());
+ assertEquals(Optional.of("1234"), c.findValue("104.0x71.2"));
+ }
+
+ @Test
+ public void cloneDatasetPathWithPackager() throws Exception {
+ GenericPackager packager = cmfv3();
+ ISOMsg m = roundTrip(packager, cmfMsg(packager)
+ .with("55.0x9F26", AC)
+ .with("55.0x9F10", IAD)
+ .with("104.0x01.0xDF01", ISOUtil.hex2byte("CAFEBABE")));
+
+ ISOMsg c = m.clone("0", "55.0x9F26", "104.0x01.0xDF01");
+ ISOMsg unpacked = roundTrip(packager, c);
+ assertArrayEquals(AC, (byte[]) unpacked.findValue("55.0x9F26").orElseThrow());
+ assertEquals(Optional.empty(), unpacked.findValue("55.0x9F10"));
+ assertArrayEquals(ISOUtil.hex2byte("CAFEBABE"), (byte[]) unpacked.findValue("104.0x01.0xDF01").orElseThrow());
+ }
+
+ @Test
+ public void cloneNestedDatasetPath() throws Exception {
+ ISOMsg m = new ISOMsg("0100");
+ m.setDatasetElement("127.55", 0x9F26, AC);
+ m.setDatasetElement("127.55", 0x9F10, IAD);
+ m.set("127.2", "value127.2");
+
+ ISOMsg c = m.clone("127.55.0x9F26");
+ assertFalse(c.hasField("127.2"));
+ assertArrayEquals(AC, (byte[]) c.findValue("127.55.0x9F26").orElseThrow());
+ assertEquals(Optional.empty(), c.findValue("127.55.0x9F10"));
+ }
+
+ // ---- helpers
+
+ private static ISOMsg populated() throws ISOException {
+ ISOMsg m = new ISOMsg("0100");
+ m.set(3, "000000");
+ m.set(52, ISOUtil.hex2byte("CAFEBABE"));
+ m.set(62, "leaf62");
+ m.set("63.2.3", "value63.2.3");
+ ISODatasetField f55 = new ISODatasetField(55);
+ f55.addDataset(new ISODataset(55, DatasetFormat.TLV).with(0x9F26, AC).with(0x9F10, IAD));
+ m.set(f55);
+ return m;
+ }
+
+ private static GenericPackager cmfv3() throws ISOException {
+ return new GenericPackager("jar:packager/cmfv3.xml");
+ }
+
+ private static ISOMsg cmfMsg(GenericPackager packager) {
+ ISOMsg m = new ISOMsg("0100");
+ m.setPackager(packager);
+ return m;
+ }
+
+ private static ISOMsg roundTrip(GenericPackager packager, ISOMsg m) throws ISOException {
+ m.setPackager(packager);
+ ISOMsg unpacked = new ISOMsg();
+ unpacked.setPackager(packager);
+ unpacked.unpack(m.pack());
+ return unpacked;
+ }
+}
diff --git a/jpos/src/test/java/org/jpos/iso/ISOMsgTest.java b/jpos/src/test/java/org/jpos/iso/ISOMsgTest.java
index f0639e6ab8..0dbbf1cac7 100644
--- a/jpos/src/test/java/org/jpos/iso/ISOMsgTest.java
+++ b/jpos/src/test/java/org/jpos/iso/ISOMsgTest.java
@@ -234,14 +234,10 @@ public void testCloneFPathBrokenHierarchy() throws Exception {
ISOMsg m = new ISOMsg("0100");
m.set(62, "leaf62");
m.set("63.3.1", "value63.3.1");
- ISODatasetField f55 = new ISODatasetField(55);
- f55.addDataset(new ISODataset(55, DatasetFormat.TLV).with(0x9F26, "0102030405060708".getBytes()));
- m.set(f55);
- ISOMsg c = m.clone("0", "62.1", "55.0x9F26", "63.3.1");
+ ISOMsg c = m.clone("0", "62.1", "63.3.1");
assertEquals("0100", c.getMTI());
assertFalse(c.hasField(62), "path through a leaf must not create a composite");
- assertFalse(c.hasField(55), "path through a dataset field must not copy the field");
assertEquals("value63.3.1", c.getString("63.3.1"));
}