Repository: xlate/staedi
Pull request #423 (2023-12-26 15:07:37)

Refine location string representation and lexer exception setup

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=xlate_staedi&pullRequest=423) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=xlate_staedi&pullRequest=423&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=xlate_staedi&pullRequest=423&resolved=false&inNewCodePeriod=true)  
[100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=xlate_staedi&pullRequest=423&metric=new_coverage&view=list)  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=xlate_staedi&pullRequest=423&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=xlate_staedi&pullRequest=423)



Patch:
diff --git a/pom.xml b/pom.xml
index 5f81bb0..70566cd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,7 @@
     <version.jackson>2.16.1</version.jackson>
 
     <version.junit-jupiter>5.10.1</version.junit-jupiter>
+    <version.hamcrest>2.2</version.hamcrest>
     <version.xmlunit>2.9.1</version.xmlunit>
     <version.mockito-core>5.8.0</version.mockito-core>
     <version.jsonassert>1.5.1</version.jsonassert>
@@ -254,6 +255,11 @@
         <artifactId>xmlunit-matchers</artifactId>
         <version>${version.xmlunit}</version>
       </dependency>
+      <dependency>
+        <groupId>org.hamcrest</groupId>
+        <artifactId>hamcrest-core</artifactId>
+        <version>${version.hamcrest}</version>
+      </dependency>
       <dependency>
         <groupId>org.skyscreamer</groupId>
         <artifactId>jsonassert</artifactId>
@@ -300,6 +306,11 @@
       <artifactId>mockito-core</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.xmlunit</groupId>
       <artifactId>xmlunit-core</artifactId>
diff --git a/src/main/java/io/xlate/edi/internal/stream/LocationView.java b/src/main/java/io/xlate/edi/internal/stream/LocationView.java
index 1cf68d4..e1d35ba 100644
--- a/src/main/java/io/xlate/edi/internal/stream/LocationView.java
+++ b/src/main/java/io/xlate/edi/internal/stream/LocationView.java
@@ -17,7 +17,7 @@ package io.xlate.edi.internal.stream;
 
 import io.xlate.edi.stream.Location;
 
