Repository: xlate/staedi (java)

Issue:
Support multiple ISA segments in an X12 EDI file
### Discussed in https://github.com/xlate/staedi/discussions/352

<div type='discussions-op-text'>

<sup>Originally posted by **dineshraghuramaiah** April 25, 2023</sup>
does the staedi supports multiple ISA segments in an X12 EDI file? </div>

The EDIStreamReader should support multiple multiple interchanges in the input stream.

Pull request:
Continue reading input following end of interchange
Fixes #358

Patch:
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 f0affef..0b41c47 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java
@@ -31,6 +31,7 @@ import java.util.stream.Collectors;
 
 import io.xlate.edi.internal.schema.SchemaUtils;
 import io.xlate.edi.internal.stream.tokenization.Dialect;
+import io.xlate.edi.internal.stream.tokenization.EDIException;
 import io.xlate.edi.internal.stream.tokenization.Lexer;
 import io.xlate.edi.internal.stream.tokenization.ProxyEventHandler;
 import io.xlate.edi.schema.EDIReference;
@@ -61,6 +62,11 @@ public class StaEDIStreamReader implements EDIStreamReader, Configurable {
     private boolean closed = false;
     private boolean deprecationLogged = false;
 
+    @FunctionalInterface
+    interface EDIStreamReaderRunner {
+        void execute() throws IOException, EDIException;
+    }
+
     public StaEDIStreamReader(
             InputStream stream,
             Charset charset,
@@ -176,6 +182,15 @@ public class StaEDIStreamReader implements EDIStreamReader, Configurable {
         return Collections.unmodifiableMap(delimiters);
     }
 
+    void executeTask(EDIStreamReaderRunner runner, String errorMessage) throws EDIStreamException {
+        try {
+            runner.execute();
+        } catch (IOException e) {
+            Location where = getLocation();
+            throw new EDIStreamException(errorMessage, where, e);
+        }
+    }
+
     private EDIStreamEvent nextEvent() throws EDIStreamException {
         ensureOpen();
         ensureIncomplete();
@@ -198,13 +213,7 @@ public class StaEDIStreamReader implements EDIStreamReader, Configurable {
 
         if (!proxy.nextEvent()) {
             proxy.resetEvents();
-
-            try {
-                lexer.parse();
-            } catch (IOException e) {
-                Location where = getLocation();
-                throw new EDIStreamException("Error parsing input", where, e);
-            }
+            executeTask(lexer::parse, "Error parsing input");
         }
 
         final EDIStreamEvent event = proxy.getEvent();
@@ -212,7 +221,7 @@ public class StaEDIStreamReader implements EDIStreamReader, Configurable {
         LOGGER.finer(() -> "EDI event: " + event);
 
         if (event == EDIStreamEvent.END_INTERCHANGE) {
-            complete = true;
+            executeTask(() -> complete = !proxy.hasNext() && !lexer.hasRemaining(), "Error reading input");
         }
 
         if (event == EDIStreamEvent.ELEMENT_DATA && proxy.isBinaryElementLength()) {
diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/CharacterSet.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/CharacterSet.java
index fb38bdb..3a2aad0 100644
--- a/src/main/java/io/xlate/edi/internal/stream/tokenization/CharacterSet.java
+++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/CharacterSet.java
@@ -221,8 +221,24 @@ public class CharacterSet {
         }
     }
 
+    public boolean isWhitespace(int character) {
+        CharacterClass clazz = getClass(character);
+
+        switch (clazz) {
+        case SPACE:
+        case WHITESPACE:
+            return true;
+        default:
+            return ignore(clazz);
+        }
+    }
+
     public boolean isIgnored(int character) {
-        switch (getClass(character)) {
+        return ignore(getClass(character));
+    }
+
+    private boolean ignore(CharacterClass clazz) {
+        switch (clazz) {
         case CONTROL:
         case INVALID:
         case WHITESPACE:
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 455c1cb..d15fd53 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
@@ -156,6 +156,19 @@ public class Lexer {
         state = State.ELEMENT_DATA_BINARY;
     }
 
+    public boolean hasRemaining() throws IOException {
+        stream.mark(200);
+        int peekInput;
+        boolean spaceOnly = true;
+
+        while ((peekInput = stream.read()) > -1 && spaceOnly) {
+            spaceOnly = characters.isWhitespace(peekInput);
+        }
+
+        stream.reset();
+        return !spaceOnly;
+    }
+
     public void parse() throws IOException, EDIException {
         try {
             parse(this::readCharacterUnchecked);
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 5ce11cd..2d8fbcd 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
@@ -131,6 +131,11 @@ public class ProxyEventHandler implements EventHandler {
         return current(StreamEvent::getData, null);
     }
 
+    public boolean hasNext() {
+        // Current event is in the first position, second will be used for `nextEvent`
+        return eventQueue.size() > 1;
+    }
+
     public boolean nextEvent() {
         if (eventQueue.isEmpty()) {
             return false;


Test patch:
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 f72ee40..155c934 100644
--- a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java
@@ -2107,4 +2107,32 @@ class StaEDIStreamReaderTest implements ConstantsTest {
 
         assertEquals(expected, actual.toString());
     }
+
+    @Test
+    void testMultipleInterchangesInStream() throws EDIStreamException, IOException {
+        EDIInputFactory factory = EDIInputFactory.newFactory();
+        EDIStreamReader reader = factory.createEDIStreamReader(getClass().getResourceAsStream("/x12/simple997-multiple-interchanges.edi"));
+        int interchangeStart = 0;
+        int interchangeEnd = 0;
+
+        try {
+            while (reader.hasNext()) {
+                switch (reader.next()) {
+                case START_INTERCHANGE:
+                    interchangeStart++;
+                    break;
+                case END_INTERCHANGE:
+                    interchangeEnd++;
+                    break;
+                default:
+                    break;
+                }
+            }
+        } finally {
+            reader.close();
+        }
+
+        assertEquals(2, interchangeStart);
+        assertEquals(2, interchangeEnd);
+    }
 }
diff --git a/src/test/resources/x12/simple997-multiple-interchanges.edi b/src/test/resources/x12/simple997-multiple-interchanges.edi
new file mode 100644
index 0000000..df07c8e
--- /dev/null
+++ b/src/test/resources/x12/simple997-multiple-interchanges.edi
@@ -0,0 +1,24 @@
+ISA*00*          *00*          *ZZ*ReceiverID     *ZZ*Sender         *050812*1953*^*00501*508121953*0*P*:~
+GS*FA*ReceiverDept*SenderDept*20050812*195335*000005*X*005010X230~
+ST*997*0001~
+AK1*HC*000001~
+AK2*837*0021~
+AK3*NM1*8**8~
+AK4*8*66*7*MI~
+AK5*R*5~
+AK9*R*1*1*0~
+SE*8*0001~
+GE*1*000005~
+IEA*1*508121953~
+ISA*00*          *00*          *ZZ*ReceiverID     *ZZ*Sender         *050812*1953*^*00501*508121953*0*P*:~
+GS*FA*ReceiverDept*SenderDept*20050812*195335*000005*X*005010X230~
+ST*997*0001~
+AK1*HC*000001~
+AK2*837*0021~
+AK3*NM1*8**8~
+AK4*8*66*7*MI~
+AK5*R*5~
+AK9*R*1*1*0~
+SE*8*0001~
+GE*1*000005~
+IEA*1*508121953~