Repository: xlate/staedi (java)

Issue:
EDIFACT Syntax Rule D7 (If first, then none of the others)
### If first, then none of the others: `D7(030,040,050)`
If the first item (at position 03) is present, then the items at 04 and 05 cannot be included.

Pull request:
Support `firstonly` syntax validation (EDIFACT D7)
Fixes #102 

- Add `copy` method to `Location` interface
- Re-factor common code from syntax validation classes
- Additional schema test coverage

Patch:
diff --git a/pom.xml b/pom.xml
index c20c672..31e0721 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
 
   <groupId>io.xlate</groupId>
   <artifactId>staedi</artifactId>
-  <version>1.10.3-SNAPSHOT</version>
+  <version>1.11.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/LocationView.java b/src/main/java/io/xlate/edi/internal/stream/LocationView.java
index 4abc5ce..1cf68d4 100644
--- a/src/main/java/io/xlate/edi/internal/stream/LocationView.java
+++ b/src/main/java/io/xlate/edi/internal/stream/LocationView.java
@@ -35,14 +35,14 @@ public class LocationView implements Location {
         segmentPosition = source.getSegmentPosition();
         segmentTag = source.getSegmentTag();
         elementPosition = source.getElementPosition();
-        componentPosition = source.getComponentPosition();
         elementOccurrence = source.getElementOccurrence();
+        componentPosition = source.getComponentPosition();
     }
 
     protected LocationView() {
         lineNumber = 1;
         columnNumber = 0;
-        characterOffset = -1;
+        characterOffset = 0;
         segmentPosition = -1;
         elementPosition = -1;
         componentPosition = -1;
@@ -120,4 +120,9 @@ 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 d7a4fd4..5aa686b 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamLocation.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamLocation.java
@@ -29,16 +29,9 @@ public class StaEDIStreamLocation extends LocationView implements Location {
         super(source);
     }
 
+    @Override
     public StaEDIStreamLocation copy() {
-        StaEDIStreamLocation copy = new StaEDIStreamLocation();
-        copy.lineNumber = this.lineNumber;
-        copy.columnNumber = this.columnNumber;
-        copy.characterOffset = this.characterOffset;
-        copy.segmentPosition = this.segmentPosition;
-        copy.segmentTag = this.segmentTag;
-        copy.elementPosition = this.elementPosition;
-        copy.elementOccurrence = this.elementOccurrence;
-        copy.componentPosition = this.componentPosition;
+        StaEDIStreamLocation copy = new StaEDIStreamLocation(this);
         copy.repeated = this.repeated;
         return copy;
     }
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/ConditionSyntaxValidator.java b/src/main/java/io/xlate/edi/internal/stream/validation/ConditionSyntaxValidator.java
index b5449d0..7c4fe8f 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/ConditionSyntaxValidator.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/ConditionSyntaxValidator.java
@@ -20,21 +20,16 @@ import io.xlate.edi.schema.EDISyntaxRule;
 
 class ConditionSyntaxValidator implements SyntaxValidator {
 
-    private static final ConditionSyntaxValidator singleton = new ConditionSyntaxValidator();
+    static final ConditionSyntaxValidator INSTANCE = new ConditionSyntaxValidator();
 
     private ConditionSyntaxValidator() {
     }
 
-    static ConditionSyntaxValidator getInstance() {
-        return singleton;
-    }
-
     @Override
-    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler) {
-        SyntaxStatus status = scanSyntax(syntax, structure.getChildren());
-
+    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler, SyntaxStatus status) {
         if (status.anchorPresent && status.elementCount < syntax.getPositions().size()) {
             signalConditionError(syntax, structure, handler);
         }
     }
+
 }
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/ExclusionSyntaxValidator.java b/src/main/java/io/xlate/edi/internal/stream/validation/ExclusionSyntaxValidator.java
index acb707a..a117b47 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/ExclusionSyntaxValidator.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/ExclusionSyntaxValidator.java
@@ -20,21 +20,16 @@ import io.xlate.edi.schema.EDISyntaxRule;
 
 class ExclusionSyntaxValidator implements SyntaxValidator {
 
-    private static final ExclusionSyntaxValidator singleton = new ExclusionSyntaxValidator();
+    static final ExclusionSyntaxValidator INSTANCE = new ExclusionSyntaxValidator();
 
     private ExclusionSyntaxValidator() {
     }
 
-    static ExclusionSyntaxValidator getInstance() {
-        return singleton;
-    }
-
     @Override
-    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler) {
-        SyntaxStatus status = scanSyntax(syntax, structure.getChildren());
-
+    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler, SyntaxStatus status) {
         if (status.elementCount > 1) {
             signalExclusionError(syntax, structure, handler);
         }
     }
