Repository: xlate/staedi (java)

Issue:
Implementation schemas not correctly validating when writing EDI
I'm running validation with the xml stream writer -- but it seem only be validating using the transaction.xml and not the implementation.xml
For example:
I have this in my Transaction.xml
```
                     .....
            <loop code="Loop1000" minOccurs="1" maxOccurs="4" >
                <sequence>
                    <segment type="N1"/>
                    <loop code="Loop1100" maxOccurs="1">
                        <sequence >
                            <segment type="ACT" minOccurs="1" />
                        </sequence>
                    </loop>
                </sequence>
            </loop>
            <loop name="Loop2000"  code="Loop2000" type="Loop2000"  maxOccurs="9999">
                <sequence>
                    <segment type="INS" minOccirs="1"/>
                     .....

```
and i have this, in  my Implementaion.xml (I have a B and C Loop1000 as well.. with a minOccurs=0, the A loop  is the only one required)
```
            <loop  title="Loop1000A" typeId="Loop1000A" code="Loop1000A" type="Loop1000" discriminator="1" minOccurs="1" maxOccurs="1">
                <sequence>
                    <segment type="N1" title="Sponsor Name"  minOccurs="1" description="1000">
                        <sequence>
                            <element position="1" title="Entity Identifier Code (Sponsor)" minOccurs="1">
                                <enumeration>
                                    <value>P5</value>
                                </enumeration>
                            </element>
                            <element position="2" minOccurs="0"/>
                            <element position="3" minOccurs="1"/>
                            <element position="4" minOccurs="1"/>
                        </sequence>
                    </segment>
                </sequence>
            </loop>
```

when I validate my transaction that has the B and C loop -- validation passes ,unexpectedly, since there is no matching 1000A loop.
if I remove the B and C loops as well.. then it fails. Not exactly as expected.. since   I get the error on the next segment
Encountered SEGMENT_ERROR [MANDATORY_SEGMENT_MISSING] in segment INS  --> the INS segment exists, its the first segment after the 1000 loop... it's the Loop1000 that's missing... but at least it fails .

My question is -- should the validation be performed using the implementation schema? Or is my expectation not correct?
Should the validation message indicate that it's the Loop1000 that's missing (as opposed to the INS)?

_Originally posted by @d-floyd in https://github.com/xlate/staedi/discussions/284_

Pull request description:
Fix selection of implementation loops/segments in EDI writer
Fixes #286 

Patch:
diff --git a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java
index 5ce2c42..fc88d0b 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java
@@ -24,8 +24,10 @@ import java.nio.CharBuffer;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Deque;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -42,6 +44,7 @@ import io.xlate.edi.internal.stream.tokenization.EDIException;
 import io.xlate.edi.internal.stream.tokenization.EDIFACTDialect;
 import io.xlate.edi.internal.stream.tokenization.ElementDataHandler;
 import io.xlate.edi.internal.stream.tokenization.State;
+import io.xlate.edi.internal.stream.tokenization.StreamEvent;
 import io.xlate.edi.internal.stream.tokenization.ValidationEventHandler;
 import io.xlate.edi.internal.stream.tokenization.X12Dialect;
 import io.xlate.edi.internal.stream.validation.UsageError;
@@ -96,6 +99,9 @@ public class StaEDIStreamWriter implements EDIStreamWriter, ElementDataHandler,
     private List<EDIValidationException> errors = new ArrayList<>();
     private CharArraySequence elementHolder = new CharArraySequence();
 
+    private final StreamEvent currentEvent = new StreamEvent();
+    private final Deque<StreamEvent> currentEventQueue = new LinkedList<>(Arrays.asList(currentEvent));
+
     private char segmentTerminator;
     private char segmentTagTerminator;
     private char dataElementSeparator;
