Repository: xlate/staedi (java)

Issue:
Allow retrieval of EDI standard name from EDIStreamWriter
Allow retrieval of EDI standard name and version from `EDIStreamWriter`. Method to match the same retrieval method from `EDIStreamReader`.

String getStandard()

Pull request description:
Add `getStandard` method to `EDIStreamWriter`
Fixes #25 

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3fea8a0..ac675bb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -66,13 +66,16 @@ jobs:
         run: mvn -B verify javadoc:javadoc --file pom.xml
 
   quality:
-    needs: [build, javadoc]
     if: github.event_name == 'push' && github.repository == 'xlate/staedi'
     runs-on: ubuntu-latest
     name: Verify Quality
 
     steps:
       - uses: actions/checkout@v2
+        with:
+          # Disabling shallow clone is recommended for improving relevancy of reporting
+          fetch-depth: 0
+
       - uses: actions/setup-java@v1.3.0
         with:
           java-version: 11
diff --git a/pom.xml b/pom.xml
index 7fff799..44f0535 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
 
   <groupId>io.xlate</groupId>
   <artifactId>staedi</artifactId>
-  <version>1.6.1-SNAPSHOT</version>
+  <version>1.7.0-SNAPSHOT</version>
 
   <name>StAEDI : Streaming API for EDI for Java</name>
   <description>Streaming API for EDI for Java</description>
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 ea7c056..762ac36 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java
@@ -236,7 +236,7 @@ public class StaEDIStreamReader implements EDIStreamReader {
 
     @Override
     public String getStandard() {
-        if (lexer.getDialect() == null || lexer.getDialect().getStandard() == null) {
+        if (lexer.getDialect() == null) {
             throw new IllegalStateException("standard not accessible");
         }
 
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 b0bb312..2e836b7 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java
@@ -132,7 +132,7 @@ public class StaEDIStreamWriter implements EDIStreamWriter, ElementDataHandler,
     }
 
     private static void ensureArgs(int arrayLength, int start, int end) {
-        if (start < 0 || start > arrayLength || end > arrayLength) {
+        if (start < 0 || start >= arrayLength || end > arrayLength) {
             throw new IndexOutOfBoundsException();
         }
 
@@ -205,6 +205,15 @@ public class StaEDIStreamWriter implements EDIStreamWriter, ElementDataHandler,
         return location;
     }
 
+    @Override
+    public String getStandard() {
+        if (dialect == null) {
+            throw new IllegalStateException("standard not accessible");
+        }
+
+        return dialect.getStandard();
+    }
+
     private Validator validator() {
         // Do not use the transactionValidator in the period where it may be set/mutated by the user
         return transaction && !transactionSchemaAllowed ? transactionValidator : controlValidator;
@@ -216,10 +225,7 @@ public class StaEDIStreamWriter implements EDIStreamWriter, ElementDataHandler,
         clazz = characters.getClass(output);
 
         if (clazz == CharacterClass.INVALID) {
-            StringBuilder message = new StringBuilder();
-            message.append("Invalid character: 0x");
-            message.append(Integer.toHexString(output));
-            throw new EDIException(message.toString());
+            throw new EDIStreamException(String.format("Invalid character: 0x%04X", output), location);
         }
 
         state = state.transition(clazz);
@@ -251,7 +257,7 @@ public class StaEDIStreamWriter implements EDIStreamWriter, ElementDataHandler,
                     }
                 }
             } else {
-                throw new EDIException("Unexpected header character: '" + (char) output + "'");
+                throw new EDIStreamException(String.format("Unexpected header character: 0x%04X [%s]", output, (char) output), location);
             }
             break;
         case INVALID:
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 0927258..0d16ce6 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java
@@ -226,11 +226,6 @@ final class StaEDIXMLStreamReader implements XMLStreamReader {
                                          this.location);
 
         case ELEMENT_OCCURRENCE_ERROR:
-            throw new XMLStreamException(String.format("Element %s has error %s",
-                                                       ediReader.getText(),
-                                                       ediReader.getErrorType()),
-                                         this.location);
-
         case ELEMENT_DATA_ERROR:
             throw new XMLStreamException(String.format("Element %s has error %s",
                                                        ediReader.getText(),
diff --git a/src/main/java/io/xlate/edi/stream/EDIStreamException.java b/src/main/java/io/xlate/edi/stream/EDIStreamException.java
index 6fbbc7f..1b46b66 100644
--- a/src/main/java/io/xlate/edi/stream/EDIStreamException.java
+++ b/src/main/java/io/xlate/edi/stream/EDIStreamException.java
@@ -65,8 +65,8 @@ public class EDIStreamException extends Exception {
                 display.append(", element ");
                 display.append(String.valueOf(location.getElementPosition()));
 
-                if (location.getElementOccurrence() > -1) {
-                    display.append("(occurrence ");
+                if (location.getElementOccurrence() > 1) {
+                    display.append(" (occurrence ");
                     display.append(String.valueOf(location.getElementOccurrence()));
                     display.append(')');
                 }
diff --git a/src/main/java/io/xlate/edi/stream/EDIStreamWriter.java b/src/main/java/io/xlate/edi/stream/EDIStreamWriter.java
index 2336c2e..6d33760 100644
--- a/src/main/java/io/xlate/edi/stream/EDIStreamWriter.java
+++ b/src/main/java/io/xlate/edi/stream/EDIStreamWriter.java
@@ -59,7 +59,8 @@ public interface EDIStreamWriter {
      * Calls to this method are only valid before the interchange is started.
      *
      * @param controlSchema
-     *             the schema instance to use for validation of control structures
+     *            the schema instance to use for validation of control
+     *            structures
      * @throws IllegalStateException
      *             when the writer is not in its initial state
      *
@@ -71,25 +72,27 @@ public interface EDIStreamWriter {
      * <p>
      * Sets the schema to be used for validation of the business transaction for
      * this stream writer. This schema will be used to validate only the
-     * contents of a transaction/message, <em>not including</em> the begin/end control
-     * structures.
+     * contents of a transaction/message, <em>not including</em> the begin/end
+     * control structures.
      * <p>
-     * This method may be called at any time. However, when non-null, the writer will make
-     * use of the transaction schema for output validation. It is the responsibility of the
-     * caller to set the transaction schema to null at the end of the business transaction.
+     * This method may be called at any time. However, when non-null, the writer
+     * will make use of the transaction schema for output validation. It is the
+     * responsibility of the caller to set the transaction schema to null at the
+     * end of the business transaction.
      *
      * @param transactionSchema
-     *             the schema instance to use for validation of business transaction structures
+     *            the schema instance to use for validation of business
+     *            transaction structures
      *
      * @since 1.1
      */
     void setTransactionSchema(Schema transactionSchema);
 
     /**
-     * Return the current location of the writer. If the Location is unknown
-     * the processor should return an implementation of Location that returns -1
-     * for the location values. The location information is only valid until
-     * the next item is written to the output.
+     * Return the current location of the writer. If the Location is unknown the
+     * processor should return an implementation of Location that returns -1 for
+     * the location values. The location information is only valid until the
+     * next item is written to the output.
      *
      * @return current location of the writer
      *
@@ -97,6 +100,20 @@ public interface EDIStreamWriter {
      */
     Location getLocation();
 
+    /**
+     * Get the EDI standard name. Calls to this method are only valid when the
+     * interchange type has been determined, after the full interchange header
+     * segment has been written.
+     *
+     * @return the name of the EDI standard
+     * @throws IllegalStateException
+     *             when the standard has not yet been determined, prior to the
+     *             start of an interchange header segment being fully written
+     *
+     * @since 1.7
+     */
+    String getStandard();
+
     EDIStreamWriter startInterchange() throws EDIStreamException;
 
     EDIStreamWriter endInterchange() throws EDIStreamException;


Test patch:
diff --git a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamWriterTest.java b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamWriterTest.java
index 783ca1b..402913e 100644
--- a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamWriterTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamWriterTest.java
@@ -17,6 +17,7 @@ package io.xlate.edi.internal.stream;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.fail;
 
@@ -31,6 +32,7 @@ import java.nio.ByteBuffer;
 import java.util.Map;
 
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 import io.xlate.edi.internal.schema.SchemaUtils;
 import io.xlate.edi.schema.EDISchemaException;
@@ -45,6 +47,7 @@ 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;
 
 @SuppressWarnings("resource")
 class StaEDIStreamWriterTest {
@@ -158,6 +161,28 @@ class StaEDIStreamWriterTest {
         assertEquals("ISA", stream.toString());
     }
 
+    @Test
+    void testWriteInvalidHeaderElement() throws EDIStreamException {
+        EDIOutputFactory factory = EDIOutputFactory.newFactory();
+        OutputStream stream = new ByteArrayOutputStream(4096);
+        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
+        writer.startInterchange();
+        writer.writeStartSegment("ISA");
+        writer.writeElement("00").writeElement("           "); // Too long
+        writer.writeElement("00").writeElement("           "); // Too long
+        writer.writeElement("ZZ").writeElement("ReceiverID     ");
+        writer.writeElement("ZZ").writeElement("Sender         ");
+        writer.writeElement("050812");
+        writer.writeElement("1953");
+        writer.writeElement("^");
+        writer.writeElement("00501");
+        writer.writeElement("508121953");
+        writer.writeElement("0");
+        writer.writeElement("P");
+        EDIStreamException thrown = assertThrows(EDIStreamException.class, () -> writer.writeElement(":"));
+        assertEquals("Unexpected header character: 0x002A [*] in segment ISA at position 1, element 15", thrown.getMessage());
+    }
+
     @Test
     void testWriteStartSegmentIllegal() throws EDIStreamException {
         EDIOutputFactory factory = EDIOutputFactory.newFactory();
@@ -220,6 +245,55 @@ class StaEDIStreamWriterTest {
         assertThrows(IllegalStateException.class, () -> writer.writeStartElement());
     }
 
+    @Test
+    void testWriteInvalidCharacter() throws EDIStreamException {
+        EDIOutputFactory factory = EDIOutputFactory.newFactory();
+        OutputStream stream = new ByteArrayOutputStream(4096);
+        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
+        writer.startInterchange();
+        writer.writeStartSegment("ISA");
+        writer.writeStartElement();
+        EDIStreamException thrown = assertThrows(EDIStreamException.class,
+                                                 () -> writer.writeElementData("\u0008\u0010"));
+        assertEquals("Invalid character: 0x0008 in segment ISA at position 1, element 1", thrown.getMessage());
+    }
+
+    @Test
+    void testWriteInvalidCharacterRepeatedComposite() throws EDIStreamException {
+        EDIOutputFactory factory = EDIOutputFactory.newFactory();
+        OutputStream stream = new ByteArrayOutputStream(4096);
+        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
+        writer.startInterchange();
+        writeHeader(writer);
+        writer.writeStartSegment("FOO");
+        writer.writeElement("BAR1");
+        writer.writeRepeatElement(); // starts new element
+        writer.writeComponent("BAR2");
+        writer.writeComponent("BAR3");
+        EDIStreamException thrown = assertThrows(EDIStreamException.class,
+                                                 () -> writer.writeComponent("\u0008\u0010"));
+        assertEquals("Invalid character: 0x0008 in segment FOO at position 2, element 1 (occurrence 2), component 3", thrown.getMessage());
+        Location l = thrown.getLocation();
+        assertEquals("FOO", l.getSegmentTag());
+        assertEquals(2, l.getSegmentPosition());
+        assertEquals(1, l.getElementPosition());
+        assertEquals(2, l.getElementOccurrence());
+        assertEquals(3, l.getComponentPosition());
+    }
+
+    @Test
+    void testWriteInvalidSegmentTag() throws EDIStreamException {
+        EDIOutputFactory factory = EDIOutputFactory.newFactory();
+        OutputStream stream = new ByteArrayOutputStream(4096);
+        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
+        writer.startInterchange();
+        writeHeader(writer);
+        writer.writeStartSegment("G");
+        EDIStreamException thrown = assertThrows(EDIStreamException.class,
+                                                 () -> writer.writeElement("FOO"));
+        assertEquals("Invalid state: INVALID; output 0x002A", thrown.getMessage());
+    }
+
     @Test
     void testWriteStartElementBinary() throws IllegalStateException, EDIStreamException {
         EDIOutputFactory factory = EDIOutputFactory.newFactory();
@@ -357,6 +431,21 @@ class StaEDIStreamWriterTest {
         assertEquals("ISA*CHARS~", stream.toString());
     }
 
+    @Test
+    void testWriteElementDataCharInvalidBoundaries() throws EDIStreamException {
+        EDIOutputFactory factory = EDIOutputFactory.newFactory();
+        OutputStream stream = new ByteArrayOutputStream(4096);
+        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
+        writer.startInterchange();
+        writeHeader(writer);
+        writer.writeStartSegment("GS");
+        writer.writeStartElement();
+        assertThrows(IndexOutOfBoundsException.class, () -> writer.writeElementData(new char[] { 'F', 'A' }, -1, 2));
+        assertThrows(IndexOutOfBoundsException.class, () -> writer.writeElementData(new char[] { 'F', 'A' }, 2, 1));
+        assertThrows(IndexOutOfBoundsException.class, () -> writer.writeElementData(new char[] { 'F', 'A' }, 0, 3));
+        assertThrows(IllegalArgumentException.class, () -> writer.writeElementData(new char[] { 'F', 'A' }, 0, -1));
+    }
+
     @Test
     void testWriteElementDataCharArrayIllegal() throws EDIStreamException {
         EDIOutputFactory factory = EDIOutputFactory.newFactory();
@@ -390,6 +479,29 @@ class StaEDIStreamWriterTest {
         assertEquals("BIN*8*\n\u0000\u0001\u0002\u0003\u0004\u0005\t~", stream.toString());
     }
 
+    @Test
+    void testWriteBinaryDataInputStreamIOException() throws Exception {
+        EDIOutputFactory factory = EDIOutputFactory.newFactory();
+        ByteArrayOutputStream stream = new ByteArrayOutputStream(4096);
+        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
+        InputStream binaryStream = Mockito.mock(InputStream.class);
+        IOException ioException = new IOException();
+        Mockito.when(binaryStream.read()).thenThrow(ioException);
+        writer.startInterchange();
+        writeHeader(writer);
+        stream.reset();
+        writer.writeStartSegment("BIN");
+        writer.writeStartElement();
+        writer.writeElementData("4");
+        writer.endElement();
+        writer.writeStartElementBinary();
+        EDIStreamException thrown = assertThrows(EDIStreamException.class,
+                                                 () -> writer.writeBinaryData(binaryStream));
+        assertEquals("Exception writing binary element data in segment BIN at position 2, element 2",
+                     thrown.getMessage());
+        assertSame(ioException, thrown.getCause());
+    }
+
     @Test
     void testWriteBinaryDataByteArray() throws EDIStreamException {
         EDIOutputFactory factory = EDIOutputFactory.newFactory();
@@ -1055,14 +1167,18 @@ class StaEDIStreamWriterTest {
                     break;
                 }
 
-                assertEquals(reader.getLocation().getSegmentPosition(), writer.getLocation().getSegmentPosition(), () ->
-                        "Segment position mismatch");
-                assertEquals(reader.getLocation().getElementPosition(), writer.getLocation().getElementPosition(), () ->
-                        "Element position mismatch");
-                assertEquals(reader.getLocation().getElementOccurrence(), writer.getLocation().getElementOccurrence(), () ->
-                        "Element occurrence mismatch");
-                assertEquals(reader.getLocation().getComponentPosition(), writer.getLocation().getComponentPosition(), () ->
-                        "Component position mismatch");
+                assertEquals(reader.getLocation().getSegmentPosition(),
+                             writer.getLocation().getSegmentPosition(),
+                             () -> "Segment position mismatch");
+                assertEquals(reader.getLocation().getElementPosition(),
+                             writer.getLocation().getElementPosition(),
+                             () -> "Element position mismatch");
+                assertEquals(reader.getLocation().getElementOccurrence(),
+                             writer.getLocation().getElementOccurrence(),
+                             () -> "Element occurrence mismatch");
+                assertEquals(reader.getLocation().getComponentPosition(),
+                             writer.getLocation().getComponentPosition(),
+                             () -> "Component position mismatch");
             }
         } finally {
             reader.close();
@@ -1070,4 +1186,23 @@ class StaEDIStreamWriterTest {
 
         assertEquals(expected.toString().trim(), result.toString().trim());
     }
+
+    @Test
+    void testGetStandardNullDialect() {
+        EDIOutputFactory factory = EDIOutputFactory.newFactory();
+        OutputStream stream = new ByteArrayOutputStream(1);
+        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
+        IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> writer.getStandard());
+        assertEquals("standard not accessible", thrown.getMessage());
+    }
+
+    @Test
+    void testGetStandardX12() throws EDIStreamException {
+        EDIOutputFactory factory = EDIOutputFactory.newFactory();
+        OutputStream stream = new ByteArrayOutputStream(1);
+        EDIStreamWriter writer = factory.createEDIStreamWriter(stream);
+        writer.startInterchange();
+        writer.writeStartSegment("ISA");
+        assertEquals(EDIStreamConstants.Standards.X12, writer.getStandard());
+    }
 }