Repository: xlate/staedi (java)

Issue:
Loop event types should not allow text retrieval
Currently, loop event types (loop, transaction, group, interchange) support the `getText` (etc) methods which return the `code` from any associated schema. These are not valid text processing states and the parser should throw an `IllegalStateException` in these cases. 

Without a major release, the fix should include a deprecated property to keep the existing behavior in place. The next major version will remove the property and the incorrect functionality.

Pull request:
Deprecate retrieval of reference code via text for structural events
Fixes #225 

Patch:
diff --git a/src/main/java/io/xlate/edi/internal/stream/StaEDIFilteredStreamReader.java b/src/main/java/io/xlate/edi/internal/stream/StaEDIFilteredStreamReader.java
index b225bc4..2fca429 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIFilteredStreamReader.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIFilteredStreamReader.java
@@ -211,4 +211,9 @@ class StaEDIFilteredStreamReader implements EDIStreamReader {
     public EDIReference getSchemaTypeReference() {
         return delegate.getSchemaTypeReference();
     }
+
+    @Override
+    public boolean hasText() {
+        return delegate.hasText();
+    }
 }
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 92cda8d..12c240b 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIInputFactory.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIInputFactory.java
@@ -35,11 +35,13 @@ public class StaEDIInputFactory extends EDIInputFactory {
 
     private EDIInputErrorReporter reporter;
 
+    @SuppressWarnings("deprecation")
     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(EDI_ENABLE_LOOP_TEXT);
 
         supportedProperties.add(XML_DECLARE_TRANSACTION_XMLNS);
         supportedProperties.add(XML_WRAP_TRANSACTION_CONTENTS);
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 7ea912a..f0affef 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java
@@ -47,6 +47,8 @@ import io.xlate.edi.stream.Location;
 public class StaEDIStreamReader implements EDIStreamReader, Configurable {
 
     private static final Logger LOGGER = Logger.getLogger(StaEDIStreamReader.class.getName());
+    private static final CharBuffer GROUP_TEXT = CharBuffer.wrap(ProxyEventHandler.LOOP_CODE_GROUP);
+    private static final CharBuffer TRANSACTION_TEXT = CharBuffer.wrap(ProxyEventHandler.LOOP_CODE_TRANSACTION);
 
     private Schema controlSchema;
     private final Map<String, Object> properties;
@@ -57,6 +59,7 @@ public class StaEDIStreamReader implements EDIStreamReader, Configurable {
 
     private boolean complete = false;
     private boolean closed = false;
+    private boolean deprecationLogged = false;
 
     public StaEDIStreamReader(
             InputStream stream,
@@ -102,9 +105,38 @@ public class StaEDIStreamReader implements EDIStreamReader, Configurable {
         throw new IllegalStateException(message);
     }
 
+    private void logDeprecation(EDIStreamEvent event) {
+        if (!deprecationLogged) {
+            deprecationLogged = true;
+
+            LOGGER.warning(() -> "DEPRECATION - Retrieving text for event " + event + " will not be supported in a future release. "
+                    + "Use `getReferenceCode` or `getSchemaTypeReference` to retrieve additional information for non-textual event types.");
+        }
+    }
+
     private CharBuffer getBuffer() {
         checkTextState();
-        return proxy.getCharacters();
+        EDIStreamEvent event = getEventType();
+
+        switch (event) {
+        case START_GROUP:
+        case END_GROUP:
+            logDeprecation(event);
+            return GROUP_TEXT;
+
+        case START_TRANSACTION:
+        case END_TRANSACTION:
+            logDeprecation(event);
+            return TRANSACTION_TEXT;
+
+        case START_LOOP:
+        case END_LOOP:
+            logDeprecation(event);
+            return CharBuffer.wrap(proxy.getSchemaTypeReference().getReferencedType().getCode());
+
+        default:
+            return proxy.getCharacters();
+        }
     }
 
     @Override
@@ -187,6 +219,7 @@ public class StaEDIStreamReader implements EDIStreamReader, Configurable {
             try {
                 this.setBinaryDataLength(Long.parseLong(getText()));
             } catch (NumberFormatException e) {
+                lexer.invalidate();
                 throw new EDIStreamException("Failed to parse binary element length", location, e);
             }
         }
@@ -339,28 +372,36 @@ public class StaEDIStreamReader implements EDIStreamReader, Configurable {
     }
 
     private void checkTextState() {
+        if (!hasText()) {
+            throw new IllegalStateException("not a valid text state [" + getEventType() + ']');
+        }
+    }
+
+    @Override
+    public boolean hasText() {
         EDIStreamEvent event = getEventType();
 
         if (event == null) {
-            throw new IllegalStateException("not a valid text state [" + event + ']');
+            return false;
         }
 
         switch (event) {
-        case START_GROUP:
-        case START_TRANSACTION:
-        case START_LOOP:
         case START_SEGMENT:
-        case END_GROUP:
-        case END_TRANSACTION:
-        case END_LOOP:
         case END_SEGMENT:
         case ELEMENT_DATA:
         case ELEMENT_DATA_ERROR:
         case ELEMENT_OCCURRENCE_ERROR:
         case SEGMENT_ERROR:
-            break;
+            return true;
+        case START_GROUP:
+        case START_TRANSACTION:
+        case START_LOOP:
+        case END_GROUP:
+        case END_TRANSACTION:
+        case END_LOOP:
+            return enableLoopText();
         default:
-            throw new IllegalStateException("not a valid text state [" + event + ']');
+            return false;
         }
     }
 
@@ -477,4 +518,10 @@ public class StaEDIStreamReader implements EDIStreamReader, Configurable {
     boolean nestHierarchicalLoops() {
         return getProperty(EDIInputFactory.EDI_NEST_HIERARCHICAL_LOOPS, Boolean::parseBoolean, true);
     }
+
+    @SuppressWarnings("deprecation")
+    boolean enableLoopText() {
+        return getProperty(EDIInputFactory.EDI_ENABLE_LOOP_TEXT, Boolean::parseBoolean, true);
+    }
+
 }
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 b634bc4..6a19466 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java
@@ -35,6 +35,7 @@ import javax.xml.stream.Location;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamReader;
 
+import io.xlate.edi.internal.stream.tokenization.ProxyEventHandler;
 import io.xlate.edi.schema.EDIComplexType;
 import io.xlate.edi.schema.EDIReference;
 import io.xlate.edi.stream.EDIInputFactory;
@@ -47,7 +48,7 @@ final class StaEDIXMLStreamReader implements XMLStreamReader {
     private static final Logger LOGGER = Logger.getLogger(StaEDIXMLStreamReader.class.getName());
     private static final QName DUMMY_QNAME = new QName("DUMMY");
     private static final QName INTERCHANGE = new QName(EDINamespaces.LOOPS, "INTERCHANGE", prefixOf(EDINamespaces.LOOPS));
-    private static final QName TRANSACTION = new QName(EDINamespaces.LOOPS, "TRANSACTION", prefixOf(EDINamespaces.LOOPS));
+    private static final QName TRANSACTION = new QName(EDINamespaces.LOOPS, ProxyEventHandler.LOOP_CODE_TRANSACTION, prefixOf(EDINamespaces.LOOPS));
 
     private final EDIStreamReader ediReader;
     private final Map<String, Object> properties;
@@ -206,15 +207,14 @@ final class StaEDIXMLStreamReader implements XMLStreamReader {
 
         case START_TRANSACTION:
             withinTransaction = true;
-            readerText = ediReader.getText();
-            name = buildName(elementStack.getFirst(), EDINamespaces.LOOPS, readerText);
+            name = buildName(elementStack.getFirst(), EDINamespaces.LOOPS, ediReader.getReferenceCode());
             enqueueEvent(START_ELEMENT, name, true);
             determineTransactionSegments();
             break;
 
         case START_GROUP:
         case START_LOOP:
-            name = buildName(elementStack.getFirst(), EDINamespaces.LOOPS, ediReader.getText());
+            name = buildName(elementStack.getFirst(), EDINamespaces.LOOPS, ediReader.getReferenceCode());
             enqueueEvent(START_ELEMENT, name, true);
             break;
 
diff --git a/src/main/java/io/xlate/edi/internal/stream/json/StaEDIJsonParser.java b/src/main/java/io/xlate/edi/internal/stream/json/StaEDIJsonParser.java
index f5a8661..3bed270 100644
--- a/src/main/java/io/xlate/edi/internal/stream/json/StaEDIJsonParser.java
+++ b/src/main/java/io/xlate/edi/internal/stream/json/StaEDIJsonParser.java
@@ -231,7 +231,7 @@ abstract class StaEDIJsonParser implements Configurable {
         case START_GROUP:
         case START_TRANSACTION:
         case START_LOOP:
-            enqueueStructureBegin("loop", ediReader.getText());
+            enqueueStructureBegin("loop", ediReader.getReferenceCode());
             break;
         case START_SEGMENT:
             enqueueStructureBegin("segment", ediReader.getText());
diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/Lexer.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/Lexer.java
index c0f2b80..c13ef60 100644
--- a/src/main/java/io/xlate/edi/internal/stream/tokenization/Lexer.java
+++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/Lexer.java
@@ -145,6 +145,13 @@ public class Lexer {
         return dialect;
     }
 
+    public void invalidate() {
+        if (state != State.INVALID) {
+            previous = state;
+            state = State.INVALID;
+        }
+    }
+
     public void setBinaryLength(long binaryLength) {
         this.binaryRemain = binaryLength;
 
@@ -604,4 +611,14 @@ public class Lexer {
             throw error(EDIException.INVALID_STATE);
         }
     }
+
+    /* test */
+    State currentState() {
+        return state;
+    }
+
+    /* test */
+    State previousState() {
+        return previous;
+    }
 }
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 fc42dea..73c10b8 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
@@ -40,6 +40,9 @@ import io.xlate.edi.stream.Location;
 
 public class ProxyEventHandler implements EventHandler {
 
+    public static final String LOOP_CODE_GROUP = EDIType.Type.GROUP.toString();
+    public static final String LOOP_CODE_TRANSACTION = EDIType.Type.TRANSACTION.toString();
+
     private final StaEDIStreamLocation location;
     private final boolean nestHierarchicalLoops;
 
@@ -200,17 +203,17 @@ public class ProxyEventHandler implements EventHandler {
     public void loopBegin(EDIReference typeReference) {
         final String loopCode = typeReference.getReferencedType().getCode();
 
-        if (EDIType.Type.TRANSACTION.toString().equals(loopCode)) {
+        if (LOOP_CODE_TRANSACTION.equals(loopCode)) {
             transaction = true;
             transactionSchemaAllowed = true;
-            enqueueEvent(EDIStreamEvent.START_TRANSACTION, EDIStreamValidationError.NONE, loopCode, typeReference, location);
+            enqueueEvent(EDIStreamEvent.START_TRANSACTION, EDIStreamValidationError.NONE, null, typeReference, location);
             if (transactionValidator != null) {
                 transactionValidator.reset();
             }
-        } else if (EDIType.Type.GROUP.toString().equals(loopCode)) {
-            enqueueEvent(EDIStreamEvent.START_GROUP, EDIStreamValidationError.NONE, loopCode, typeReference, location);
+        } else if (LOOP_CODE_GROUP.equals(loopCode)) {
+            enqueueEvent(EDIStreamEvent.START_GROUP, EDIStreamValidationError.NONE, null, typeReference, location);
         } else {
-            enqueueEvent(EDIStreamEvent.START_LOOP, EDIStreamValidationError.NONE, loopCode, typeReference, location);
+            enqueueEvent(EDIStreamEvent.START_LOOP, EDIStreamValidationError.NONE, null, typeReference, location);
 
             if (nestHierarchicalLoops && isHierarchicalLoop(typeReference.getReferencedType())) {
                 EDILoopType loop = (EDILoopType) typeReference.getReferencedType();
@@ -229,17 +232,17 @@ public class ProxyEventHandler implements EventHandler {
         // Validator can not be null when a loopEnd event has been signaled.
         validator().validateLoopSyntax(this);
 
-        if (EDIType.Type.TRANSACTION.toString().equals(loopCode)) {
+        if (LOOP_CODE_TRANSACTION.equals(loopCode)) {
             transaction = false;
             dialect.transactionEnd();
-            enqueueEvent(EDIStreamEvent.END_TRANSACTION, EDIStreamValidationError.NONE, loopCode, typeReference, location);
-        } else if (EDIType.Type.GROUP.toString().equals(loopCode)) {
+            enqueueEvent(EDIStreamEvent.END_TRANSACTION, EDIStreamValidationError.NONE, null, typeReference, location);
+        } else if (LOOP_CODE_GROUP.equals(loopCode)) {
             dialect.groupEnd();
-            enqueueEvent(EDIStreamEvent.END_GROUP, EDIStreamValidationError.NONE, loopCode, typeReference, location);
+            enqueueEvent(EDIStreamEvent.END_GROUP, EDIStreamValidationError.NONE, null, typeReference, location);
         } else if (nestHierarchicalLoops && isHierarchicalLoop(typeReference.getReferencedType())) {
             levelCheckPending = true;
         } else {
-            enqueueEvent(EDIStreamEvent.END_LOOP, EDIStreamValidationError.NONE, loopCode, typeReference, location);
+            enqueueEvent(EDIStreamEvent.END_LOOP, EDIStreamValidationError.NONE, null, typeReference, location);
         }
     }
 
diff --git a/src/main/java/io/xlate/edi/stream/EDIInputFactory.java b/src/main/java/io/xlate/edi/stream/EDIInputFactory.java
index c219ca5..1f85f80 100644
--- a/src/main/java/io/xlate/edi/stream/EDIInputFactory.java
+++ b/src/main/java/io/xlate/edi/stream/EDIInputFactory.java
@@ -66,15 +66,33 @@ public abstract class EDIInputFactory extends PropertySupport {
      * 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, functional group, transaction, and loop start/end
+     * events will allow for {@link EDIStreamReader#getText()} to be called,
+     * which is the legacy behavior.
+     *
+     * The default value is `true` and this property is deprecated. In the next
+     * major release, the property's default value will be `false`.
+     *
+     * Default value: true
+     *
+     * @since 1.20
+     * @deprecated use {@link EDIStreamReader#getReferenceCode()} and
+     *             {@link EDIStreamReader#getSchemaTypeReference()} to retrieve
+     *             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
+
     /**
      * When set to true, simple data elements not containing data will be
      * represented via the JSON parsers as a <i>null</i> value.
diff --git a/src/main/java/io/xlate/edi/stream/EDIStreamReader.java b/src/main/java/io/xlate/edi/stream/EDIStreamReader.java
index 96dd24f..2f9aeb6 100644
--- a/src/main/java/io/xlate/edi/stream/EDIStreamReader.java
+++ b/src/main/java/io/xlate/edi/stream/EDIStreamReader.java
@@ -282,23 +282,37 @@ public interface EDIStreamReader extends Closeable, EDIStreamConstants {
 
     /**
      * Returns the current value of the parse event as a string. This returns
-     * the string value of an ELEMENT_DATA event, and the string value of a
-     * segment tag in a START_SEGMENT event. During an ELEMENT_ERROR event, this
-     * contains the invalid element (when available).
      *
-     * @return the current text or null
+     * <ul>
+     * <li>the string value of an element for an
+     * {@link EDIStreamEvent#ELEMENT_DATA ELEMENT_DATA} event
+     * <li>the string value of a segment tag for a
+     * {@link EDIStreamEvent#START_SEGMENT START_SEGMENT} event
+     * <li>the string value of the current segment tag for an
+     * {@link EDIStreamEvent#END_SEGMENT END_SEGMENT} event
+     * <li>the invalid element text from an
+     * {@link EDIStreamEvent#ELEMENT_DATA_ERROR ELEMENT_DATA_ERROR} (when
+     * available)
+     * <li>the invalid element text from an
+     * {@link EDIStreamEvent#ELEMENT_OCCURRENCE_ERROR ELEMENT_OCCURRENCE_ERROR}
+     * (when available)
+     * <li>the string value of a segment tag for a
+     * {@link EDIStreamEvent#SEGMENT_ERROR SEGMENT_ERROR} event
+     * </ul>
+     *
+     * @return the current text or an empty {@link java.lang.String String}
      * @throws IllegalStateException
      *             if this state is not a valid text state
      */
     String getText();
 
     /**
-     * Returns an array which contains the characters from this event. This
-     * array should be treated as read-only and transient. I.e. the array will
-     * contain the text characters until the EDIStreamReader moves on to the
-     * next event. Attempts to hold onto the character array beyond that time or
-     * modify the contents of the array are breaches of the contract for this
-     * interface.
+     * Returns an array which contains the characters from this event (as
+     * specified by {@link #getText}). This array should be treated as read-only
+     * and transient. I.e. the array will contain the text characters until the
+     * EDIStreamReader moves on to the next event. Attempts to hold onto the
+     * character array beyond that time or modify the contents of the array are
+     * breaches of the contract for this interface.
      *
      * @return the current text or an empty array
      * @throws IllegalStateException
@@ -307,16 +321,16 @@ public interface EDIStreamReader extends Closeable, EDIStreamConstants {
     char[] getTextCharacters();
 
     /**
-     * Gets the the text associated with a ELEMENT_DATA, ELEMENT_ERROR,
-     * START_SEGMENT, or END_SEGMENT event. Text starting at "sourceStart" is
-     * copied into "target" starting at "targetStart". Up to "length" characters
-     * are copied. The number of characters actually copied is returned. The
-     * "sourceStart" argument must be greater or equal to 0 and less than or
-     * equal to the number of characters associated with the event. Usually, one
-     * requests text starting at a "sourceStart" of 0. If the number of
-     * characters actually copied is less than the "length", then there is no
-     * more text. Otherwise, subsequent calls need to be made until all text has
-     * been retrieved.
+     * Returns the the text associated with an event (as specified by
+     * {@link #getText}). Text starting at "sourceStart" is copied into "target"
+     * starting at "targetStart". Up to "length" characters are copied. The
+     * number of characters actually copied is returned. The "sourceStart"
+     * argument must be greater or equal to 0 and less than or equal to the
+     * number of characters associated with the event. Usually, one requests
+     * text starting at a "sourceStart" of 0. If the number of characters
+     * actually copied is less than the "length", then there is no more text.
+     * Otherwise, subsequent calls need to be made until all text has been
+     * retrieved.
      *
      * For example:
      *
@@ -446,4 +460,21 @@ public interface EDIStreamReader extends Closeable, EDIStreamConstants {
      * @since 1.9
      */
     EDIReference getSchemaTypeReference();
+
+    /**
+     * Return true if the current event has text, false otherwise. The following
+     * events have text:
+     * <ul>
+     * <li>START_SEGMENT
+     * <li>END_SEGMENT
+     * <li>ELEMENT_DATA
+     * <li>ELEMENT_DATA_ERROR
+     * <li>ELEMENT_OCCURRENCE_ERROR
+     * <li>SEGMENT_ERROR
+     * </ul>
+     *
+     * @return true if the current event has text, false otherwise
+     * @since 1.20
+     */
+    boolean hasText();
 }


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 dacd09e..69c6590 100644
--- a/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java
@@ -475,9 +475,11 @@ class SegmentValidationTest {
         assertTrue(!reader.hasNext(), "Unexpected segment errors exist");
     }
 
+    @SuppressWarnings("deprecation")
     @Test
     void testImplementationValidAlternateSequence() throws EDISchemaException, EDIStreamException {
         EDIInputFactory factory = EDIInputFactory.newFactory();
+        factory.setProperty(EDIInputFactory.EDI_ENABLE_LOOP_TEXT, "false");
         InputStream stream = new ByteArrayInputStream((""
                 + "ISA*00*          *00*          *ZZ*ReceiverID     *ZZ*Sender         *050812*1953*^*00501*508121953*0*P*:~"
                 + "S01*X~"
@@ -510,22 +512,22 @@ class SegmentValidationTest {
         reader.setTransactionSchema(schemaFactory.createSchema(getClass().getResource("/x12/EDISchemaSegmentValidationImpl.xml")));
 
         List<StaEDITestEvent> expected = Arrays.asList(
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, "TRANSACTION", "TRANSACTION"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, null, "TRANSACTION"),
             // Loop A
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0000", "0000A"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0000", "0000A"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, null, "0000A"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, null, "0000A"),
             // Loop C - Occurrence 1
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0000", "0000C"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0000", "0000C"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, null, "0000C"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, null, "0000C"),
             // Loop B
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0000", "0000B"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0000", "0000B"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, null, "0000B"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, null, "0000B"),
             // Loop D
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0000", "0000D"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0000", "0000D"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, null, "0000D"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, null, "0000D"),
             // Loop C - Occurrence 2
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0000", "0000C"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0000", "0000C"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, null, "0000C"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, null, "0000C"),
 
             StaEDITestEvent.forError(EDIStreamValidationError.MANDATORY_SEGMENT_MISSING, "S20", "S20"));
 
@@ -588,9 +590,11 @@ class SegmentValidationTest {
         assertTrue(!reader.hasNext(), "Unexpected segment errors exist");
     }
 
+    @SuppressWarnings("deprecation")
     @Test
     void testImplementationValidSequenceWithCompositeDiscr() throws EDISchemaException, EDIStreamException {
         EDIInputFactory factory = EDIInputFactory.newFactory();
+        factory.setProperty(EDIInputFactory.EDI_ENABLE_LOOP_TEXT, "false");
         SchemaFactory schemaFactory = SchemaFactory.newFactory();
         InputStream stream = new ByteArrayInputStream((""
                 + "ISA*00*          *00*          *ZZ*ReceiverID     *ZZ*Sender         *050812*1953*^*00501*508121953*0*P*:~"
@@ -622,18 +626,18 @@ class SegmentValidationTest {
         reader.setTransactionSchema(schemaFactory.createSchema(getClass().getResource("/x12/EDISchemaSegmentValidationImpl2.xml")));
 
         List<StaEDITestEvent> expected = Arrays.asList(
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, "TRANSACTION", "TRANSACTION"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, null, "TRANSACTION"),
             // Loop A
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0000", "0000A"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, null, "0000A"),
             // Loop AXX
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0002", "0002AXX"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0002", "0002AXX"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, null, "0002AXX"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, null, "0002AXX"),
             // Loop AYY
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0002", "0002AYY"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, null, "0002AYY"),
             StaEDITestEvent.forError(EDIStreamValidationError.SEGMENT_EXCEEDS_MAXIMUM_USE, "S31", "S31"),
             StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_SEGMENT_BELOW_MINIMUM_USE, "S31", "S31B"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0002", "0002AYY"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0000", "0000A"));
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, null, "0002AYY"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, null, "0000A"));
 
         List<StaEDITestEvent> events = new ArrayList<>();
         events.add(StaEDITestEvent.from(reader, false));
@@ -645,9 +649,11 @@ class SegmentValidationTest {
         assertEquals(expected, events);
     }
 
+    @SuppressWarnings("deprecation")
     @Test
     void testImplementation_Only_BHT_HL_Valid() throws EDISchemaException, EDIStreamException {
         EDIInputFactory factory = EDIInputFactory.newFactory();
+        factory.setProperty(EDIInputFactory.EDI_ENABLE_LOOP_TEXT, "false");
         SchemaFactory schemaFactory = SchemaFactory.newFactory();
         InputStream stream = getClass().getResourceAsStream("/x12/sample837-small.edi");
 
@@ -664,24 +670,24 @@ class SegmentValidationTest {
         reader.setTransactionSchema(schemaFactory.createSchema(getClass().getResource("/x12/005010X222/837.xml")));
 
         List<StaEDITestEvent> expected = Arrays.asList(
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, "TRANSACTION", "TRANSACTION"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, null, "TRANSACTION"),
             // Occurrence 1
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0001", "L0001"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, null, "L0001"),
             StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "NM1", "NM1"),
             StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "PER", "PER"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0001", "L0001"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, null, "L0001"),
             // Occurrence 2
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0001", "L0001"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, null, "L0001"),
             StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "NM1", "NM1"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0001", "L0001"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, null, "L0001"),
             // Loop 2010A
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0002", "2010A"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, null, "2010A"),
             StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "PRV", "PRV"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0002", "2010A"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, "TRANSACTION", "TRANSACTION"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, null, "2010A"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, null, "TRANSACTION"),
             // Loop 2010A
-            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0002", "2010A"),
-            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0002", "2010A"));
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, null, "2010A"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, null, "2010A"));
 
         List<StaEDITestEvent> events = new ArrayList<>();
         events.add(StaEDITestEvent.from(reader, false));
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 52ab634..2f19113 100644
--- a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java
@@ -1215,7 +1215,6 @@ class StaEDIStreamReaderTest implements ConstantsTest {
         EDIStreamReader reader = factory.createEDIStreamReader(stream);
 
         EDIStreamEvent event;
-        String tag = null;
         EDIStreamException thrown = null;
 
         try {
@@ -1226,9 +1225,7 @@ class StaEDIStreamReaderTest implements ConstantsTest {
                     break;
                 }
 
-                tag = reader.getText();
-
-                if ("BIN".equals(tag)) {
+                if (reader.getEventType() == EDIStreamEvent.START_SEGMENT && "BIN".equals(reader.getText())) {
                     reader.next();
                     long binaryDataLength = Long.parseLong(reader.getText());
                     assertEquals(1839, binaryDataLength);
@@ -1255,33 +1252,33 @@ class StaEDIStreamReaderTest implements ConstantsTest {
         Schema schema = schemaFactory.createSchema(getClass().getResource("/x12/EDISchemaBinarySegment.xml"));
 
         EDIStreamEvent event;
-        String tag = null;
-        EDIStreamException thrown = null;
+        EDIStreamException bin01ParseException = null;
+        EDIStreamException inconsistentParser = null;
 
         try {
             while (reader.hasNext()) {
                 try {
-                    reader.nextTag();
+                    event = reader.nextTag();
                 } catch (NoSuchElementException e) {
                     break;
                 }
 
-                if (reader.getEventType() == EDIStreamEvent.START_TRANSACTION) {
+                if (event == EDIStreamEvent.START_TRANSACTION) {
                     reader.setTransactionSchema(schema);
                     assertThrows(IllegalStateException.class, () -> reader.setBinaryDataLength(1L));
-                } else {
-                    tag = reader.getText();
-
-                    if ("BIN".equals(tag)) {
-                        thrown = assertThrows(EDIStreamException.class, () -> reader.nextTag());
-                        break;
-                    }
+                } else if (event == EDIStreamEvent.START_SEGMENT && "BIN".equals(reader.getText())) {
+                    // BIN01 is non-numeric
+                    bin01ParseException = assertThrows(EDIStreamException.class, () -> reader.nextTag());
+                    inconsistentParser = assertThrows(EDIStreamException.class, () -> reader.nextTag());
+                    break;
                 }
             }
         } finally {
             reader.close();
         }
-        assertNotNull(thrown);
+
+        assertNotNull(bin01ParseException);
+        assertNotNull(inconsistentParser);
     }
 
     @Test
@@ -1295,7 +1292,6 @@ class StaEDIStreamReaderTest implements ConstantsTest {
         EDIStreamReader reader = factory.createEDIStreamReader(stream);
 
         EDIStreamEvent event;
-        String tag = null;
 
         try {
             while (reader.hasNext()) {
@@ -1305,9 +1301,7 @@ class StaEDIStreamReaderTest implements ConstantsTest {
                     break;
                 }
 
-                tag = reader.getText();
-
-                if ("BIN".equals(tag)) {
+                if (reader.getEventType() == EDIStreamEvent.START_SEGMENT && "BIN".equals(reader.getText())) {
                     reader.next();
                     long binaryDataLength = Long.parseLong(reader.getText());
                     assertEquals(2768, binaryDataLength);
diff --git a/src/test/java/io/xlate/edi/internal/stream/tokenization/LexerTest.java b/src/test/java/io/xlate/edi/internal/stream/tokenization/LexerTest.java
index bfad343..0c8ebb5 100644
--- a/src/test/java/io/xlate/edi/internal/stream/tokenization/LexerTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/tokenization/LexerTest.java
@@ -16,6 +16,7 @@
 package io.xlate.edi.internal.stream.tokenization;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
@@ -392,4 +393,21 @@ class LexerTest {
         MalformedInputException thrown = assertThrows(MalformedInputException.class, lexer::parse);
         assertEquals("Input length = 1", thrown.getMessage());
     }
+
+    @Test
+    void testPreviousStateRetainedWhenInvalidateInvoked() {
+        InputStream stream = new ByteArrayInputStream(new byte[0]);
+        TestLexerEventHandler eventHandler = new TestLexerEventHandler();
+        final StaEDIStreamLocation location = new StaEDIStreamLocation();
+        final Lexer lexer = new Lexer(stream, StandardCharsets.US_ASCII, eventHandler, location, false);
+
+        assertEquals(State.INITIAL, lexer.currentState());
+        assertNull(lexer.previousState());
+
+        for (int i = 0; i < 2; i++) {
+            lexer.invalidate();
+            assertEquals(State.INVALID, lexer.currentState());
+            assertEquals(State.INITIAL, lexer.previousState());
+        }
+    }
 }
diff --git a/src/test/java/io/xlate/edi/test/StaEDITestEvent.java b/src/test/java/io/xlate/edi/test/StaEDITestEvent.java
index ab920de..565b361 100644
--- a/src/test/java/io/xlate/edi/test/StaEDITestEvent.java
+++ b/src/test/java/io/xlate/edi/test/StaEDITestEvent.java
@@ -41,7 +41,7 @@ public class StaEDITestEvent {
 
         return new StaEDITestEvent(reader.getEventType(),
                                    error ? reader.getErrorType() : null,
-                                   reader.getText(),
+                                   reader.hasText() ? reader.getText() : null,
                                    reader.getReferenceCode(),
                                    includeLocation ? reader.getLocation().copy() : null);
     }