@@ -214,7 +220,7 @@ public class StaEDIStreamWriter implements EDIStreamWriter, ElementDataHandler,
     }
 
     private void ensureLevelBetween(int min, int max) {
-        ensureFalse(this.level < min || this.level > max);
+        ensureFalse(this.level < min || this.level > max); // NOSONAR - ISE should be thrown when in an illegal state
     }
 
     @Override
@@ -527,7 +533,10 @@ public class StaEDIStreamWriter implements EDIStreamWriter, ElementDataHandler,
             validateElement(this.elementBuffer::flip, this.elementBuffer);
         }
         level = LEVEL_SEGMENT;
-        validate(validator -> validator.validateSyntax(dialect, this, this, location, false));
+        validate(validator -> {
+            validator.clearImplementationCandidates(this);
+            validator.validateSyntax(dialect, this, this, location, false);
+        });
 
         if (state == State.ELEMENT_DATA_BINARY) {
             state = State.ELEMENT_END_BINARY;
@@ -981,6 +990,11 @@ public class StaEDIStreamWriter implements EDIStreamWriter, ElementDataHandler,
             reportElementErrors(validator, elementData);
         }
 
+        if (validator.isPendingDiscrimination()) {
+            currentEvent.update(EDIStreamEvent.ELEMENT_DATA, null, data, null, location);
+            validator.selectImplementation(currentEventQueue, this);
+        }
+
         if (!errors.isEmpty()) {
             throw validationExceptionChain(errors);
         }
diff --git a/src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java b/src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java
index b8f136b..6557bfc 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java
@@ -43,6 +43,7 @@ import io.xlate.edi.stream.EDIInputFactory;
 import io.xlate.edi.stream.EDINamespaces;
 import io.xlate.edi.stream.EDIStreamEvent;
 import io.xlate.edi.stream.EDIStreamReader;