+
 }
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/FirstOnlySyntaxValidator.java b/src/main/java/io/xlate/edi/internal/stream/validation/FirstOnlySyntaxValidator.java
new file mode 100644
index 0000000..5d2e7f9
--- /dev/null
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/FirstOnlySyntaxValidator.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright 2020 xlate.io LLC, http://www.xlate.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ ******************************************************************************/
+package io.xlate.edi.internal.stream.validation;
+
+import io.xlate.edi.internal.stream.tokenization.ValidationEventHandler;
+import io.xlate.edi.schema.EDISyntaxRule;
+
+class FirstOnlySyntaxValidator implements SyntaxValidator {
+
+    static final FirstOnlySyntaxValidator INSTANCE = new FirstOnlySyntaxValidator();
+
+    private FirstOnlySyntaxValidator() {
+    }
+
+    @Override
+    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler, SyntaxStatus status) {
+        if (status.anchorPresent && status.elementCount > 1) {
+            signalExclusionError(syntax, structure, handler);
+        }
+    }
+
+}
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/ListSyntaxValidator.java b/src/main/java/io/xlate/edi/internal/stream/validation/ListSyntaxValidator.java
index 4ab3b43..5d83ab5 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/ListSyntaxValidator.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/ListSyntaxValidator.java
@@ -20,21 +20,16 @@ import io.xlate.edi.schema.EDISyntaxRule;
 
 class ListSyntaxValidator implements SyntaxValidator {
 
-    private static final ListSyntaxValidator singleton = new ListSyntaxValidator();
+    static final ListSyntaxValidator INSTANCE = new ListSyntaxValidator();
 
     private ListSyntaxValidator() {
     }
 
-    static ListSyntaxValidator getInstance() {
-        return singleton;
-    }
-
     @Override
-    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler) {
-        SyntaxStatus status = scanSyntax(syntax, structure.getChildren());
-
+    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler, SyntaxStatus status) {
         if (status.anchorPresent && status.elementCount == 1) {
             signalConditionError(syntax, structure, handler);
         }
     }
+
 }
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/PairedSyntaxValidator.java b/src/main/java/io/xlate/edi/internal/stream/validation/PairedSyntaxValidator.java
index fc6c011..c5f99f5 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/PairedSyntaxValidator.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/PairedSyntaxValidator.java
@@ -20,19 +20,13 @@ import io.xlate.edi.schema.EDISyntaxRule;
 
 class PairedSyntaxValidator implements SyntaxValidator {
 
-    private static final PairedSyntaxValidator singleton = new PairedSyntaxValidator();
+    static final PairedSyntaxValidator INSTANCE = new PairedSyntaxValidator();
 
     private PairedSyntaxValidator() {
     }
 
-    static PairedSyntaxValidator getInstance() {
-        return singleton;
-    }
-
     @Override
-    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler) {
-        SyntaxStatus status = scanSyntax(syntax, structure.getChildren());
-
+    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler, SyntaxStatus status) {
         if (status.elementCount == 0) {
             return;
         }
@@ -41,4 +35,5 @@ class PairedSyntaxValidator implements SyntaxValidator {
             signalConditionError(syntax, structure, handler);
         }
     }
