diff --git a/pom.xml b/pom.xml
index 9b59ed19..68149219 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
io.xlate
staedi
- 1.17.1-SNAPSHOT
+ 1.18.0-SNAPSHOT
StAEDI : Streaming API for EDI for Java
Streaming API for EDI for Java
diff --git a/src/main/java/io/xlate/edi/internal/schema/ElementPosition.java b/src/main/java/io/xlate/edi/internal/schema/ElementPosition.java
new file mode 100644
index 00000000..d8c6ed13
--- /dev/null
+++ b/src/main/java/io/xlate/edi/internal/schema/ElementPosition.java
@@ -0,0 +1,56 @@
+package io.xlate.edi.internal.schema;
+
+import java.util.Objects;
+
+import io.xlate.edi.schema.EDIElementPosition;
+
+public class ElementPosition implements EDIElementPosition {
+
+ private static final String TOSTRING_FORMAT = "%d.%d";
+
+ final int elementIndex;
+ final int componentIndex;
+
+ public ElementPosition(int elementPosition, int componentPosition) {
+ super();
+ this.elementIndex = elementPosition;
+ this.componentIndex = componentPosition;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!getClass().isInstance(o)) {
+ return false;
+ }
+
+ ElementPosition other = (ElementPosition) o;
+
+ return Objects.equals(elementIndex, other.elementIndex) &&
+ Objects.equals(componentIndex, other.componentIndex);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(elementIndex, componentIndex);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(TOSTRING_FORMAT, elementIndex, componentIndex);
+ }
+
+ @Override
+ public int getElementPosition() {
+ return elementIndex;
+ }
+
+ @Override
+ public int getComponentPosition() {
+ return componentIndex > 0 ? componentIndex : -1;
+ }
+
+}
diff --git a/src/main/java/io/xlate/edi/internal/schema/LoopType.java b/src/main/java/io/xlate/edi/internal/schema/LoopType.java
new file mode 100644
index 00000000..9cc632e9
--- /dev/null
+++ b/src/main/java/io/xlate/edi/internal/schema/LoopType.java
@@ -0,0 +1,40 @@
+package io.xlate.edi.internal.schema;
+
+import java.util.List;
+
+import io.xlate.edi.schema.EDIElementPosition;
+import io.xlate.edi.schema.EDILoopType;
+import io.xlate.edi.schema.EDIReference;
+import io.xlate.edi.schema.EDISyntaxRule;
+import io.xlate.edi.schema.EDIType;
+
+@SuppressWarnings("java:S2160") // Intentionally inherit 'equals' from superclass
+class LoopType extends StructureType implements EDILoopType {
+
+ private final EDIElementPosition levelIdPosition;
+ private final EDIElementPosition parentIdPosition;
+
+ LoopType(String code,
+ List references,
+ List syntaxRules,
+ EDIElementPosition levelIdPosition,
+ EDIElementPosition parentIdPosition,
+ String title,
+ String description) {
+
+ super(code, EDIType.Type.LOOP, code, references, syntaxRules, title, description);
+ this.levelIdPosition = levelIdPosition;
+ this.parentIdPosition = parentIdPosition;
+ }
+
+ @Override
+ public EDIElementPosition getLevelIdPosition() {
+ return levelIdPosition;
+ }
+
+ @Override
+ public EDIElementPosition getParentIdPosition() {
+ return parentIdPosition;
+ }
+
+}
diff --git a/src/main/java/io/xlate/edi/internal/schema/SchemaReaderBase.java b/src/main/java/io/xlate/edi/internal/schema/SchemaReaderBase.java
index ef4432a4..e0bd4dbc 100644
--- a/src/main/java/io/xlate/edi/internal/schema/SchemaReaderBase.java
+++ b/src/main/java/io/xlate/edi/internal/schema/SchemaReaderBase.java
@@ -5,6 +5,7 @@
import static io.xlate.edi.internal.schema.StaEDISchemaFactory.unexpectedEvent;
import static java.util.stream.Collectors.toList;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -25,6 +26,7 @@
import javax.xml.stream.XMLStreamReader;
import io.xlate.edi.schema.EDIComplexType;
+import io.xlate.edi.schema.EDIElementPosition;
import io.xlate.edi.schema.EDIReference;
import io.xlate.edi.schema.EDISchemaException;
import io.xlate.edi.schema.EDISimpleType;
@@ -45,6 +47,8 @@ abstract class SchemaReaderBase implements SchemaReader {
static final String ATTR_MIN_OCCURS = "minOccurs";
static final String ATTR_MAX_OCCURS = "maxOccurs";
static final String ATTR_TITLE = "title";
+ static final String ATTR_LEVEL_ID_POSITION = "levelIdPosition";
+ static final String ATTR_PARENT_ID_POSITION = "parentIdPosition";
static final EDIReference ANY_ELEMENT_REF_OPT = new Reference(StaEDISchema.ANY_ELEMENT_ID, LOCALNAME_ELEMENT, 0, 1, null, null);
static final EDIReference ANY_COMPOSITE_REF_OPT = new Reference(StaEDISchema.ANY_COMPOSITE_ID, LOCALNAME_COMPOSITE, 0, 99, null, null);
@@ -388,11 +392,15 @@ StructureType readComplexType(XMLStreamReader reader,
final EDIType.Type type = complex.get(complexType);
final String id;
String code = parseAttribute(reader, "code", String::valueOf, null);
+ EDIElementPosition levelIdPosition = null;
+ EDIElementPosition parentIdPosition = null;
if (qnTransaction.equals(complexType)) {
id = StaEDISchema.TRANSACTION_ID;
} else if (qnLoop.equals(complexType)) {
id = code;
+ levelIdPosition = parseElementPosition(reader, ATTR_LEVEL_ID_POSITION);
+ parentIdPosition = parseElementPosition(reader, ATTR_PARENT_ID_POSITION);
} else {
id = parseAttribute(reader, "name", String::valueOf);
@@ -423,7 +431,15 @@ StructureType readComplexType(XMLStreamReader reader,
event = reader.getEventType();
if (event == XMLStreamConstants.END_ELEMENT) {
- return new StructureType(id, type, code, refs, rules, title, descr);
+ StructureType structure;
+
+ if (qnLoop.equals(complexType)) {
+ structure = new LoopType(code, refs, rules, levelIdPosition, parentIdPosition, title, descr);
+ } else {
+ structure = new StructureType(id, type, code, refs, rules, title, descr);
+ }
+
+ return structure;
} else {
throw unexpectedEvent(reader);
}
@@ -806,6 +822,21 @@ void logUnusedTypes(Map types, Level level) {
}
}
+ EDIElementPosition parseElementPosition(XMLStreamReader reader, String attrName) {
+ BigDecimal positionAttr = parseAttribute(reader, attrName, BigDecimal::new, null);
+
+ if (positionAttr == null) {
+ return null;
+ }
+
+ int elementPosition = positionAttr.intValue();
+ int componentPosition = positionAttr.remainder(BigDecimal.ONE)
+ .movePointRight(positionAttr.scale())
+ .intValue();
+
+ return new ElementPosition(elementPosition, componentPosition);
+ }
+
protected abstract String readReferencedId(XMLStreamReader reader);
protected abstract void readInclude(XMLStreamReader reader, Map types) throws EDISchemaException;
diff --git a/src/main/java/io/xlate/edi/internal/schema/SchemaReaderV3.java b/src/main/java/io/xlate/edi/internal/schema/SchemaReaderV3.java
index 241f5149..3f1858d2 100644
--- a/src/main/java/io/xlate/edi/internal/schema/SchemaReaderV3.java
+++ b/src/main/java/io/xlate/edi/internal/schema/SchemaReaderV3.java
@@ -3,7 +3,6 @@
import static io.xlate.edi.internal.schema.StaEDISchemaFactory.schemaException;
import static io.xlate.edi.internal.schema.StaEDISchemaFactory.unexpectedElement;
-import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
@@ -34,6 +33,7 @@
import io.xlate.edi.internal.schema.implementation.SegmentImpl;
import io.xlate.edi.internal.schema.implementation.TransactionImpl;
import io.xlate.edi.schema.EDIComplexType;
+import io.xlate.edi.schema.EDIElementPosition;
import io.xlate.edi.schema.EDIReference;
import io.xlate.edi.schema.EDISchemaException;
import io.xlate.edi.schema.EDIType;
@@ -215,7 +215,7 @@ LoopImplementation readLoopImplementation(XMLStreamReader reader, QName complexT
String typeId;
int minOccurs = 0;
int maxOccurs = 0;
- BigDecimal discriminatorAttr = null;
+ EDIElementPosition discriminatorPos = null;
String title = null;
if (transactionLoop) {
@@ -226,7 +226,7 @@ LoopImplementation readLoopImplementation(XMLStreamReader reader, QName complexT
typeId = parseAttribute(reader, "type", String::valueOf);
minOccurs = parseAttribute(reader, ATTR_MIN_OCCURS, Integer::parseInt, -1);
maxOccurs = parseAttribute(reader, ATTR_MAX_OCCURS, Integer::parseInt, -1);
- discriminatorAttr = parseAttribute(reader, ATTR_DISCRIMINATOR, BigDecimal::new, null);
+ discriminatorPos = parseElementPosition(reader, ATTR_DISCRIMINATOR);
title = parseAttribute(reader, ATTR_TITLE, String::valueOf, null);
}
@@ -237,9 +237,9 @@ LoopImplementation readLoopImplementation(XMLStreamReader reader, QName complexT
Discriminator disc = null;
- if (discriminatorAttr != null) {
+ if (discriminatorPos != null) {
SegmentImpl segImpl = (SegmentImpl) sequence.get(0);
- disc = buildDiscriminator(discriminatorAttr, segImpl.getSequence());
+ disc = buildDiscriminator(discriminatorPos, segImpl.getSequence());
}
return new LoopImpl(minOccurs, maxOccurs, id, typeId, disc, sequence, title, descr);
@@ -266,13 +266,13 @@ SegmentImpl readSegmentImplementation() {
String code = parseAttribute(reader, "code", String::valueOf, typeId);
int minOccurs = parseAttribute(reader, ATTR_MIN_OCCURS, Integer::parseInt, -1);
int maxOccurs = parseAttribute(reader, ATTR_MAX_OCCURS, Integer::parseInt, -1);
- BigDecimal discriminatorAttr = parseAttribute(reader, ATTR_DISCRIMINATOR, BigDecimal::new, null);
+ EDIElementPosition discriminatorPos = parseElementPosition(reader, ATTR_DISCRIMINATOR);
String title = parseAttribute(reader, ATTR_TITLE, String::valueOf, null);
return readTypeImplementation(reader,
() -> readSequence(reader, e -> readPositionedSequenceEntry(e, sequence, true)),
descr -> whenExpected(reader, qnSegment, () -> {
- Discriminator disc = buildDiscriminator(discriminatorAttr, sequence);
+ Discriminator disc = buildDiscriminator(discriminatorPos, sequence);
SegmentImpl segment = new SegmentImpl(minOccurs,
maxOccurs,
typeId,
@@ -312,20 +312,19 @@ void readPositionedSequenceEntry(QName entryName, List se
}
}
- Discriminator buildDiscriminator(BigDecimal discriminatorAttr,
+ Discriminator buildDiscriminator(EDIElementPosition discriminatorPos,
List sequence) {
Discriminator disc = null;
- if (discriminatorAttr != null) {
- int elementPosition = discriminatorAttr.intValue();
- int componentPosition = discriminatorAttr.remainder(BigDecimal.ONE)
- .movePointRight(discriminatorAttr.scale()).intValue();
+ if (discriminatorPos != null) {
+ final int elementPosition = discriminatorPos.getElementPosition();
+ final int componentPosition = discriminatorPos.getComponentPosition();
- EDITypeImplementation eleImpl = getDiscriminatorElement(discriminatorAttr, elementPosition, sequence, "element");
+ EDITypeImplementation eleImpl = getDiscriminatorElement(discriminatorPos, elementPosition, sequence, "element");
if (eleImpl instanceof CompositeImpl) {
sequence = ((CompositeImpl) eleImpl).getSequence();
- eleImpl = getDiscriminatorElement(discriminatorAttr, componentPosition, sequence, "component");
+ eleImpl = getDiscriminatorElement(discriminatorPos, componentPosition, sequence, "component");
}
Set discValues;
@@ -333,24 +332,24 @@ Discriminator buildDiscriminator(BigDecimal discriminatorAttr,
if (eleImpl != null) {
discValues = ((ElementImpl) eleImpl).getValueSet();
} else {
- throw schemaException("Discriminator position is unused (not specified): " + discriminatorAttr, reader);
+ throw schemaException("Discriminator position is unused (not specified): " + discriminatorPos, reader);
}
if (!discValues.isEmpty()) {
- disc = new DiscriminatorImpl(elementPosition, componentPosition, discValues);
+ disc = new DiscriminatorImpl(discriminatorPos, discValues);
} else {
- throw schemaException("Discriminator element does not specify value enumeration: " + discriminatorAttr, reader);
+ throw schemaException("Discriminator element does not specify value enumeration: " + discriminatorPos, reader);
}
}
return disc;
}
- EDITypeImplementation getDiscriminatorElement(BigDecimal attr, int position, List sequence, String type) {
+ EDITypeImplementation getDiscriminatorElement(EDIElementPosition discriminatorPos, int position, List sequence, String type) {
if (position > 0 && position <= sequence.size()) {
return sequence.get(position - 1);
} else {
- throw schemaException("Discriminator " + type + " position invalid: " + attr, reader);
+ throw schemaException("Discriminator " + type + " position invalid: " + discriminatorPos, reader);
}
}
diff --git a/src/main/java/io/xlate/edi/internal/schema/implementation/DiscriminatorImpl.java b/src/main/java/io/xlate/edi/internal/schema/implementation/DiscriminatorImpl.java
index 6ae85ebc..a1f9da63 100644
--- a/src/main/java/io/xlate/edi/internal/schema/implementation/DiscriminatorImpl.java
+++ b/src/main/java/io/xlate/edi/internal/schema/implementation/DiscriminatorImpl.java
@@ -3,19 +3,17 @@
import java.util.Objects;
import java.util.Set;
+import io.xlate.edi.schema.EDIElementPosition;
import io.xlate.edi.schema.implementation.Discriminator;
public class DiscriminatorImpl implements Discriminator {
- private static final String TOSTRING_FORMAT = "position: [%d, %d], values: %s";
- private final int elementPosition;
- private final int componentPosition;
- private final Set valueSet;
+ private static final String TOSTRING_FORMAT = "position: %s, values: %s";
+ final EDIElementPosition position;
+ final Set valueSet;
- public DiscriminatorImpl(int elementPosition, int componentPosition, Set valueSet) {
- super();
- this.elementPosition = elementPosition;
- this.componentPosition = componentPosition;
+ public DiscriminatorImpl(EDIElementPosition position, Set valueSet) {
+ this.position = position;
this.valueSet = valueSet;
}
@@ -30,30 +28,28 @@ public boolean equals(Object o) {
}
DiscriminatorImpl other = (DiscriminatorImpl) o;
-
- return Objects.equals(elementPosition, other.elementPosition) &&
- Objects.equals(componentPosition, other.componentPosition) &&
+ return Objects.equals(position, other.position) &&
Objects.equals(valueSet, other.valueSet);
}
@Override
public int hashCode() {
- return Objects.hash(elementPosition, componentPosition, valueSet);
+ return Objects.hash(position, valueSet);
}
@Override
public String toString() {
- return String.format(TOSTRING_FORMAT, elementPosition, componentPosition, valueSet);
+ return String.format(TOSTRING_FORMAT, position, valueSet);
}
@Override
public int getElementPosition() {
- return elementPosition;
+ return position.getElementPosition();
}
@Override
public int getComponentPosition() {
- return componentPosition;
+ return position.getComponentPosition();
}
@Override
diff --git a/src/main/java/io/xlate/edi/internal/stream/CharArraySequence.java b/src/main/java/io/xlate/edi/internal/stream/CharArraySequence.java
index 39806be7..6e431e75 100644
--- a/src/main/java/io/xlate/edi/internal/stream/CharArraySequence.java
+++ b/src/main/java/io/xlate/edi/internal/stream/CharArraySequence.java
@@ -45,10 +45,6 @@ public int length() {
return length;
}
- public char[] getText() {
- return text;
- }
-
@Override
public char charAt(int index) {
if (index < 0 || index >= length) {
diff --git a/src/main/java/io/xlate/edi/internal/stream/StaEDIInputFactory.java b/src/main/java/io/xlate/edi/internal/stream/StaEDIInputFactory.java
index b5e491e2..92cda8d6 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIInputFactory.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIInputFactory.java
@@ -38,9 +38,11 @@ public class StaEDIInputFactory extends EDIInputFactory {
public StaEDIInputFactory() {
supportedProperties.add(EDI_VALIDATE_CONTROL_STRUCTURE);
supportedProperties.add(EDI_VALIDATE_CONTROL_CODE_VALUES);
+ supportedProperties.add(EDI_IGNORE_EXTRANEOUS_CHARACTERS);
+ supportedProperties.add(EDI_NEST_HIERARCHICAL_LOOPS);
+
supportedProperties.add(XML_DECLARE_TRANSACTION_XMLNS);
supportedProperties.add(XML_WRAP_TRANSACTION_CONTENTS);
- supportedProperties.add(EDI_IGNORE_EXTRANEOUS_CHARACTERS);
supportedProperties.add(JSON_NULL_EMPTY_ELEMENTS);
supportedProperties.add(JSON_OBJECT_ELEMENTS);
diff --git a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java
index 392b6e92..7ea912a7 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java
@@ -68,7 +68,7 @@ public StaEDIStreamReader(
this.controlSchema = schema;
this.properties = new HashMap<>(properties);
this.reporter = reporter;
- this.proxy = new ProxyEventHandler(location, this.controlSchema);
+ this.proxy = new ProxyEventHandler(location, this.controlSchema, nestHierarchicalLoops());
this.lexer = new Lexer(stream, charset, proxy, location, ignoreExtraneousCharacters());
}
@@ -103,6 +103,7 @@ void requireEvent(String message, EDIStreamEvent... events) {
}
private CharBuffer getBuffer() {
+ checkTextState();
return proxy.getCharacters();
}
@@ -365,19 +366,13 @@ private void checkTextState() {
@Override
public String getText() {
- ensureOpen();
- checkTextState();
final CharBuffer buffer = getBuffer();
-
return buffer.toString();
}
@Override
public char[] getTextCharacters() {
- ensureOpen();
- checkTextState();
final CharBuffer buffer = getBuffer();
-
return Arrays.copyOf(buffer.array(), buffer.length());
}
@@ -387,9 +382,6 @@ public int getTextCharacters(int sourceStart,
int targetStart,
int length) {
- ensureOpen();
- checkTextState();
-
if (target == null) {
throw new NullPointerException("Null target array");
}
@@ -426,19 +418,13 @@ public int getTextCharacters(int sourceStart,
@Override
public int getTextStart() {
- ensureOpen();
- checkTextState();
final CharBuffer buffer = getBuffer();
-
return buffer.position();
}
@Override
public int getTextLength() {
- ensureOpen();
- checkTextState();
final CharBuffer buffer = getBuffer();
-
return buffer.limit();
}
@@ -487,4 +473,8 @@ boolean useInternalControlSchema() {
boolean ignoreExtraneousCharacters() {
return getProperty(EDIInputFactory.EDI_IGNORE_EXTRANEOUS_CHARACTERS, Boolean::parseBoolean, false);
}
+
+ boolean nestHierarchicalLoops() {
+ return getProperty(EDIInputFactory.EDI_NEST_HIERARCHICAL_LOOPS, Boolean::parseBoolean, true);
+ }
}
diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/Dialect.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/Dialect.java
index 5cdae9c5..f0eb95e5 100644
--- a/src/main/java/io/xlate/edi/internal/stream/tokenization/Dialect.java
+++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/Dialect.java
@@ -176,4 +176,5 @@ public String[] getTransactionVersion() {
public String getTransactionVersionString() {
return transactionVersionString;
}
+
}
diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/ProxyEventHandler.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/ProxyEventHandler.java
index 55ea9b46..da16e570 100644
--- a/src/main/java/io/xlate/edi/internal/stream/tokenization/ProxyEventHandler.java
+++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/ProxyEventHandler.java
@@ -17,14 +17,20 @@
import java.io.InputStream;
import java.nio.CharBuffer;
+import java.util.Deque;
import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
+import java.util.Queue;
+import java.util.function.Function;
import io.xlate.edi.internal.stream.CharArraySequence;
import io.xlate.edi.internal.stream.StaEDIStreamLocation;
import io.xlate.edi.internal.stream.validation.UsageError;
import io.xlate.edi.internal.stream.validation.Validator;
+import io.xlate.edi.schema.EDIElementPosition;
+import io.xlate.edi.schema.EDILoopType;
import io.xlate.edi.schema.EDIReference;
import io.xlate.edi.schema.EDIType;
import io.xlate.edi.schema.Schema;
@@ -35,6 +41,7 @@
public class ProxyEventHandler implements EventHandler {
private final StaEDIStreamLocation location;
+ private final boolean nestHierarchicalLoops;
private Schema controlSchema;
private Validator controlValidator;
@@ -49,17 +56,41 @@ public class ProxyEventHandler implements EventHandler {
private String segmentTag;
private CharArraySequence elementHolder = new CharArraySequence();
- private StreamEvent[] events = new StreamEvent[99];
- private int eventCount = 0;
- private int eventIndex = 0;
+ private final Queue eventPool = new LinkedList<>();
+ // Use implementation to access as both Deque & List
+ private final LinkedList eventQueue = new LinkedList<>();
+
+ private final Deque openLevels = new LinkedList<>();
+
+ static class HierarchicalLevel {
+ final String id;
+ final StreamEvent event;
+
+ HierarchicalLevel(String id, StreamEvent event) {
+ this.id = id;
+ this.event = event;
+ }
+
+ boolean isParentOf(String parentId) {
+ return !parentId.isEmpty() && parentId.equals(id);
+ }
+ }
+
+ private boolean levelCheckPending;
+ private StreamEvent currentSegmentBegin;
+ private StreamEvent startedLevel;
+ private EDIElementPosition levelIdPosition;
+ private String startedLevelId;
+ private EDIElementPosition parentIdPosition;
+ private String startedLevelParentId;
+
private Dialect dialect;
- public ProxyEventHandler(StaEDIStreamLocation location, Schema controlSchema) {
+ public ProxyEventHandler(StaEDIStreamLocation location, Schema controlSchema, boolean nestHierarchicalLoops) {
this.location = location;
+ this.nestHierarchicalLoops = nestHierarchicalLoops;
+
setControlSchema(controlSchema, true);
- for (int i = 0; i < 99; i++) {
- events[i] = new StreamEvent();
- }
}
public void setControlSchema(Schema controlSchema, boolean validateCodeValues) {
@@ -87,48 +118,53 @@ public void setTransactionSchema(Schema transactionSchema) {
}
public void resetEvents() {
- eventCount = 0;
- eventIndex = 0;
+ eventPool.addAll(eventQueue);
+ eventQueue.clear();
}
public EDIStreamEvent getEvent() {
- if (hasEvents()) {
- return events[eventIndex].type;
- }
- return null;
+ return current(StreamEvent::getType, null);
}
public CharBuffer getCharacters() {
- if (hasEvents()) {
- return events[eventIndex].getData();
- }
- throw new IllegalStateException();
- }
-
- public boolean hasEvents() {
- return eventIndex < eventCount;
+ return current(StreamEvent::getData, null);
}
public boolean nextEvent() {
- if (eventCount < 1) {
+ if (eventQueue.isEmpty()) {
return false;
}
- return ++eventIndex < eventCount;
+
+ eventPool.add(eventQueue.removeFirst());
+ return !eventQueue.isEmpty();
}
public EDIStreamValidationError getErrorType() {
- return events[eventIndex].errorType;
+ return current(StreamEvent::getErrorType, null);
}
public String getReferenceCode() {
- return hasEvents() ? events[eventIndex].getReferenceCode() : null;
+ return current(StreamEvent::getReferenceCode, null);
}
public Location getLocation() {
- if (hasEvents() && events[eventIndex].location != null) {
- return events[eventIndex].location;
+ return current(StreamEvent::getLocation, this.location);
+ }
+
+ T current(Function mapper, T defaultValue) {
+ final T value;
+
+ if (eventQueue.isEmpty()) {
+ value = defaultValue;
+ } else {
+ value = mapper.apply(eventQueue.getFirst());
}
- return location;
+
+ return value;
+ }
+
+ StreamEvent getPooledEvent() {
+ return eventPool.isEmpty() ? new StreamEvent() : eventPool.remove();
}
public InputStream getBinary() {
@@ -140,7 +176,7 @@ public void setBinary(InputStream binary) {
}
public EDIReference getSchemaTypeReference() {
- return hasEvents() ? events[eventIndex].getTypeReference() : null;
+ return current(StreamEvent::getTypeReference, null);
}
@Override
@@ -175,6 +211,14 @@ public void loopBegin(EDIReference typeReference) {
enqueueEvent(EDIStreamEvent.START_GROUP, EDIStreamValidationError.NONE, loopCode, typeReference, location);
} else {
enqueueEvent(EDIStreamEvent.START_LOOP, EDIStreamValidationError.NONE, loopCode, typeReference, location);
+
+ if (nestHierarchicalLoops && isHierarchicalLoop(typeReference.getReferencedType())) {
+ EDILoopType loop = (EDILoopType) typeReference.getReferencedType();
+ startedLevel = eventQueue.getLast();
+ levelIdPosition = loop.getLevelIdPosition();
+ parentIdPosition = loop.getParentIdPosition();
+ levelCheckPending = true;
+ }
}
}
@@ -192,6 +236,8 @@ public void loopEnd(EDIReference typeReference) {
} else if (EDIType.Type.GROUP.toString().equals(loopCode)) {
dialect.groupEnd();
enqueueEvent(EDIStreamEvent.END_GROUP, EDIStreamValidationError.NONE, loopCode, typeReference, location);
+ } else if (nestHierarchicalLoops && isHierarchicalLoop(typeReference.getReferencedType())) {
+ levelCheckPending = true;
} else {
enqueueEvent(EDIStreamEvent.END_LOOP, EDIStreamValidationError.NONE, loopCode, typeReference, location);
}
@@ -209,6 +255,7 @@ public boolean segmentBegin(String segmentTag) {
Validator validator = validator();
boolean eventsReady = true;
EDIReference typeReference = null;
+ clearLevelCheck();
if (validator != null && !dialect.isServiceAdviceSegment(segmentTag)) {
validator.validateSegment(this, segmentTag);
@@ -231,11 +278,14 @@ public boolean segmentBegin(String segmentTag) {
}
enqueueEvent(EDIStreamEvent.START_SEGMENT, EDIStreamValidationError.NONE, segmentTag, typeReference, location);
- return eventsReady;
+ currentSegmentBegin = eventQueue.getLast();
+ return !levelCheckPending && eventsReady;
}
boolean exitTransaction(CharSequence tag) {
- return transaction && !transactionSchemaAllowed && controlSchema != null
+ return transaction
+ && !transactionSchemaAllowed
+ && controlSchema != null
&& controlSchema.containsSegment(tag.toString());
}
@@ -250,6 +300,10 @@ public boolean segmentEnd() {
typeReference = validator.getSegmentReference();
}
+ if (levelCheckPending) {
+ performLevelCheck();
+ }
+
location.clearSegmentLocations();
enqueueEvent(EDIStreamEvent.END_SEGMENT, EDIStreamValidationError.NONE, segmentTag, typeReference, location);
return true;
@@ -277,7 +331,7 @@ public boolean compositeBegin(boolean isNil) {
}
enqueueEvent(EDIStreamEvent.START_COMPOSITE, EDIStreamValidationError.NONE, "", typeReference, location);
- return eventsReady;
+ return !levelCheckPending && eventsReady;
}
@Override
@@ -291,7 +345,7 @@ public boolean compositeEnd(boolean isNil) {
location.clearComponentPosition();
enqueueEvent(EDIStreamEvent.END_COMPOSITE, EDIStreamValidationError.NONE, "", null, location);
- return eventsReady;
+ return !levelCheckPending && eventsReady;
}
@Override
@@ -306,6 +360,10 @@ public boolean elementData(char[] text, int start, int length) {
Validator validator = validator();
boolean valid;
+ if (levelCheckPending && startedLevel != null) {
+ setLevelIdentifiers();
+ }
+
if (validator != null) {
valid = validator.validateElement(dialect, location, elementHolder, null);
derivedComposite = !compositeFromStream && validator.isComposite();
@@ -317,7 +375,14 @@ public boolean elementData(char[] text, int start, int length) {
typeReference = null;
}
- if (derivedComposite && elementHolder.getText() != null/* Not an empty composite */) {
+ /*
+ * The first component of a composite was the only element received
+ * for the composite. It was found to be a composite via the schema
+ * and the composite begin/end events must be generated.
+ **/
+ final boolean componentReceivedAsSimple = derivedComposite && text != null;
+
+ if (componentReceivedAsSimple) {
this.compositeBegin(elementHolder.length() == 0);
location.incrementComponentPosition();
}
@@ -332,16 +397,79 @@ public boolean elementData(char[] text, int start, int length) {
location);
if (validator != null && validator.isPendingDiscrimination()) {
- eventsReady = validator.selectImplementation(events, eventIndex, eventCount, this);
+ eventsReady = validator.selectImplementation(eventQueue, this);
}
}
- if (derivedComposite && text != null /* Not an empty composite */) {
+ if (componentReceivedAsSimple) {
this.compositeEnd(length == 0);
location.clearComponentPosition();
}
- return eventsReady;
+ return !levelCheckPending && eventsReady;
+ }
+
+ void clearLevelCheck() {
+ levelCheckPending = false;
+ currentSegmentBegin = null;
+ startedLevel = null;
+
+ levelIdPosition = null;
+ startedLevelId = "";
+
+ parentIdPosition = null;
+ startedLevelParentId = "";
+ }
+
+ boolean isHierarchicalLoop(EDIType type) {
+ EDILoopType loop = (EDILoopType) type;
+
+ return loop.getLevelIdPosition() != null &&
+ loop.getParentIdPosition() != null;
+ }
+
+ void setLevelIdentifiers() {
+ if (levelIdPosition.matchesLocation(location)) {
+ startedLevelId = elementHolder.toString();
+ }
+
+ if (parentIdPosition.matchesLocation(location)) {
+ startedLevelParentId = elementHolder.toString();
+ }
+ }
+
+ void performLevelCheck() {
+ if (startedLevel != null) {
+ completeLevel(startedLevel, startedLevelParentId);
+
+ StreamEvent openLevel = getPooledEvent();
+ openLevel.type = EDIStreamEvent.END_LOOP;
+ openLevel.errorType = startedLevel.errorType;
+ openLevel.setData(startedLevel.data);
+ openLevel.setTypeReference(startedLevel.typeReference);
+ openLevel.setLocation(startedLevel.location);
+
+ /*
+ * startedLevelId will not be null due to Validator#validateSyntax.
+ * Although the client never sees the generated element event, here
+ * it will be an empty string.
+ */
+ openLevels.addLast(new HierarchicalLevel(startedLevelId, openLevel));
+ } else {
+ completeLevel(currentSegmentBegin, "");
+ }
+
+ clearLevelCheck();
+ }
+
+ void completeLevel(StreamEvent successor, String parentId) {
+ while (!openLevels.isEmpty() && !openLevels.getLast().isParentOf(parentId)) {
+ HierarchicalLevel completed = openLevels.removeLast();
+ completed.event.location.set(location);
+ completed.event.location.clearSegmentLocations();
+
+ eventQueue.add(eventQueue.indexOf(successor), completed.event);
+ }
}
void enqueueElementOccurrenceErrors(Validator validator, boolean valid) {
@@ -435,28 +563,35 @@ private void enqueueEvent(EDIStreamEvent event,
EDIReference typeReference,
Location location) {
- final int index = eventCount;
- StreamEvent target = events[index];
- EDIStreamEvent associatedEvent = (index > 0) ? getAssociatedEvent(error) : null;
+ StreamEvent target = getPooledEvent();
+ EDIStreamEvent associatedEvent = eventQueue.isEmpty() ? null : getAssociatedEvent(error);
- if (eventExists(associatedEvent, index)) {
+ if (eventExists(associatedEvent)) {
/*
* Ensure segment errors occur before other event types
* when the array has other events already present.
*/
- int offset = index;
+ int offset = eventQueue.size();
boolean complete = false;
while (!complete) {
- if (events[offset - 1].type == associatedEvent) {
+ StreamEvent enqueuedEvent = eventQueue.get(offset - 1);
+
+ if (enqueuedEvent.type == associatedEvent) {
complete = true;
} else {
- events[offset] = events[offset - 1];
+ if (eventQueue.size() == offset) {
+ eventQueue.add(offset, enqueuedEvent);
+ } else {
+ eventQueue.set(offset, enqueuedEvent);
+ }
offset--;
}
}
- events[offset] = target;
+ eventQueue.set(offset, target);
+ } else {
+ eventQueue.add(target);
}
target.type = event;
@@ -464,15 +599,13 @@ private void enqueueEvent(EDIStreamEvent event,
target.setData(data);
target.setTypeReference(typeReference);
target.setLocation(location);
-
- eventCount++;
}
- private boolean eventExists(EDIStreamEvent associatedEvent, int index) {
- int offset = index;
+ private boolean eventExists(EDIStreamEvent associatedEvent) {
+ int offset = eventQueue.size();
while (associatedEvent != null && offset > 0) {
- if (events[offset - 1].type == associatedEvent) {
+ if (eventQueue.get(offset - 1).type == associatedEvent) {
return true;
}
offset--;
diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/StreamEvent.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/StreamEvent.java
index fdcb5b72..812b7fb3 100644
--- a/src/main/java/io/xlate/edi/internal/stream/tokenization/StreamEvent.java
+++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/StreamEvent.java
@@ -33,6 +33,10 @@ public EDIStreamEvent getType() {
return type;
}
+ public EDIStreamValidationError getErrorType() {
+ return errorType;
+ }
+
public CharBuffer getData() {
return dataNull ? null : data;
}
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/Validator.java b/src/main/java/io/xlate/edi/internal/stream/validation/Validator.java
index 11800766..281b36ef 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/Validator.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/Validator.java
@@ -746,8 +746,8 @@ private void handleMissingMandatory(ValidationEventHandler handler, int depth) {
}
}
- public boolean selectImplementation(StreamEvent[] events, int index, int count, ValidationEventHandler handler) {
- StreamEvent currentEvent = events[index + count - 1];
+ public boolean selectImplementation(Deque eventQueue, ValidationEventHandler handler) {
+ StreamEvent currentEvent = eventQueue.getLast();
if (currentEvent.getType() != EDIStreamEvent.ELEMENT_DATA) {
return false;
@@ -763,14 +763,14 @@ public boolean selectImplementation(StreamEvent[] events, int index, int count,
if (implNode.isFirstChild()) {
//start of loop, update the loop, segment, and element references that were already reported
- updateEventReferences(events, index, count, implType, implSeg.getLink());
+ updateEventReferences(eventQueue, implType, implSeg.getLink());
// Replace the standard loop with the implementation on the stack
loopStack.pop();
loopStack.push(implNode.getParent());
} else {
//update segment and element references that were already reported
- updateEventReferences(events, index, count, null, implSeg.getLink());
+ updateEventReferences(eventQueue, null, implSeg.getLink());
}
return true;
@@ -864,17 +864,8 @@ static boolean isMatch(PolymorphicImplementation implType, StreamEvent currentEv
return true;
}
- if (discr.getValueSet().contains(currentEvent.getData().toString())) {
- int eleLoc = discr.getElementPosition();
- int comLoc = discr.getComponentPosition() == 0 ? -1 : discr.getComponentPosition();
- Location location = currentEvent.getLocation();
-
- if (eleLoc == location.getElementPosition() && comLoc == location.getComponentPosition()) {
- return true;
- }
- }
-
- return false;
+ return discr.matchesLocation(currentEvent.getLocation())
+ && discr.getValueSet().contains(currentEvent.getData().toString());
}
/**
@@ -886,21 +877,21 @@ static boolean isMatch(PolymorphicImplementation implType, StreamEvent currentEv
* @param count
* @param implType
*/
- static void updateEventReferences(StreamEvent[] events, int index, int count, EDIReference implType, EDIReference implSeg) {
- for (int i = index; i < count; i++) {
- switch (events[i].getType()) {
+ static void updateEventReferences(Deque eventQueue, EDIReference implType, EDIReference implSeg) {
+ for (StreamEvent event : eventQueue) {
+ switch (event.getType()) {
case START_LOOP:
// Assuming implType is not null if we find a START_LOOP event
Objects.requireNonNull(implType, "Unexpected loop event during implementation segment selection");
- updateReferenceWhenMatched(events[i], implType);
+ updateReferenceWhenMatched(event, implType);
break;
case START_SEGMENT:
- updateReferenceWhenMatched(events[i], implSeg);
+ updateReferenceWhenMatched(event, implSeg);
break;
case START_COMPOSITE:
case END_COMPOSITE:
case ELEMENT_DATA:
- updateReference(events[i], (SegmentImplementation) implSeg);
+ updateReference(event, (SegmentImplementation) implSeg);
break;
default:
break;
diff --git a/src/main/java/io/xlate/edi/schema/EDIElementPosition.java b/src/main/java/io/xlate/edi/schema/EDIElementPosition.java
new file mode 100644
index 00000000..fd5933ae
--- /dev/null
+++ b/src/main/java/io/xlate/edi/schema/EDIElementPosition.java
@@ -0,0 +1,44 @@
+package io.xlate.edi.schema;
+
+import io.xlate.edi.stream.Location;
+
+/**
+ * Specification of the position of an element. Provides information about the
+ * element position within a segment and (optionally) a component element
+ * position within a composite element.
+ *
+ * @since 1.18
+ * @see io.xlate.edi.schema.implementation.Discriminator
+ */
+public interface EDIElementPosition {
+
+ /**
+ * Get the element position within a segment.
+ *
+ * @return the element position within a segment
+ */
+ int getElementPosition();
+
+ /**
+ * Get the component position within a composite element. Returns -1 if not
+ * available.
+ *
+ * @return the component position with a composite element or -1
+ */
+ int getComponentPosition();
+
+ /**
+ * Determine if the given location's element and component matches this
+ * element position specification.
+ *
+ * @param location
+ * the location to match against this position
+ * @return true if the element and component positions match, otherwise
+ * false
+ */
+ default boolean matchesLocation(Location location) {
+ return location.getElementPosition() == getElementPosition()
+ && location.getComponentPosition() == getComponentPosition();
+ }
+
+}
diff --git a/src/main/java/io/xlate/edi/schema/EDILoopType.java b/src/main/java/io/xlate/edi/schema/EDILoopType.java
new file mode 100644
index 00000000..5ae2d318
--- /dev/null
+++ b/src/main/java/io/xlate/edi/schema/EDILoopType.java
@@ -0,0 +1,28 @@
+package io.xlate.edi.schema;
+
+/**
+ * Provides access to attributes specified for a loop complex type.
+ *
+ * @since 1.18
+ */
+public interface EDILoopType extends EDIComplexType {
+
+ /**
+ * For hierarchical loops, get the position of the element within the first
+ * segment that identifies the loop. If not a hierarchical loop, null is
+ * returned.
+ *
+ * @return the position of the element holding the loop's level ID
+ */
+ EDIElementPosition getLevelIdPosition();
+
+ /**
+ * For hierarchical loops, get the position of the element within the first
+ * segment that identifies the parent of the loop. If not a hierarchical
+ * loop, null is returned.
+ *
+ * @return the position of the element holding the loop's parent level ID
+ */
+ EDIElementPosition getParentIdPosition();
+
+}
diff --git a/src/main/java/io/xlate/edi/schema/implementation/Discriminator.java b/src/main/java/io/xlate/edi/schema/implementation/Discriminator.java
index c8e4b78d..452de98d 100644
--- a/src/main/java/io/xlate/edi/schema/implementation/Discriminator.java
+++ b/src/main/java/io/xlate/edi/schema/implementation/Discriminator.java
@@ -2,11 +2,9 @@
import java.util.Set;
-public interface Discriminator {
+import io.xlate.edi.schema.EDIElementPosition;
- int getElementPosition();
-
- int getComponentPosition();
+public interface Discriminator extends EDIElementPosition {
Set getValueSet();
diff --git a/src/main/java/io/xlate/edi/stream/EDIInputFactory.java b/src/main/java/io/xlate/edi/stream/EDIInputFactory.java
index b0e0e0f9..c219ca59 100644
--- a/src/main/java/io/xlate/edi/stream/EDIInputFactory.java
+++ b/src/main/java/io/xlate/edi/stream/EDIInputFactory.java
@@ -62,6 +62,19 @@ public abstract class EDIInputFactory extends PropertySupport {
*/
public static final String EDI_IGNORE_EXTRANEOUS_CHARACTERS = "io.xlate.edi.stream.EDI_IGNORE_EXTRANEOUS_CHARACTERS";
+ /**
+ * When set to true, hierarchical loops will be nested in the EDI input
+ * stream. The nesting structure is determined by the linkage specified by
+ * the EDI data itself using pointers given in the EDI schema for a loop.
+ *
+ * For example, the hierarchical information given by the X12 HL segment.
+ *
+ * Default value: true
+ *
+ * @since 1.18
+ */
+ public static final String EDI_NEST_HIERARCHICAL_LOOPS = "io.xlate.edi.stream.EDI_NEST_HIERARCHICAL_LOOPS";
+
/**
* When set to true, simple data elements not containing data will be
* represented via the JSON parsers as a null value.
diff --git a/src/main/resources/schema/EDISchema-v4.xsd b/src/main/resources/schema/EDISchema-v4.xsd
index f7d7a910..45ec868c 100644
--- a/src/main/resources/schema/EDISchema-v4.xsd
+++ b/src/main/resources/schema/EDISchema-v4.xsd
@@ -84,7 +84,7 @@
-
+
@@ -92,6 +92,10 @@
+
+
+
+
@@ -439,6 +443,26 @@
+
+
+
+
+ The element position in the loop's first segment used to identify a
+ hierarchical level in the context of hierarchical nested loops.
+ When used, attribute parentId must also be specified.
+
+
+
+
+
+
+
+ The element position in the loop's first segment used to identify a
+ hierarchical parent level in the context of hierarchical nested loops.
+ When used, attribute levelId must also be specified.
+
+
+
diff --git a/src/test/java/io/xlate/edi/internal/schema/implementation/DiscriminatorImplTest.java b/src/test/java/io/xlate/edi/internal/schema/implementation/DiscriminatorImplTest.java
index 1d51f33c..c95a0f81 100644
--- a/src/test/java/io/xlate/edi/internal/schema/implementation/DiscriminatorImplTest.java
+++ b/src/test/java/io/xlate/edi/internal/schema/implementation/DiscriminatorImplTest.java
@@ -9,7 +9,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import io.xlate.edi.schema.implementation.Discriminator;
+import io.xlate.edi.internal.schema.ElementPosition;
+import io.xlate.edi.schema.EDIElementPosition;
class DiscriminatorImplTest {
@@ -17,23 +18,29 @@ class DiscriminatorImplTest {
@BeforeEach
void setUp() throws Exception {
- target = new DiscriminatorImpl(1, 2, Collections.singleton("50"));
+ target = new DiscriminatorImpl(position(1, 2), Collections.singleton("50"));
+ }
+
+ ElementPosition position(int e, int c) {
+ return new ElementPosition(e, c);
}
@Test
void testHashCode() {
- int expected = new DiscriminatorImpl(1, 2, Collections.singleton("50")).hashCode();
+ int expected = new DiscriminatorImpl(position(1, 2), Collections.singleton("50")).hashCode();
assertEquals(expected, target.hashCode());
}
@Test
void testEquals_Same() {
+ assertEquals(target.position, target.position);
assertEquals(target, target);
}
@Test
void testEquals_Identical() {
- Discriminator identical = new DiscriminatorImpl(1, 2, Collections.singleton("50"));
+ DiscriminatorImpl identical = new DiscriminatorImpl(position(1, 2), Collections.singleton("50"));
+ assertEquals(target.position, identical.position);
assertEquals(target, identical);
}
@@ -44,16 +51,33 @@ void testEquals_NotInstance() {
}
}
+ @Test
+ void testEquals_PositionNotInstance() {
+ DiscriminatorImpl actual = new DiscriminatorImpl(new EDIElementPosition() {
+ @Override
+ public int getComponentPosition() {
+ return 1;
+ }
+
+ @Override
+ public int getElementPosition() {
+ return 2;
+ }
+ }, Collections.singleton("50"));
+
+ assertNotEquals(target, actual);
+ }
+
@Test
void testEquals_Different() {
- assertNotEquals(new DiscriminatorImpl(1, 2, Collections.singleton("60")), target);
- assertNotEquals(new DiscriminatorImpl(2, 2, Collections.singleton("50")), target);
- assertNotEquals(new DiscriminatorImpl(1, 3, Collections.singleton("50")), target);
+ assertNotEquals(new DiscriminatorImpl(position(1, 2), Collections.singleton("60")), target);
+ assertNotEquals(new DiscriminatorImpl(position(2, 2), Collections.singleton("50")), target);
+ assertNotEquals(new DiscriminatorImpl(position(1, 3), Collections.singleton("50")), target);
}
@Test
void testToString() {
- String expected = new DiscriminatorImpl(1, 2, Collections.singleton("50")).toString();
+ String expected = new DiscriminatorImpl(position(1, 2), Collections.singleton("50")).toString();
assertEquals(expected, target.toString());
}
diff --git a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java
index 62597c3e..52ab6340 100644
--- a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java
@@ -24,11 +24,14 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.math.BigInteger;
+import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -38,6 +41,7 @@
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
+import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
@@ -2032,4 +2036,90 @@ void testTRADACOMS_IncorrectSegmentTagDelimiterIsInvalid() throws Exception {
EDIStreamException thrown = assertThrows(EDIStreamException.class, () -> reader.next());
assertTrue(thrown.getMessage().contains("Expected TRADACOMS segment tag delimiter"));
}
+
+ @Test
+ void testHierarchicalLoopsNested() throws IOException {
+ EDIInputFactory factory = EDIInputFactory.newFactory();
+ EDIStreamReader reader = factory.createEDIStreamReader(getClass().getResourceAsStream("/x12/sample837-HL-nested.edi"));
+ StringBuilder actual = new StringBuilder();
+ List