Skip to content

Commit

Permalink
Validate impl requirement for elements preceding discriminator element
Browse files Browse the repository at this point in the history
  • Loading branch information
MikeEdgar committed Feb 23, 2021
1 parent c7e6fb4 commit d470464
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 29 deletions.
118 changes: 89 additions & 29 deletions src/main/java/io/xlate/edi/internal/stream/validation/Validator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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);
}
}

/* ********************************************************************** */
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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()) {
Expand All @@ -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();
Expand All @@ -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 {
Expand All @@ -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;
Expand All @@ -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<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();
elementErrors.clear();
}

void validateElementValue(Dialect dialect, UsageNode element, UsageNode implElement, CharSequence value, StringBuilder formattedValue) {
Expand Down Expand Up @@ -1202,33 +1242,53 @@ 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;
}
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
}
}
18 changes: 18 additions & 0 deletions src/test/java/io/xlate/edi/test/StaEDITestUtil.java
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<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);
}
}
43 changes: 43 additions & 0 deletions src/test/resources/x12/005010X222/837_loop1000_only.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>

0 comments on commit d470464

Please sign in to comment.