+
 }
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/RequiredSyntaxValidator.java b/src/main/java/io/xlate/edi/internal/stream/validation/RequiredSyntaxValidator.java
index 9a77d00..84ecdd3 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/RequiredSyntaxValidator.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/RequiredSyntaxValidator.java
@@ -20,21 +20,16 @@ import io.xlate.edi.schema.EDISyntaxRule;
 
 class RequiredSyntaxValidator implements SyntaxValidator {
 
-    private static final RequiredSyntaxValidator singleton = new RequiredSyntaxValidator();
+    static final RequiredSyntaxValidator INSTANCE = new RequiredSyntaxValidator();
 
     private RequiredSyntaxValidator() {
     }
 
-    static RequiredSyntaxValidator getInstance() {
-        return singleton;
-    }
-
     @Override
-    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler) {
-        SyntaxStatus status = scanSyntax(syntax, structure.getChildren());
-
+    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler, SyntaxStatus status) {
         if (status.elementCount < 1) {
             signalConditionError(syntax, structure, handler);
         }
     }
+
 }
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/SingleSyntaxValidator.java b/src/main/java/io/xlate/edi/internal/stream/validation/SingleSyntaxValidator.java
index b4fdac3..2044427 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/SingleSyntaxValidator.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/SingleSyntaxValidator.java
@@ -20,23 +20,18 @@ import io.xlate.edi.schema.EDISyntaxRule;
 
 class SingleSyntaxValidator implements SyntaxValidator {
 
-    private static final SingleSyntaxValidator singleton = new SingleSyntaxValidator();
+    static final SingleSyntaxValidator INSTANCE = new SingleSyntaxValidator();
 
     private SingleSyntaxValidator() {
     }
 
-    static SingleSyntaxValidator getInstance() {
-        return singleton;
-    }
-
     @Override
-    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler) {
-        SyntaxStatus status = scanSyntax(syntax, structure.getChildren());
-
+    public void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler, SyntaxStatus status) {
         if (status.elementCount > 1) {
             signalExclusionError(syntax, structure, handler);
         } else if (status.elementCount == 0) {
             signalConditionError(syntax, structure, handler);
         }
     }
+
 }
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/SyntaxValidator.java b/src/main/java/io/xlate/edi/internal/stream/validation/SyntaxValidator.java
index bc07146..3f5cea4 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/SyntaxValidator.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/SyntaxValidator.java
@@ -38,22 +38,25 @@ interface SyntaxValidator {
 
         switch (type) {
         case CONDITIONAL:
-            instance = ConditionSyntaxValidator.getInstance();
+            instance = ConditionSyntaxValidator.INSTANCE;
             break;
         case EXCLUSION:
-            instance = ExclusionSyntaxValidator.getInstance();
+            instance = ExclusionSyntaxValidator.INSTANCE;
             break;
         case LIST:
-            instance = ListSyntaxValidator.getInstance();
+            instance = ListSyntaxValidator.INSTANCE;
             break;
         case PAIRED:
-            instance = PairedSyntaxValidator.getInstance();
+            instance = PairedSyntaxValidator.INSTANCE;
             break;
         case REQUIRED:
-            instance = RequiredSyntaxValidator.getInstance();
+            instance = RequiredSyntaxValidator.INSTANCE;
             break;
         case SINGLE:
-            instance = SingleSyntaxValidator.getInstance();
+            instance = SingleSyntaxValidator.INSTANCE;
+            break;
+        case FIRSTONLY:
+            instance = FirstOnlySyntaxValidator.INSTANCE;
             break;
         }
 
@@ -172,5 +175,9 @@ interface SyntaxValidator {
         return position;
     }
 
-    void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler);
+    default void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler) {
+        validate(syntax, structure, handler, scanSyntax(syntax, structure.getChildren()));
+    }
+
+    void validate(EDISyntaxRule syntax, UsageNode structure, ValidationEventHandler handler, SyntaxStatus status);
 }
diff --git a/src/main/java/io/xlate/edi/schema/EDISyntaxRule.java b/src/main/java/io/xlate/edi/schema/EDISyntaxRule.java
index 6c32129..9eb5fe1 100644
--- a/src/main/java/io/xlate/edi/schema/EDISyntaxRule.java
+++ b/src/main/java/io/xlate/edi/schema/EDISyntaxRule.java
@@ -49,7 +49,12 @@ public interface EDISyntaxRule {
          * X12: Type L
          * EDIFACT: (D6) If first, then at least one more
          */
-        LIST;
+        LIST,
+        /**
+         * X12: N/A
+         * EDIFACT: (D7) If first, then none of the others
+         */
+        FIRSTONLY;
     }
 
     Type getType();
