Repository: xlate/staedi (java)

Issue:
Validate implementation elements/composites when segment selected
Validate implementation constraints on the elements and composites not selected until implementation segment is selected

Pull request description:
Validate impl requirement for elements preceding discriminator element
Fixes #88 

Patch:
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/Validator.java b/src/main/java/io/xlate/edi/internal/stream/validation/Validator.java
index 7bc5c59..1180076 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/Validator.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/Validator.java
@@ -144,7 +144,7 @@ public class Validator {
         final UsageNode standard;
         final UsageNode impl;
         final CharBuffer data;
-        final Location location;
+        final StaEDIStreamLocation location;
 
         public RevalidationNode(UsageNode standard, UsageNode impl, CharSequence data, StaEDIStreamLocation location) {
             super();
@@ -786,14 +786,21 @@ public class Validator {
         implNode = implSeg;
         implSegmentSelected = true;
 
-        // TODO: Implementation nodes must be incremented and validated (#88)
+        /*
+         * NOTE: Validation of prior implementation elements will occur only occur
+         * for prior simple elements and composites (not prior components in the same
+         * composite when the discriminator is a component element and the position
+         * within the composite is > 1).
+         */
         if (this.isComposite()) {
             this.implComposite = implSeg.getChild(this.composite.getIndex());
             this.implElement = this.implComposite.getChild(this.element.getIndex());
+            checkPreviousSiblings(implSeg, handler);
         } else if (this.element != null) {
             // Set implementation when standard element is already set (e.g. via discriminator)
             this.implComposite = null;
             this.implElement = implSeg.getChild(this.element.getIndex());
+            checkPreviousSiblings(implSeg, handler);
         }
 
         if (candidate.isNodeType(Type.LOOP)) {
@@ -821,6 +828,34 @@ public class Validator {
         handleMissingMandatory(handler);
     }
 
+    /**
+     * Validate any implementation elements previously skipped while searching
+     * for the loop discriminator element. Validation of enumerated values specified
+     * by the implementation schema are currently not supported for elements occurring
+     * prior to the discriminator element.
+     *
+     * @param implSeg selected implementation segment
+     * @param handler validation handler
+     */
+    void checkPreviousSiblings(UsageNode implSeg, ValidationEventHandler handler) {
+        for (RevalidationNode entry : this.revalidationQueue) {
+            UsageNode std = entry.standard;
+            UsageNode impl = implSeg.getChild(std.getIndex());
+
+            validateImplRepetitions(null, impl);
+
+            if (std.isUsed()) {
+                validateImplUnusedElementBlank(std, impl, true);
+            } else {
+                validateDataElementRequirement(null, std, impl, entry.data, entry.location);
+            }
+
+            handleRevalidatedElementErrors(entry, elementErrors, handler);
+        }
+
+        revalidationQueue.clear();
+    }
+
     static boolean isMatch(PolymorphicImplementation implType, StreamEvent currentEvent) {
         Discriminator discr = implType.getDiscriminator();
 
@@ -865,7 +900,6 @@ public class Validator {
             case START_COMPOSITE:
             case END_COMPOSITE:
             case ELEMENT_DATA:
-                // TODO: Implementation nodes must be incremented and validated (#88)
                 updateReference(events[i], (SegmentImplementation) implSeg);
                 break;
             default:
@@ -893,7 +927,10 @@ public class Validator {
             element = composite.getSequence().get(componentIndex);
         }
 
-        event.setTypeReference(element);
+        if (element != null) {
+            // Set the impl element if present, otherwise leave the standard in place
+            event.setTypeReference(element);
+        }
     }
 
     /* ********************************************************************** */
@@ -914,8 +951,8 @@ public class Validator {
         return implSegmentSelected && this.implElement != null;
     }
 
-    boolean isImplUnusedElementPresent(boolean valueReceived) {
-        return valueReceived && implSegmentSelected && this.implElement == null;
+    boolean isImplUnusedElementPresent(UsageNode implElementUsed, boolean valueReceived) {
+        return valueReceived && implSegmentSelected && implElementUsed == null;
     }
 
     public boolean validCompositeOccurrences(Dialect dialect, Location position) {
@@ -964,7 +1001,7 @@ public class Validator {
             return false;
         }
 
-        if (!validateImplUnusedElementBlank(this.composite, true)) {
+        if (!validateImplUnusedElementBlank(this.composite, this.implElement, true)) {
             return false;
         }
 
@@ -1030,7 +1067,7 @@ public class Validator {
             validateComponentElement(dialect, componentIndex, valueReceived);
         } else {
             // Validated in validCompositeOccurrences for received composites
-            validateImplUnusedElementBlank(this.element, valueReceived);
+            validateImplUnusedElementBlank(this.element, this.implElement, valueReceived);
         }
 
         if (!elementErrors.isEmpty()) {
@@ -1040,7 +1077,7 @@ public class Validator {
         if (valueReceived) {
             validateElementValue(dialect, position, value, formattedValue);
         } else {
-            validateDataElementRequirement(version);
+            validateDataElementRequirement(version, this.element, this.implElement, value, position);
         }
 
         return elementErrors.isEmpty();
@@ -1066,7 +1103,7 @@ public class Validator {
 
                     if (isImplElementSelected()) {
                         this.implElement = this.implElement.getChild(version, componentIndex);
-                        validateImplUnusedElementBlank(this.element, valueReceived);
+                        validateImplUnusedElementBlank(this.element, this.implElement, valueReceived);
                     }
                 }
             } else {
@@ -1090,7 +1127,7 @@ public class Validator {
             }
         }
 
-        if (version.isEmpty() && this.element.hasVersions()) {
+        if ((version.isEmpty() && this.element.hasVersions()) || isPendingDiscrimination()) {
             // This element value can not be validated until the version is determined
             revalidationQueue.add(new RevalidationNode(this.element, this.implElement, value, position));
             return;
@@ -1102,21 +1139,24 @@ public class Validator {
     public void validateVersionConstraints(Dialect dialect, ValidationEventHandler validationHandler, StringBuilder formattedValue) {
         for (RevalidationNode entry : revalidationQueue) {
             validateElementValue(dialect, entry.standard, entry.impl, entry.data, formattedValue);
+            handleRevalidatedElementErrors(entry, elementErrors, validationHandler);
+        }
 
-            for (UsageError error : elementErrors) {
-                validationHandler.elementError(error.getError().getCategory(),
-                                               error.getError(),
-                                               error.getTypeReference(),
-                                               entry.data,
-                                               entry.location.getElementPosition(),
-                                               entry.location.getComponentPosition(),
-                                               entry.location.getElementOccurrence());
-            }
+        revalidationQueue.clear();
+    }
 
-            elementErrors.clear();
+    void handleRevalidatedElementErrors(RevalidationNode entry, List<UsageError> errors, ValidationEventHandler validationHandler) {
+        for (UsageError error : errors) {
+            validationHandler.elementError(error.getError().getCategory(),
+                                           error.getError(),
+                                           error.getTypeReference(),
+                                           entry.data,
+                                           entry.location.getElementPosition(),
+                                           entry.location.getComponentPosition(),
+                                           entry.location.getElementOccurrence());
         }
 
-        revalidationQueue.clear();
+        errors.clear();
     }
 
     void validateElementValue(Dialect dialect, UsageNode element, UsageNode implElement, CharSequence value, StringBuilder formattedValue) {
@@ -1202,18 +1242,31 @@ public class Validator {
         }
     }
 
+    /**
+     * Validate that the implementation element prior to the element identified
+     * by elemenetPosition and componentPosition met its required number of
+     * repetitions. Validation will only occur for simple elements or at the
+     * start of a composite element.
+     *
+     * @param version version to use for validation
+     * @param elementPosition position of the current element
+     * @param componentPosition position of the current component
+     */
     void validateImplRepetitions(String version, int elementPosition, int componentPosition) {
         if (elementPosition > 0 && componentPosition < 0) {
             UsageNode previousImpl = getImplElement(version, elementPosition - 1);
+            validateImplRepetitions(version, previousImpl);
+        }
+    }
 
-            if (tooFewRepetitions(version, previousImpl)) {
-                elementErrors.add(new UsageError(previousImpl, IMPLEMENTATION_TOO_FEW_REPETITIONS));
-            }
+    void validateImplRepetitions(String version, UsageNode implElement) {
+        if (tooFewRepetitions(version, implElement)) {
+            elementErrors.add(new UsageError(implElement, IMPLEMENTATION_TOO_FEW_REPETITIONS));
         }
     }
 
-    boolean validateImplUnusedElementBlank(UsageNode node, boolean valueReceived) {
-        if (isImplUnusedElementPresent(valueReceived)) {
+    boolean validateImplUnusedElementBlank(UsageNode node, UsageNode implNode, boolean valueReceived) {
+        if (isImplUnusedElementPresent(implNode, valueReceived)) {
             // Validated in validCompositeOccurrences for received composites
             elementErrors.add(new UsageError(node, IMPLEMENTATION_UNUSED_DATA_ELEMENT_PRESENT));
             return false;
@@ -1221,14 +1274,21 @@ public class Validator {
         return true;
     }
 
-    void validateDataElementRequirement(String version) {
+    void validateDataElementRequirement(String version, UsageNode element, UsageNode implElement, CharSequence value, StaEDIStreamLocation position) {
         if (!UsageNode.hasMinimumUsage(version, element) || !UsageNode.hasMinimumUsage(version, implElement)) {
-            elementErrors.add(new UsageError(this.element, REQUIRED_DATA_ELEMENT_MISSING));
+            elementErrors.add(new UsageError(element, REQUIRED_DATA_ELEMENT_MISSING));
+        } else if (isPendingDiscrimination()) {
+            // This element requirement can not be validated until the correct implementation is determined
+            revalidationQueue.add(new RevalidationNode(this.element, this.implElement, value, position));
         }
     }
 
     boolean tooFewRepetitions(String version, UsageNode node) {
         if (!UsageNode.hasMinimumUsage(version, node)) {
+            /*
+             * Compare to `1` for repetitions. Elements not meeting requirement
+             * of `> 0` are instead signaled as missing requirement elements.
+             */
             return node.getLink().getMinOccurs(version) > 1;
         }
 


Test patch:
diff --git a/src/test/java/io/xlate/edi/internal/schema/implementation/ElementImplTest.java b/src/test/java/io/xlate/edi/internal/schema/implementation/ElementImplTest.java
index 493b1e3..4dcc0ca 100644
--- a/src/test/java/io/xlate/edi/internal/schema/implementation/ElementImplTest.java
+++ b/src/test/java/io/xlate/edi/internal/schema/implementation/ElementImplTest.java
@@ -20,9 +20,11 @@ import io.xlate.edi.schema.EDISimpleType;
 import io.xlate.edi.schema.EDIType;
 import io.xlate.edi.schema.SchemaFactory;
 import io.xlate.edi.stream.EDIInputFactory;
+import io.xlate.edi.stream.EDIStreamEvent;
 import io.xlate.edi.stream.EDIStreamException;
 import io.xlate.edi.stream.EDIStreamReader;
 import io.xlate.edi.stream.EDIStreamValidationError;
+import io.xlate.edi.test.StaEDITestUtil;
 
 class ElementImplTest {
 
@@ -205,6 +207,7 @@ class ElementImplTest {
                 + "PER*IC*EDI DEPT*EM*FEEDBACK@example.com*TE*3305551212~"
                 + "NM1*40*2*PPO BLUE*****46*54771~"
                 + "HL*1**20*1~"
+                + "HL*2*1*22*0~"
                 + "SE*6*0001~"
                 + "GE*1*1~"
                 + "IEA*1*000000001~").getBytes());
@@ -358,4 +361,40 @@ class ElementImplTest {
         assertEquals("Hierarchical Level Code", hlReferences.get(2).getReferencedType().getTitle());
         assertEquals("Hierarchical Child Code", hlReferences.get(3).getReferencedType().getTitle());
     }
+
+    @Test
+    void testImplElementsPriorToDiscriminatorAreValidated() throws EDIStreamException, EDISchemaException {
+        EDIInputFactory factory = EDIInputFactory.newFactory();
+        ByteArrayInputStream stream = new ByteArrayInputStream((""
+                + "ISA*00*          *00*          *ZZ*ReceiverID     *ZZ*Sender         *200711*0100*^*00501*000000001*0*T*:~"
+                + "GS*HC*99999999999*888888888888*20111219*1340*1*X*005010X222~"
+                + "ST*837*0001*005010X222~"
+                + "BHT*0019*00*565743*20110523*154959*CH~"
+                + "NM1*41*2*SAMPLE INC*****46*496103~"
+                + "PER*IC*EDI DEPT*EM*FEEDBACK@example.com*TE*3305551212~"
+                + "NM1*40*2*PPO BLUE*****46*54771~"
+                + "HL*1*BAD*20*1~"
+                + "HL*2**22*0~"
+                + "SE*6*0001~"
+                + "GE*1*1~"
+                + "IEA*1*000000001~").getBytes());
+
+        EDIStreamReader reader = StaEDITestUtil.filterEvents(factory,
+                                                             factory.createEDIStreamReader(stream),
+                                                             EDIStreamEvent.START_TRANSACTION,
+                                                             EDIStreamEvent.SEGMENT_ERROR,
+                                                             EDIStreamEvent.ELEMENT_OCCURRENCE_ERROR,
+                                                             EDIStreamEvent.ELEMENT_DATA_ERROR);
+
+        assertEquals(EDIStreamEvent.START_TRANSACTION, reader.next());
+        reader.setTransactionSchema(SchemaFactory.newFactory()
+                                    .createSchema(getClass().getResource("/x12/005010X222/837_loop1000_only.xml")));
+
+        assertEquals(EDIStreamEvent.ELEMENT_OCCURRENCE_ERROR, reader.next());
+        assertEquals(EDIStreamValidationError.IMPLEMENTATION_UNUSED_DATA_ELEMENT_PRESENT, reader.getErrorType());
+        assertEquals("BAD", reader.getText());
+
+        assertEquals(EDIStreamEvent.ELEMENT_OCCURRENCE_ERROR, reader.next());
+        assertEquals(EDIStreamValidationError.REQUIRED_DATA_ELEMENT_MISSING, reader.getErrorType());
+    }
 }
diff --git a/src/test/java/io/xlate/edi/test/StaEDITestUtil.java b/src/test/java/io/xlate/edi/test/StaEDITestUtil.java
index 39bfab4..bc7b0e6 100644
--- a/src/test/java/io/xlate/edi/test/StaEDITestUtil.java
+++ b/src/test/java/io/xlate/edi/test/StaEDITestUtil.java
@@ -1,6 +1,14 @@
 package io.xlate.edi.test;
 
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import io.xlate.edi.stream.EDIInputFactory;
+import io.xlate.edi.stream.EDIStreamEvent;
 import io.xlate.edi.stream.EDIStreamException;
+import io.xlate.edi.stream.EDIStreamFilter;
+import io.xlate.edi.stream.EDIStreamReader;
 import io.xlate.edi.stream.EDIStreamWriter;
 
 public class StaEDITestUtil {
@@ -32,4 +40,14 @@ public class StaEDITestUtil {
         writer.writeEndSegment();
     }
 
+    public static EDIStreamReader filterEvents(EDIInputFactory factory, EDIStreamReader reader, EDIStreamEvent... events) {
+        final Set<EDIStreamEvent> filteredEvents = new HashSet<>(Arrays.asList(events));
+        final EDIStreamFilter filter = new EDIStreamFilter() {
+            @Override
+            public boolean accept(EDIStreamReader reader) {
+                return filteredEvents.contains(reader.getEventType());
+            }
+        };
+        return factory.createFilteredReader(reader, filter);
+    }
 }
diff --git a/src/test/resources/x12/005010X222/837_loop1000_only.xml b/src/test/resources/x12/005010X222/837_loop1000_only.xml
index cd2b74c..a1355d6 100644
--- a/src/test/resources/x12/005010X222/837_loop1000_only.xml
+++ b/src/test/resources/x12/005010X222/837_loop1000_only.xml
@@ -65,6 +65,49 @@
           </segment>
         </sequence>
       </loop>
+
+      <loop code="2000B" type="L0002" discriminator="3" title="Subscriber Level (2000B)" minOccurs="0">
+        <sequence>
+          <segment type="HL" title="Subscriber Level (HL)">
+            <sequence>
+              <element position="1" title="Hierarchical ID Number (22)" />
+              <element position="2" title="Hierarchical Parent ID Number (22)" minOccurs="1" />
+              <element position="3" title="Hierarchical Level Code (22)">
+                <enumeration>
+                  <value title="Subscriber">22</value>
+                </enumeration>
+              </element>
+              <element position="4" minOccurs="1" title="Hierarchical Child Code (22)">
+                <enumeration>
+                  <value>0</value>
+                  <value>1</value>
+                </enumeration>
+              </element>
+            </sequence>
+          </segment>
+        </sequence>
+      </loop>
+
+      <loop code="2000C" type="L0002" discriminator="3" title="Patient Level (2000C)" minOccurs="0">
+        <sequence>
+          <segment type="HL" title="Patient Level (HL)">
+            <sequence>
+              <element position="1" title="Hierarchical ID Number (23)" />
+              <element position="2" title="Hierarchical Parent ID Number (23)" minOccurs="1" />
+              <element position="3" title="Hierarchical Level Code (23)">
+                <enumeration>
+                  <value title="Patient">23</value>
+                </enumeration>
+              </element>
+              <element position="4" minOccurs="1" title="Hierarchical Child Code (23)">
+                <enumeration>
+                  <value>0</value>
+                </enumeration>
+              </element>
+            </sequence>
+          </segment>
+        </sequence>
+      </loop>
     </sequence>
   </implementation>
 </schema>