+import io.xlate.edi.stream.EDIValidationException;
 
 final class StaEDIXMLStreamReader implements XMLStreamReader {
 
@@ -251,17 +252,13 @@ final class StaEDIXMLStreamReader implements XMLStreamReader {
             break;
 
         case SEGMENT_ERROR:
-            throw new XMLStreamException(String.format("Segment %s has error %s",
-                                                       ediReader.getText(),
-                                                       ediReader.getErrorType()),
-                                         this.location);
-
         case ELEMENT_OCCURRENCE_ERROR:
         case ELEMENT_DATA_ERROR:
-            throw new XMLStreamException(String.format("Element %s has error %s",
-                                                       ediReader.getText(),
-                                                       ediReader.getErrorType()),
-                                         this.location);
+            EDIValidationException cause = new EDIValidationException(ediEvent,
+                ediReader.getErrorType(),
+                ediReader.getLocation().copy(),
+                ediReader.getText());
+            throw new XMLStreamException("Validation exception reading EDI data as XML", this.location, cause);
 
         default:
             throw new IllegalStateException("Unknown state: " + ediEvent);
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 2fad475..2bb0b82 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
@@ -471,11 +471,7 @@ public class ProxyEventHandler implements EventHandler {
             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);
+            openLevel.update(EDIStreamEvent.END_LOOP, startedLevel.errorType, startedLevel.data, startedLevel.typeReference, startedLevel.location);
 
             /*
              * startedLevelId will not be null due to Validator#validateSyntax.
@@ -595,12 +591,7 @@ public class ProxyEventHandler implements EventHandler {
                               StaEDIStreamLocation location) {
 
         StreamEvent target = getPooledEvent();
-
-        target.type = event;
-        target.errorType = error;
-        target.setData(data);
-        target.setTypeReference(typeReference);
-        target.setLocation(location);
+        target.update(event, error, data, typeReference, location);
 
         eventQueue.add(target);
     }
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 1283b6b..3df0452 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
@@ -23,6 +23,14 @@ public class StreamEvent {
 
     StaEDIStreamLocation location;
 
+    public void update(EDIStreamEvent type, EDIStreamValidationError errorType, CharSequence data, EDIReference typeReference, StaEDIStreamLocation location) {
+        this.type = type;
+        this.errorType = errorType;
+        setData(data);
+        setTypeReference(typeReference);
+        setLocation(location);
+    }
+
     @Override
     public String toString() {
         return String.format(TOSTRING_FORMAT, type, errorType, data, typeReference, location);
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 e0ad7f8..9ee4a05 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
@@ -632,7 +632,7 @@ public class Validator {
             } else if (node.getNodeType() == Type.SEGMENT) {
                 useErrors.add(new UsageError(segmentNode.getLink(), IMPLEMENTATION_SEGMENT_BELOW_MINIMUM_USE, node.getDepth()));
             } else {
-                useErrors.add(new UsageError(segmentNode.getLink(), IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, node.getDepth()));
+                useErrors.add(new UsageError(segmentNode.getParent().getLink(), IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, node.getDepth()));
             }
         }
     }
@@ -877,7 +877,7 @@ public class Validator {
             implSeg.incrementUsage();
 
             if (candidate.exceedsMaximumUsage(SEGMENT_VERSION)) {
-                handler.segmentError(implSeg.getId(), implSeg.getLink(), LOOP_OCCURS_OVER_MAXIMUM_TIMES);
+                handler.segmentError(implSeg.getId(), candidate.getLink(), LOOP_OCCURS_OVER_MAXIMUM_TIMES);
             }
         } else {
             candidate.incrementUsage();
@@ -1096,7 +1096,8 @@ public class Validator {
             return true;
         }
 
-        boolean valueReceived = value != null && value.length() > 0;
+        boolean valueReceived = value.length() > 0;
+
         clearElements();
         elementErrors.clear();
 
diff --git a/src/main/java/io/xlate/edi/stream/EDIInputFactory.java b/src/main/java/io/xlate/edi/stream/EDIInputFactory.java
index 1ef7dd7..025d9cd 100644
--- a/src/main/java/io/xlate/edi/stream/EDIInputFactory.java
+++ b/src/main/java/io/xlate/edi/stream/EDIInputFactory.java
@@ -118,11 +118,11 @@ public abstract class EDIInputFactory extends PropertySupport {
      *             additional information for non-textual event types.
      */
     @Deprecated
-    public static final String EDI_ENABLE_LOOP_TEXT = "io.xlate.edi.stream.EDI_ENABLE_LOOP_TEXT"; //NOSONAR
+    public static final String EDI_ENABLE_LOOP_TEXT = "io.xlate.edi.stream.EDI_ENABLE_LOOP_TEXT"; // NOSONAR
 
     /**
      * When set to true, simple data elements not containing data will be
-     * represented via the JSON parsers as a <i>null</i> value.
+     * represented via the JSON parsers as a {@code null} value.
      *
      * Default value: false
      *


Test patch:
diff --git a/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java b/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java
index 3978e20..3dcd343 100644
--- a/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
 import java.net.URL;
 import java.util.ArrayList;
@@ -30,16 +31,22 @@ import java.util.List;
 import org.junit.jupiter.api.Test;
 
 import io.xlate.edi.internal.schema.SchemaUtils;
+import io.xlate.edi.schema.EDIReference;
 import io.xlate.edi.schema.EDISchemaException;
+import io.xlate.edi.schema.EDIType;
 import io.xlate.edi.schema.Schema;
 import io.xlate.edi.schema.SchemaFactory;
 import io.xlate.edi.stream.EDIInputFactory;
+import io.xlate.edi.stream.EDIOutputErrorReporter;
+import io.xlate.edi.stream.EDIOutputFactory;
 import io.xlate.edi.stream.EDIStreamConstants.Standards;
 import io.xlate.edi.stream.EDIStreamEvent;
 import io.xlate.edi.stream.EDIStreamException;
 import io.xlate.edi.stream.EDIStreamFilter;
 import io.xlate.edi.stream.EDIStreamReader;
 import io.xlate.edi.stream.EDIStreamValidationError;
+import io.xlate.edi.stream.EDIStreamWriter;
+import io.xlate.edi.stream.Location;
 import io.xlate.edi.test.StaEDITestEvent;
 import io.xlate.edi.test.StaEDITestUtil;
 
@@ -453,7 +460,7 @@ class SegmentValidationTest {
 
         // Loop B - Occurrence 3
         assertEvent(reader, EDIStreamEvent.START_LOOP, "0000B");
-        assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.LOOP_OCCURS_OVER_MAXIMUM_TIMES, "S11");
+        assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.LOOP_OCCURS_OVER_MAXIMUM_TIMES, "S11", "0000B");
         assertEvent(reader, EDIStreamEvent.END_LOOP, "0000B");
 
         // Loop C - Occurrence 1
@@ -467,7 +474,7 @@ class SegmentValidationTest {
         assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.LOOP_OCCURS_OVER_MAXIMUM_TIMES, "S11", "L0000");
         assertEvent(reader, EDIStreamEvent.END_LOOP, "0000D");
 
-        assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, "S11");
+        assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, "0000C");
 
         // Loop L0001 has minOccurs=1 in standard (not used in implementation, invalid configuration)
         assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.MANDATORY_SEGMENT_MISSING, "S20");
@@ -543,7 +550,7 @@ class SegmentValidationTest {
     }
 
     @Test
-    void testImplementationValidSequenceAllMissing() throws EDISchemaException, EDIStreamException {
+    void testImplementationValidSequenceAllMissingReader() throws EDISchemaException, EDIStreamException {
         EDIInputFactory factory = EDIInputFactory.newFactory();
         InputStream stream = new ByteArrayInputStream((""
                 + "ISA*00*          *00*          *ZZ*ReceiverID     *ZZ*Sender         *050812*1953*^*00501*508121953*0*P*:~"
@@ -569,19 +576,19 @@ class SegmentValidationTest {
 
         assertEquals(EDIStreamEvent.SEGMENT_ERROR, reader.next());
         assertEquals(EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, reader.getErrorType());
-        assertEquals("S11", reader.getReferenceCode());
+        assertEquals("0000A", reader.getReferenceCode());
 
         assertEquals(EDIStreamEvent.SEGMENT_ERROR, reader.next());
         assertEquals(EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, reader.getErrorType());
-        assertEquals("S11", reader.getReferenceCode());
+        assertEquals("0000B", reader.getReferenceCode());
 
         assertEquals(EDIStreamEvent.SEGMENT_ERROR, reader.next());
         assertEquals(EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, reader.getErrorType());
-        assertEquals("S11", reader.getReferenceCode());
+        assertEquals("0000C", reader.getReferenceCode());
 
         assertEquals(EDIStreamEvent.SEGMENT_ERROR, reader.next());
         assertEquals(EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, reader.getErrorType());
-        assertEquals("S11", reader.getReferenceCode());
+        assertEquals("0000D", reader.getReferenceCode());
 
         // Loop L0001 has minOccurs=1 in standard (not used in implementation, invalid configuration)
         assertEquals(EDIStreamEvent.SEGMENT_ERROR, reader.next());
@@ -591,6 +598,170 @@ class SegmentValidationTest {
         assertTrue(!reader.hasNext(), "Unexpected segment errors exist");
     }
 
+    @Test
+    void testImplementationValidSequenceAllMissingWriter() throws EDISchemaException, EDIStreamException {
+        SchemaFactory schemaFactory = SchemaFactory.newFactory();
+        Schema ctlSchema = schemaFactory.createSchema(getClass().getResource("/x12/EDISchemaSegmentValidation.xml"));
+        Schema txnSchema = schemaFactory.createSchema(getClass().getResource("/x12/EDISchemaSegmentValidationImpl.xml"));
+
+        List<StaEDITestEvent> errorEvents = new ArrayList<>();
+
+        EDIOutputFactory factory = EDIOutputFactory.newFactory();
+        factory.setProperty(EDIOutputFactory.FORMAT_ELEMENTS, true);
+        factory.setErrorReporter(new EDIOutputErrorReporter() {
+            @Override
+            public void report(EDIStreamValidationError errorType,
+                               EDIStreamWriter writer,
+                               Location location,
+                               CharSequence data,
+                               EDIReference typeReference) {
+                EDIType type = typeReference instanceof EDIType ? EDIType.class.cast(typeReference) : typeReference.getReferencedType();
+                errorEvents.add(StaEDITestEvent.forError(errorType, data.toString(), type.getCode()));
+            }
+        });
+
+        EDIStreamWriter writer = factory.createEDIStreamWriter(new ByteArrayOutputStream());
+        writer.setControlSchema(ctlSchema);
+        writer.setTransactionSchema(txnSchema);
+
+        writer.startInterchange();
+        writer.writeStartSegment("ISA")
+            .writeElement("00")
+            .writeElement(" ")
+            .writeElement("00")
+            .writeElement(" ")
+            .writeElement("ZZ")
+            .writeElement("ReceiverID")
+            .writeElement("ZZ")
+            .writeElement("Sender")
+            .writeElement("203001")
+            .writeElement("1430")
+            .writeElement("^")
+            .writeElement("00501")
+            .writeElement("000000001")
+            .writeElement("0")
+            .writeElement("P")
+            .writeElement(":")
+            .writeEndSegment();
+        writer.writeStartSegment("S01")
+            .writeElement("X")
+            .writeEndSegment();
+        writer.writeStartSegment("S09")
+            .writeElement("X")
+            .writeEndSegment();
+        writer.writeStartSegment("IEA")
+            .writeElement("1")
+            .writeElement("000000001")
+            .writeEndSegment();
+
+        List<StaEDITestEvent> expected = Arrays.asList(
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, "L0000", "0000A"),
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, "L0000", "0000B"),
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, "L0000", "0000C"),
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, "L0000", "0000D"),
+            StaEDITestEvent.forError(EDIStreamValidationError.MANDATORY_SEGMENT_MISSING, "S20", "S20")
+        );
+
+        assertEquals(expected, errorEvents);
+    }
+
+    @Test
+    void testImplementationLoopsSelectedByWriter() throws EDISchemaException, EDIStreamException {
+        SchemaFactory schemaFactory = SchemaFactory.newFactory();
+        Schema ctlSchema = schemaFactory.createSchema(getClass().getResource("/x12/EDISchemaSegmentValidation.xml"));
+        Schema txnSchema = schemaFactory.createSchema(getClass().getResource("/x12/EDISchemaSegmentValidationImpl.xml"));
+
+        List<StaEDITestEvent> errorEvents = new ArrayList<>();
+
+        EDIOutputFactory factory = EDIOutputFactory.newFactory();
+        factory.setProperty(EDIOutputFactory.FORMAT_ELEMENTS, true);
+        factory.setErrorReporter(new EDIOutputErrorReporter() {
+            @Override
+            public void report(EDIStreamValidationError errorType,
+                               EDIStreamWriter writer,
+                               Location location,
+                               CharSequence data,
+                               EDIReference typeReference) {
+                EDIType type = typeReference instanceof EDIType ? EDIType.class.cast(typeReference) : typeReference.getReferencedType();
+                errorEvents.add(StaEDITestEvent.forError(errorType, data.toString(), type.getCode()));
+            }
+        });
+
+        EDIStreamWriter writer = factory.createEDIStreamWriter(new ByteArrayOutputStream());
+        writer.setControlSchema(ctlSchema);
+        writer.setTransactionSchema(txnSchema);
+
+        writer.startInterchange();
+        writer.writeStartSegment("ISA")
+            .writeElement("00")
+            .writeElement(" ")
+            .writeElement("00")
+            .writeElement(" ")
+            .writeElement("ZZ")
+            .writeElement("ReceiverID")
+            .writeElement("ZZ")
+            .writeElement("Sender")
+            .writeElement("203001")
+            .writeElement("1430")
+            .writeElement("^")
+            .writeElement("00501")
+            .writeElement("000000001")
+            .writeElement("0")
+            .writeElement("P")
+            .writeElement(":")
+            .writeEndSegment();
+        writer.writeStartSegment("S01")
+            .writeElement("X")
+            .writeEndSegment();
+
+        // 0000A
+        writer.writeStartSegment("S11")
+            .writeElement("A")
+            .writeEndSegment();
+        // Unused for impl loop 0000A
+        writer.writeStartSegment("S12")
+            .writeElement("X")
+            .writeEndSegment();
+        // skipping S13, required in 0000A
+
+        // 0000B
+        for (int i = 0; i < 3; i++) {
+            // loop can only appear twice, but we write 3 times
+            writer.writeStartSegment("S11")
+                .writeElement("B")
+                .writeEndSegment();
+        }
+
+        // 0000C - requires exactly two occurrence, but we write once
+        writer.writeStartSegment("S11")
+            .writeElement("C")
+            .writeEndSegment();
+
+        // S20 is unused by the impl, but required by the standard (i.e. the impl is buggy/disagrees w/standard)
+        writer.writeStartSegment("S20")
+            .writeElement("X")
+            .writeEndSegment();
+
+        writer.writeStartSegment("S09")
+            .writeElement("X")
+            .writeEndSegment();
+        writer.writeStartSegment("IEA")
+            .writeElement("1")
+            .writeElement("000000001")
+            .writeEndSegment();
+
+        List<StaEDITestEvent> expected = Arrays.asList(
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "S12", "S12"),
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_SEGMENT_BELOW_MINIMUM_USE, "S13", "S13"),
+            StaEDITestEvent.forError(EDIStreamValidationError.LOOP_OCCURS_OVER_MAXIMUM_TIMES, "S11", "0000B"),
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, "L0000", "0000C"),
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, "L0000", "0000D"),
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "S20", "S20")
+        );
+
+        assertEquals(expected, errorEvents);
+    }
+
     @SuppressWarnings("deprecation")
     @Test
     void testImplementationValidSequenceWithCompositeDiscr() throws EDISchemaException, EDIStreamException {
diff --git a/src/test/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReaderTest.java b/src/test/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReaderTest.java
index 2a21291..0f12363 100644
--- a/src/test/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReaderTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReaderTest.java
@@ -77,6 +77,7 @@ import io.xlate.edi.stream.EDIStreamFilter;
 import io.xlate.edi.stream.EDIStreamReader;
 import io.xlate.edi.stream.EDIStreamValidationError;
 import io.xlate.edi.stream.EDIStreamWriter;
+import io.xlate.edi.stream.EDIValidationException;
 import io.xlate.edi.stream.Location;
 import io.xlate.edi.test.StaEDITestUtil;
 
@@ -896,7 +897,11 @@ class StaEDIXMLStreamReaderTest {
         javax.xml.stream.Location l = ((XMLStreamException) cause).getLocation();
         assertEquals("ParseError at [row,col]:[" + l.getLineNumber() + "," +
                 l.getColumnNumber() + "]\n" +
-                "Message: " + "Segment IK5 has error UNEXPECTED_SEGMENT", cause.getMessage());
+                "Message: Validation exception reading EDI data as XML", cause.getMessage());
+
+        Throwable rootCause = ((XMLStreamException) cause).getNestedException();
+        assertTrue(rootCause instanceof EDIValidationException);
+        assertEquals("Encountered SEGMENT_ERROR [UNEXPECTED_SEGMENT] in segment IK5 at position 4", rootCause.getMessage());
     }
 
     @Test