diff --git a/src/main/java/io/xlate/edi/stream/Location.java b/src/main/java/io/xlate/edi/stream/Location.java
index 9ccf449..fa14744 100644
--- a/src/main/java/io/xlate/edi/stream/Location.java
+++ b/src/main/java/io/xlate/edi/stream/Location.java
@@ -90,4 +90,13 @@ public interface Location {
      * @return the current component element position
      */
     int getComponentPosition();
+
+    /**
+     * Create a new copy of this instance
+     *
+     * @return a new {@link Location } instance with the same values of the instance being copied
+     *
+     * @since 1.11
+     */
+    Location copy();
 }
diff --git a/src/main/resources/X12/common.xml b/src/main/resources/X12/common.xml
index a6e3587..bba36a1 100644
--- a/src/main/resources/X12/common.xml
+++ b/src/main/resources/X12/common.xml
@@ -270,7 +270,7 @@
     </enumeration>
   </elementType>
 
-  <elementType name="I65" base="string"/>
+  <elementType name="I65" base="string" title="Repetition Separator"/>
 
   <!-- Elements for ISX Segment (Interchange Syntax Extension) -->
   <elementType name="I69" base="string" minLength="1" maxLength="1" title="Release Character"/>
diff --git a/src/main/resources/schema/EDISchema-v2.xsd b/src/main/resources/schema/EDISchema-v2.xsd
index 11dc26a..bb6c746 100644
--- a/src/main/resources/schema/EDISchema-v2.xsd
+++ b/src/main/resources/schema/EDISchema-v2.xsd
@@ -88,6 +88,7 @@
       <enumeration value="exclusion" />
       <enumeration value="conditional" />
       <enumeration value="list" />
+      <enumeration value="firstonly" />
     </restriction>
   </simpleType>
 
diff --git a/src/main/resources/schema/EDISchema-v3.xsd b/src/main/resources/schema/EDISchema-v3.xsd
index b68542b..f5f07d4 100644
--- a/src/main/resources/schema/EDISchema-v3.xsd
+++ b/src/main/resources/schema/EDISchema-v3.xsd
@@ -40,6 +40,7 @@
       <enumeration value="exclusion" />
       <enumeration value="conditional" />
       <enumeration value="list" />
+      <enumeration value="firstonly" />
     </restriction>
   </simpleType>
 
diff --git a/src/main/resources/schema/EDISchema-v4.xsd b/src/main/resources/schema/EDISchema-v4.xsd
index 1878a95..988f30d 100644
--- a/src/main/resources/schema/EDISchema-v4.xsd
+++ b/src/main/resources/schema/EDISchema-v4.xsd
@@ -64,6 +64,11 @@
           <documentation>If first, then at least one more</documentation>
         </annotation>
       </enumeration>
+      <enumeration value="firstonly">
+        <annotation>
+          <documentation>If first, then none of the others</documentation>
+        </annotation>
+      </enumeration>
     </restriction>
   </simpleType>
 


Test patch:
diff --git a/src/test/java/io/xlate/edi/internal/schema/StaEDISchemaTest.java b/src/test/java/io/xlate/edi/internal/schema/StaEDISchemaTest.java
index ab5af7f..bbd5c74 100644
--- a/src/test/java/io/xlate/edi/internal/schema/StaEDISchemaTest.java
+++ b/src/test/java/io/xlate/edi/internal/schema/StaEDISchemaTest.java
@@ -16,8 +16,10 @@
 package io.xlate.edi.internal.schema;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.InputStream;
 import java.net.URL;
