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 7bc5c593..8667eb97 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 @@ static class RevalidationNode { 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 @@ void handleImplementationSelected(UsageNode candidate, UsageNode implSeg, Valida 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 @@ void checkMinimumImplUsage(UsageNode sibling, UsageNode selected, ValidationEven 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 @@ static void updateEventReferences(StreamEvent[] events, int index, int count, ED 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 @@ static void updateReference(StreamEvent event, SegmentImplementation override) { 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 @@ boolean isImplElementSelected() { 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 boolean validCompositeOccurrences(Dialect dialect, Location position) { return false; } - if (!validateImplUnusedElementBlank(this.composite, true)) { + if (!validateImplUnusedElementBlank(this.composite, this.implElement, true)) { return false; } @@ -1030,7 +1067,7 @@ public boolean validateElement(Dialect dialect, StaEDIStreamLocation position, C 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 boolean validateElement(Dialect dialect, StaEDIStreamLocation position, C 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 @@ void validateComponentElement(Dialect dialect, int componentIndex, boolean value if (isImplElementSelected()) { this.implElement = this.implElement.getChild(version, componentIndex); - validateImplUnusedElementBlank(this.element, valueReceived); + validateImplUnusedElementBlank(this.element, this.implElement, valueReceived); } } } else { @@ -1090,7 +1127,7 @@ void validateElementValue(Dialect dialect, StaEDIStreamLocation position, CharSe } } - 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 @@ void validateElementValue(Dialect dialect, StaEDIStreamLocation position, CharSe 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 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(); + elementErrors.clear(); } void validateElementValue(Dialect dialect, UsageNode element, UsageNode implElement, CharSequence value, StringBuilder formattedValue) { @@ -1202,18 +1242,31 @@ public void validateLoopSyntax(ValidationEventHandler validationHandler) { } } + /** + * 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 @@ boolean validateImplUnusedElementBlank(UsageNode node, boolean valueReceived) { 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; } 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 493b1e3c..4dcc0ca1 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.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 @@ void testElementTypeDescriptionAccess() throws EDIStreamException, EDISchemaExce + "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 @@ void testElementTypeDescriptionAccessPriorToDescriminator() throws EDIStreamExce 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 39bfab4e..bc7b0e6f 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 static void write(EDIStreamWriter writer, String segmentTag, Object... el writer.writeEndSegment(); } + public static EDIStreamReader filterEvents(EDIInputFactory factory, EDIStreamReader reader, EDIStreamEvent... events) { + final Set 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 cd2b74ce..a1355d67 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 @@ + + + + + + + + + + 22 + + + + + 0 + 1 + + + + + + + + + + + + + + + + 23 + + + + + 0 + + + + + +