-public class LocationView implements Location {
+abstract class LocationView implements Location {
 
     protected int lineNumber;
     protected int columnNumber;
@@ -28,7 +28,7 @@ public class LocationView implements Location {
     protected int componentPosition;
     protected int elementOccurrence;
 
-    public LocationView(Location source) {
+    protected LocationView(Location source) {
         lineNumber = source.getLineNumber();
         columnNumber = source.getColumnNumber();
         characterOffset = source.getCharacterOffset();
@@ -49,14 +49,15 @@ public class LocationView implements Location {
         elementOccurrence = -1;
     }
 
-    public String toString() {
+    protected String toString(boolean afterSegment) {
         StringBuilder display = new StringBuilder();
 
         if (getSegmentPosition() < 0) {
             display.append("at offset ");
             display.append(getCharacterOffset());
         } else {
-            display.append("in segment ");
+            display.append(afterSegment ? "after " : "in ");
+            display.append("segment ");
             display.append(String.valueOf(getSegmentTag()));
             display.append(" at position ");
             display.append(String.valueOf(getSegmentPosition()));
@@ -120,9 +121,4 @@ public class LocationView implements Location {
     public int getElementOccurrence() {
         return elementOccurrence;
     }
-
-    @Override
-    public Location copy() {
-        return new LocationView(this);
-    }
 }
diff --git a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamLocation.java b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamLocation.java
index 198ab75..34a1852 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamLocation.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamLocation.java
@@ -19,6 +19,7 @@ import io.xlate.edi.stream.Location;
 
 public class StaEDIStreamLocation extends LocationView implements Location {
 
+    private boolean afterSegment = false;
     private boolean composite = false;
     private boolean repeating = false;
     private int repeatCount = -1;
@@ -31,16 +32,22 @@ public class StaEDIStreamLocation extends LocationView implements Location {
         super(source);
     }
 
+    @Override
+    public String toString() {
+        return super.toString(afterSegment);
+    }
+
     @Override
     public StaEDIStreamLocation copy() {
         StaEDIStreamLocation copy = new StaEDIStreamLocation(this);
+        copy.afterSegment = this.afterSegment;
         copy.composite = this.composite;
         copy.repeating = this.repeating;
         copy.repeatCount = this.repeatCount;
         return copy;
     }
 
-    public void set(Location source) {
+    public void set(StaEDIStreamLocation source) {
         lineNumber = source.getLineNumber();
         columnNumber = source.getColumnNumber();
         characterOffset = source.getCharacterOffset();
@@ -49,6 +56,7 @@ public class StaEDIStreamLocation extends LocationView implements Location {
         elementPosition = source.getElementPosition();
         componentPosition = source.getComponentPosition();
         elementOccurrence = source.getElementOccurrence();
+        afterSegment = source.afterSegment;
     }
 
     public void setElementPosition(int elementPosition) {
@@ -82,10 +90,11 @@ public class StaEDIStreamLocation extends LocationView implements Location {
     public void incrementSegmentPosition(String segmentTag) {
         this.segmentPosition = initOrIncrement(segmentPosition);
         this.segmentTag = segmentTag;
-        clearSegmentLocations();
+        clearSegmentLocations(false);
     }
 
-    public void clearSegmentLocations() {
+    public void clearSegmentLocations(boolean afterSegment) {
+        this.afterSegment = afterSegment;
         this.elementPosition = -1;
         this.elementOccurrence = -1;
         this.repeating = false;
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 39de8e4..5ce2c42 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java
@@ -548,7 +548,7 @@ public class StaEDIStreamWriter implements EDIStreamWriter, ElementDataHandler,
         }
 
         level = LEVEL_INTERCHANGE;
-        location.clearSegmentLocations();
+        location.clearSegmentLocations(true);
         transactionSchemaAllowed = false;
 
         return this;
diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/EDIException.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/EDIException.java
index 68a1be7..f1f74d4 100644
--- a/src/main/java/io/xlate/edi/internal/stream/tokenization/EDIException.java
+++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/EDIException.java
@@ -15,12 +15,12 @@
  ******************************************************************************/
 package io.xlate.edi.internal.stream.tokenization;
 
-import io.xlate.edi.stream.EDIStreamException;
-import io.xlate.edi.stream.Location;
-
 import java.util.HashMap;
 import java.util.Map;
 
+import io.xlate.edi.stream.EDIStreamException;
+import io.xlate.edi.stream.Location;
+
 public class EDIException extends EDIStreamException {
 
     private static final long serialVersionUID = -2724168743697298348L;
@@ -41,7 +41,7 @@ public class EDIException extends EDIStreamException {
         exceptionMessages.put(INVALID_STATE,
                               "EDIE003 - Invalid processing state");
         exceptionMessages.put(INVALID_CHARACTER,
-                              "EDIE004 - Invalid input character");
+                              "EDIE004 - Invalid character");
         exceptionMessages.put(INCOMPLETE_STREAM,
                               "EDIE005 - Unexpected end of stream");
     }
@@ -55,7 +55,7 @@ public class EDIException extends EDIStreamException {
     }
 
     EDIException(Integer id, String message, Location location) {
-        super(exceptionMessages.get(id) + "; " + message, location);
+        super(buildMessage(exceptionMessages.get(id), location) + "; " + message, location);
     }
 
     public EDIException(Integer id, String message) {
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 19dbe06..e530bca 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
@@ -30,9 +30,7 @@ import java.util.function.IntSupplier;
 import java.util.logging.Logger;
 
 import io.xlate.edi.internal.stream.CharArraySequence;
-import io.xlate.edi.internal.stream.LocationView;
 import io.xlate.edi.internal.stream.StaEDIStreamLocation;
-import io.xlate.edi.stream.Location;
 
 public class Lexer {
 
@@ -45,8 +43,8 @@ public class Lexer {
     }
 
     private final Deque<Mode> modes = new ArrayDeque<>();
-    private int input = 0;
     private State state = State.INITIAL;
+    private int previousInput = 0;
     private State previous;
 
     private interface Notifier {
@@ -182,125 +180,16 @@ public class Lexer {
             return;
         }
 
-        if (state == State.INVALID) {
+        if (state.isInvalid()) {
             // Unable to proceed once the state becomes invalid
-            throw invalidStateError();
+            throw invalidStateError(previousInput, state, previous);
         }
 
+        int input = 0;
         boolean eventsReady = false;
 
         while (!eventsReady && (input = inputSource.getAsInt()) > -1) {
-            location.incrementOffset(input);
-
-            CharacterClass clazz = characters.getClass(input);
-            previous = state;
-            state = State.transition(state, dialect, clazz);
-            LOGGER.finer(() -> String.format("%s + (%s, '%s', %s) -> %s", previous, Dialect.getStandard(dialect), (char) input, clazz, state));
-
-            switch (state) {
-            case INITIAL:
-            case TAG_SEARCH:
-            case HEADER_EDIFACT_UNB_SEARCH:
-                break;
-            case HEADER_X12_I:
-            case HEADER_X12_S:
-            case HEADER_EDIFACT_N:
-            case HEADER_EDIFACT_U:
-            case HEADER_TRADACOMS_S:
-            case HEADER_TRADACOMS_T:
-            case TAG_1:
-            case TAG_2:
-            case TAG_3:
-            case TRAILER_X12_I:
-            case TRAILER_X12_E:
-            case TRAILER_X12_A:
-            case TRAILER_EDIFACT_U:
-            case TRAILER_EDIFACT_N:
-            case TRAILER_EDIFACT_Z:
-            case TRAILER_TRADACOMS_E:
-            case TRAILER_TRADACOMS_N:
-            case TRAILER_TRADACOMS_D:
-            case ELEMENT_DATA:
-            case TRAILER_ELEMENT_DATA:
-                buffer.put((char) input);
-                break;
-            case ELEMENT_INVALID_DATA:
-                if (!characters.isIgnored(input)) {
-                    buffer.put((char) input);
-                }
-                break;
-            case HEADER_EDIFACT_UNB_1: // U - When UNA is present
-            case HEADER_EDIFACT_UNB_2: // N - When UNA is present
-            case HEADER_EDIFACT_UNB_3: // B - When UNA is present
-                handleStateHeaderTag(input);
-                break;
-            case HEADER_RELEASE:
-            case DATA_RELEASE:
-                // Skip this character - next character will be literal value
-                break;
-            case ELEMENT_DATA_BINARY:
-                handleStateElementDataBinary();
-                break;
-            case INTERCHANGE_CANDIDATE:
-                // ISA, UNA, or UNB was found
-                handleStateInterchangeCandidate(input);
-                break;
-            case HEADER_DATA:
-            case HEADER_INVALID_DATA:
-                handleStateHeaderData((char) input);
-                eventsReady = dialectConfirmed(State.TAG_SEARCH);
-                break;
-            case HEADER_SEGMENT_BEGIN:
-                dialect.appendHeader(characters, (char) input);
-                openSegment();
-                eventsReady = dialectConfirmed(State.ELEMENT_END);
-                break;
-            case HEADER_ELEMENT_END:
-                dialect.appendHeader(characters, (char) input);
-                handleElement();
-                eventsReady = dialectConfirmed(State.ELEMENT_END);
-                break;
-            case HEADER_COMPONENT_END:
-                dialect.appendHeader(characters, (char) input);
-                handleComponent();
-                eventsReady = dialectConfirmed(State.COMPONENT_END);
-                break;
-            case SEGMENT_BEGIN:
-            case TRAILER_BEGIN:
-                openSegment();
-                eventsReady = nextEvent();
-                break;
-            case SEGMENT_END:
-                closeSegment();
-                eventsReady = nextEvent();
-                break;
-            case SEGMENT_EMPTY:
-                emptySegment();
-                eventsReady = nextEvent();
-                break;
-            case COMPONENT_END:
-                handleComponent();
-                eventsReady = nextEvent();
-                break;
-            case ELEMENT_END:
-            case TRAILER_ELEMENT_END:
-            case ELEMENT_REPEAT:
-                handleElement();
-                eventsReady = nextEvent();
-                break;
-            case INTERCHANGE_END:
-                closeInterchange();
-                eventsReady = nextEvent();
-                break;
-            default:
-                if (characters.isIgnored(input)) {
-                    state = previous;
-                } else if (clazz != CharacterClass.INVALID) {
-                    throw invalidStateError();
-                } else {
-                    throw error(EDIException.INVALID_CHARACTER);
-                }
-            }
+            eventsReady = processInputCharacter(input);
         }
 
         if (input < 0) {
@@ -308,6 +197,125 @@ public class Lexer {
         }
     }
 
+    boolean processInputCharacter(int input) throws EDIException {
+        boolean eventsReady = false;
+        location.incrementOffset(input);
+
+        CharacterClass clazz = characters.getClass(input);
+        previous = state;
+        previousInput = input;
+
+        state = State.transition(state, dialect, clazz);
+        LOGGER.finer(() -> String.format("%s + (%s, '%s', %s) -> %s", previous, Dialect.getStandard(dialect), (char) input, clazz, state));
+
+        switch (state) {
+        case INITIAL:
+        case TAG_SEARCH:
+        case HEADER_EDIFACT_UNB_SEARCH:
+            break;
+        case HEADER_X12_I:
+        case HEADER_X12_S:
+        case HEADER_EDIFACT_N:
+        case HEADER_EDIFACT_U:
+        case HEADER_TRADACOMS_S:
+        case HEADER_TRADACOMS_T:
+        case TAG_1:
+        case TAG_2:
+        case TAG_3:
+        case TRAILER_X12_I:
+        case TRAILER_X12_E:
+        case TRAILER_X12_A:
+        case TRAILER_EDIFACT_U:
+        case TRAILER_EDIFACT_N:
+        case TRAILER_EDIFACT_Z:
+        case TRAILER_TRADACOMS_E:
+        case TRAILER_TRADACOMS_N:
+        case TRAILER_TRADACOMS_D:
+        case ELEMENT_DATA:
+        case TRAILER_ELEMENT_DATA:
+            buffer.put((char) input);
+            break;
+        case ELEMENT_INVALID_DATA:
+            if (!characters.isIgnored(input)) {
+                buffer.put((char) input);
+            }
+            break;
+        case HEADER_EDIFACT_UNB_1: // U - When UNA is present
+        case HEADER_EDIFACT_UNB_2: // N - When UNA is present
+        case HEADER_EDIFACT_UNB_3: // B - When UNA is present
+            handleStateHeaderTag(input);
+            break;
+        case HEADER_RELEASE:
+        case DATA_RELEASE:
+            // Skip this character - next character will be literal value
+            break;
+        case ELEMENT_DATA_BINARY:
+            handleStateElementDataBinary();
+            break;
+        case INTERCHANGE_CANDIDATE:
+            // ISA, UNA, or UNB was found
+            handleStateInterchangeCandidate(input);
+            break;
+        case HEADER_DATA:
+        case HEADER_INVALID_DATA:
+            handleStateHeaderData((char) input);
+            eventsReady = dialectConfirmed(State.TAG_SEARCH);
+            break;
+        case HEADER_SEGMENT_BEGIN:
+            dialect.appendHeader(characters, (char) input);
+            openSegment();
+            eventsReady = dialectConfirmed(State.ELEMENT_END);
+            break;
+        case HEADER_ELEMENT_END:
+            dialect.appendHeader(characters, (char) input);
+            handleElement();
+            eventsReady = dialectConfirmed(State.ELEMENT_END);
+            break;
+        case HEADER_COMPONENT_END:
+            dialect.appendHeader(characters, (char) input);
+            handleComponent();
+            eventsReady = dialectConfirmed(State.COMPONENT_END);
+            break;
+        case SEGMENT_BEGIN:
+        case TRAILER_BEGIN:
+            openSegment();
+            eventsReady = nextEvent();
+            break;
+        case SEGMENT_END:
+            closeSegment();
+            eventsReady = nextEvent();
+            break;
+        case SEGMENT_EMPTY:
+            emptySegment();
+            eventsReady = nextEvent();
+            break;
+        case COMPONENT_END:
+            handleComponent();
+            eventsReady = nextEvent();
+            break;
+        case ELEMENT_END:
+        case TRAILER_ELEMENT_END:
+        case ELEMENT_REPEAT:
+            handleElement();
+            eventsReady = nextEvent();
+            break;
+        case INTERCHANGE_END:
+            closeInterchange();
+            eventsReady = nextEvent();
+            break;
+        default:
+            if (characters.isIgnored(input)) {
+                state = previous;
+            } else if (clazz != CharacterClass.INVALID) {
+                throw invalidStateError(input, this.state, this.previous);
+            } else {
+                throw error(EDIException.INVALID_CHARACTER);
+            }
+        }
+
+        return eventsReady;
+    }
+
     int readCharacterUnchecked() {
         try {
             return readCharacter();
@@ -445,11 +453,11 @@ public class Lexer {
         return false;
     }
 
-    private EDIException invalidStateError() {
+    private EDIException invalidStateError(int input, State state, State previousState) {
         StringBuilder message = new StringBuilder();
         message.append(state);
         message.append(" (previous: ");
-        message.append(previous);
+        message.append(previousState);
         message.append("); input: '");
         message.append((char) input);
         message.append('\'');
@@ -457,13 +465,11 @@ public class Lexer {
     }
 
     private EDIException error(int code, CharSequence message) {
-        Location where = new LocationView(location);
-        return new EDIException(code, message.toString(), where);
+        return new EDIException(code, message.toString(), location.copy());
     }
 
     private EDIException error(int code) {
-        Location where = new LocationView(location);
-        return new EDIException(code, where);
+        return new EDIException(code, location.copy());
     }
 
     private boolean nextEvent() {
@@ -476,10 +482,10 @@ public class Lexer {
             int start = startQueue.remove();
             int length = lengthQueue.remove();
             eventsReady = event.execute(nextState, start, length);
-        }
 
-        if (events.isEmpty()) {
-            buffer.clear();
+            if (events.isEmpty()) {
+                buffer.clear();
+            }
         }
 
         return eventsReady;
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 0238d55..2fad475 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
@@ -35,7 +35,6 @@ import io.xlate.edi.schema.EDIType;
 import io.xlate.edi.schema.Schema;
 import io.xlate.edi.stream.EDIStreamEvent;
 import io.xlate.edi.stream.EDIStreamValidationError;
-import io.xlate.edi.stream.Location;
 
 public class ProxyEventHandler implements EventHandler {
 
@@ -153,7 +152,7 @@ public class ProxyEventHandler implements EventHandler {
         return current(StreamEvent::getReferenceCode, null);
     }
 
-    public Location getLocation() {
+    public StaEDIStreamLocation getLocation() {
         return current(StreamEvent::getLocation, this.location);
     }
 
@@ -317,7 +316,7 @@ public class ProxyEventHandler implements EventHandler {
             performLevelCheck();
         }
 
-        location.clearSegmentLocations();
+        location.clearSegmentLocations(true);
         enqueueEvent(EDIStreamEvent.END_SEGMENT, EDIStreamValidationError.NONE, segmentTag, typeReference, location);
         return true;
     }
@@ -495,7 +494,7 @@ public class ProxyEventHandler implements EventHandler {
         while (!openLevels.isEmpty() && !openLevels.getLast().isParentOf(parentId)) {
             HierarchicalLevel completed = openLevels.removeLast();
             completed.event.location.set(location);
-            completed.event.location.clearSegmentLocations();
+            completed.event.location.clearSegmentLocations(true);
 
             eventQueue.add(eventQueue.indexOf(successor), completed.event);
         }
@@ -593,7 +592,7 @@ public class ProxyEventHandler implements EventHandler {
                               EDIStreamValidationError error,
                               CharSequence data,
                               EDIReference typeReference,
-                              Location location) {
+                              StaEDIStreamLocation location) {
 
         StreamEvent target = getPooledEvent();
 
diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/State.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/State.java
index 877651d..f7e393f 100644
--- a/src/main/java/io/xlate/edi/internal/stream/tokenization/State.java
+++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/State.java
@@ -357,6 +357,10 @@ public enum State {
         return TRANSITIONS[dialect][code][clazz.code];
     }
 
+    public boolean isInvalid() {
+        return Category.INVALID == code;
+    }
+
     public boolean isHeaderState() {
         return Category.HEADER == code && DialectCode.UNKNOWN != table;
     }
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 812b7fb..1283b6b 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
@@ -8,7 +8,6 @@ import io.xlate.edi.schema.EDIReference;
 import io.xlate.edi.schema.implementation.EDITypeImplementation;
 import io.xlate.edi.stream.EDIStreamEvent;
 import io.xlate.edi.stream.EDIStreamValidationError;
-import io.xlate.edi.stream.Location;
 
 public class StreamEvent {
 
@@ -70,11 +69,11 @@ public class StreamEvent {
         this.typeReference = typeReference;
     }
 
-    public Location getLocation() {
+    public StaEDIStreamLocation getLocation() {
         return location;
     }
 
-    public void setLocation(Location location) {
+    public void setLocation(StaEDIStreamLocation location) {
         if (this.location == null) {
             this.location = new StaEDIStreamLocation(location);
         }
diff --git a/src/main/java/io/xlate/edi/stream/EDIStreamException.java b/src/main/java/io/xlate/edi/stream/EDIStreamException.java
index c1ae917..d4ea71b 100644
--- a/src/main/java/io/xlate/edi/stream/EDIStreamException.java
+++ b/src/main/java/io/xlate/edi/stream/EDIStreamException.java
@@ -21,6 +21,14 @@ public class EDIStreamException extends Exception {
 
     protected final transient Location location;
 
+    protected static String buildMessage(String message, Location location) {
+        String locationString = location.toString();
+        if (message.contains(locationString)) {
+            return message;
+        }
+        return message + " " + locationString;
+    }
+
     /**
      * Construct an exception with the associated message.
      *
@@ -58,7 +66,7 @@ public class EDIStreamException extends Exception {
      *            a nested error / exception
      */
     public EDIStreamException(String message, Location location, Throwable cause) {
-        super(message + " " + location.toString(), cause);
+        super(buildMessage(message, location), cause);
         this.location = location;
     }
 
@@ -72,7 +80,7 @@ public class EDIStreamException extends Exception {
      *            the location of the error
      */
     public EDIStreamException(String message, Location location) {
-        super(message + " " + location.toString());
+        super(buildMessage(message, location));
         this.location = location;
     }
 


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 a766f49..d99ee7e 100644
--- a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java
@@ -17,6 +17,8 @@ package io.xlate.edi.internal.stream;
 
 import static io.xlate.edi.test.StaEDITestUtil.assertLocation;
 import static io.xlate.edi.test.StaEDITestUtil.assertTextLocation;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -49,6 +51,8 @@ import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -1036,6 +1040,12 @@ class StaEDIStreamReaderTest implements ConstantsTest {
                 s += s == -1 ? 2 : 1;
                 assertLocation(reader, s, -1, -1, -1);
                 tag = reader.getText();
+                String startSegmentLocation = reader.getLocation().toString();
+                assertThat(startSegmentLocation, containsString("in segment " + tag));
+                break;
+            case END_SEGMENT:
+                String endSegmentLocation = reader.getLocation().toString();
+                assertThat(endSegmentLocation, containsString("after segment " + tag));
                 break;
             case ELEMENT_DATA:
                 Location l = reader.getLocation();