@@ -100,16 +102,28 @@ class StaEDISchemaTest {
 
         assertEquals(EDIType.Type.TRANSACTION, schema.getType(StaEDISchema.TRANSACTION_ID).getType());
 
+        assertFalse(schema.containsSegment("DE0479"));
         EDIType ak101 = schema.getType("DE0479");
         assertEquals(EDIType.Type.ELEMENT, ak101.getType());
         assertEquals(ak101, schema.getType("DE0479"));
 
+        assertFalse(schema.containsSegment("DE0715"));
         EDIType ak901 = schema.getType("DE0715");
         assertEquals(EDIType.Type.ELEMENT, ak901.getType());
         assertNotEquals(ak101, ak901);
 
+        assertTrue(schema.containsSegment("AK9"));
         EDIType ak9 = schema.getType("AK9");
         assertEquals(EDIType.Type.SEGMENT, ak9.getType());
         assertNotEquals(ak101, ak9);
     }
+
+    @Test
+    void testEmptyTypesMapThrowsException() throws Exception {
+        StaEDISchema schema = new StaEDISchema(StaEDISchema.INTERCHANGE_ID, StaEDISchema.TRANSACTION_ID);
+        Map<String, EDIType> types = Collections.emptyMap();
+        EDISchemaException thrown = assertThrows(EDISchemaException.class, () -> schema.setTypes(types));
+        assertEquals(String.format("Schema must contain either %s or %s", StaEDISchema.INTERCHANGE_ID, StaEDISchema.TRANSACTION_ID),
+                     thrown.getMessage());
+    }
 }
diff --git a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderSyntaxTest.java b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderSyntaxTest.java
new file mode 100644
index 0000000..8e604c6
--- /dev/null
+++ b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderSyntaxTest.java
@@ -0,0 +1,120 @@
+package io.xlate.edi.internal.stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import io.xlate.edi.schema.EDISchemaException;
+import io.xlate.edi.schema.Schema;
+import io.xlate.edi.schema.SchemaFactory;
+import io.xlate.edi.stream.EDIInputFactory;
+import io.xlate.edi.stream.EDIStreamException;
+import io.xlate.edi.stream.EDIStreamReader;
+import io.xlate.edi.stream.EDIStreamValidationError;
+import io.xlate.edi.stream.Location;
+
+@SuppressWarnings("unchecked")
+class StaEDIStreamReaderSyntaxTest {
+
+    Object[] readInterchange(String data, String schemaPath) throws EDIStreamException, EDISchemaException {
+        EDIInputFactory factory = EDIInputFactory.newFactory();
+        InputStream stream = new ByteArrayInputStream(data.getBytes());
+        EDIStreamReader reader = factory.createEDIStreamReader(stream);
+        List<EDIStreamValidationError> errors = new ArrayList<>();
+        List<Location> errorLocations = new ArrayList<>();
+
+        while (reader.hasNext()) {
+            switch (reader.next()) {
+            case START_TRANSACTION:
+                SchemaFactory schemaFactory = SchemaFactory.newFactory();
+                Schema schema = schemaFactory.createSchema(getClass().getResource(schemaPath));
+                reader.setTransactionSchema(schema);
+                break;
+            case SEGMENT_ERROR:
+            case ELEMENT_OCCURRENCE_ERROR:
+            case ELEMENT_DATA_ERROR:
+                errors.add(reader.getErrorType());
+                errorLocations.add(reader.getLocation().copy());
+                break;
+            default:
+                break;
+            }
+        }
+
+        return new Object[] { errors, errorLocations };
+    }
+
+    @Test
+    void testFirstSyntaxValidation_NonePresent() throws Exception {
+        Object[] result = readInterchange(""
+                + "UNB+UNOA:3+SENDER+RECEIVER+200906:1148+1'"
+                + "UNH+1+INVOIC:D:93A:UN'"
+                + "UXA+++'"
+                + "UNT+3+1'"
+                + "UNZ+1+1'", "/EDIFACT/fragment-first-syntax-validation.xml");
+
+        List<EDIStreamValidationError> errors = (List<EDIStreamValidationError>) result[0];
+        assertEquals(0, errors.size(), () -> "Unexpected errors: " + errors);
+    }
+
+    @Test
+    void testFirstSyntaxValidation_FirstOnlyPresent() throws Exception {
+        Object[] result = readInterchange(""
+                + "UNB+UNOA:3+SENDER+RECEIVER+200906:1148+1'"
+                + "UNH+1+INVOIC:D:93A:UN'"
+                + "UXA+FIRST++'"
+                + "UNT+3+1'"
+                + "UNZ+1+1'", "/EDIFACT/fragment-first-syntax-validation.xml");
+
+        List<EDIStreamValidationError> errors = (List<EDIStreamValidationError>) result[0];
+        assertEquals(0, errors.size(), () -> "Unexpected errors: " + errors);
+    }
+
+    @Test
+    void testFirstSyntaxValidation_FirstAndSecondPresent() throws Exception {
+        Object[] result = readInterchange(""
+                + "UNB+UNOA:3+SENDER+RECEIVER+200906:1148+1'"
+                + "UNH+1+INVOIC:D:93A:UN'"
+                + "UXA+FIRST+SECOND+'"
+                + "UNT+3+1'"
+                + "UNZ+1+1'", "/EDIFACT/fragment-first-syntax-validation.xml");
+
+        List<EDIStreamValidationError> errors = (List<EDIStreamValidationError>) result[0];
+        assertEquals(0, errors.size(), () -> "Unexpected errors: " + errors);
+    }
+
+    @Test
+    void testFirstSyntaxValidation_SecondAndThirdPresent() throws Exception {
+        Object[] result = readInterchange(""
+                + "UNB+UNOA:3+SENDER+RECEIVER+200906:1148+1'"
+                + "UNH+1+INVOIC:D:93A:UN'"
+                + "UXA++SECOND+THIRD'"
+                + "UNT+3+1'"
+                + "UNZ+1+1'", "/EDIFACT/fragment-first-syntax-validation.xml");
+
+        List<EDIStreamValidationError> errors = (List<EDIStreamValidationError>) result[0];
+        assertEquals(0, errors.size(), () -> "Unexpected errors: " + errors);
+    }
+
+    @Test
+    void testFirstSyntaxValidation_FirstAndThirdPresent() throws Exception {
+        Object[] result = readInterchange(""
+                + "UNB+UNOA:3+SENDER+RECEIVER+200906:1148+1'"
+                + "UNH+1+INVOIC:D:93A:UN'"
+                + "UXA+FIRST++THIRD'"
+                + "UNT+3+1'"
+                + "UNZ+1+1'", "/EDIFACT/fragment-first-syntax-validation.xml");
+
+        List<EDIStreamValidationError> errors = (List<EDIStreamValidationError>) result[0];
+        List<Location> errorLocations = (List<Location>) result[1];
+        assertEquals(1, errors.size(), () -> "Unexpected errors: " + errors);
+        assertEquals(EDIStreamValidationError.EXCLUSION_CONDITION_VIOLATED, errors.get(0));
+        assertEquals("UXA", errorLocations.get(0).getSegmentTag());
+        assertEquals(3, errorLocations.get(0).getElementPosition());
+    }
+}
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 af3b4ed..3e7b07c 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
@@ -339,6 +339,7 @@ class LexerTest {
         }
         EDIException thrown = assertThrows(EDIException.class, lexer::parse);
         assertTrue(thrown.getMessage().contains("EDIE004"));
+        assertEquals(109, thrown.getLocation().copy().getCharacterOffset());
     }
 
     @Test
diff --git a/src/test/resources/EDIFACT/fragment-first-syntax-validation.xml b/src/test/resources/EDIFACT/fragment-first-syntax-validation.xml
new file mode 100644
index 0000000..9705e2f
--- /dev/null
+++ b/src/test/resources/EDIFACT/fragment-first-syntax-validation.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<schema xmlns="http://xlate.io/EDISchema/v4" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://xlate.io/EDISchema/v4 https://www.xlate.io/EDISchema/EDISchema-v4.xsd">
+
+  <transaction>
+    <sequence>
+      <segment type="UXA"/>
+    </sequence>
+  </transaction>
+
+  <elementType name="DE0000" code="0" base="string" maxLength="35"/>
+
+  <segmentType name="UXA">
+    <sequence>
+      <element type="DE0000"/>
+      <element type="DE0000"/>
+      <element type="DE0000"/>
+    </sequence>
+
+    <syntax type="firstonly">
+      <position>1</position>
+      <position>3</position>
+    </syntax>
+  </segmentType>
+</schema>