From 97f39a34c609e64f170c70846c66c36a5bbcb53f Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Sat, 27 Nov 2021 17:14:01 -0500 Subject: [PATCH] Allow implementation nodes to occur/reoccur in any order in the input --- .../tokenization/ProxyEventHandler.java | 60 +------------ .../internal/stream/validation/UsageNode.java | 34 +++++--- .../internal/stream/validation/Validator.java | 36 +++----- .../stream/SegmentValidationTest.java | 78 +++++++++-------- .../io/xlate/edi/test/StaEDITestEvent.java | 85 +++++++++++++++++++ .../io/xlate/edi/test/StaEDITestUtil.java | 26 ++++++ 6 files changed, 188 insertions(+), 131 deletions(-) create mode 100644 src/test/java/io/xlate/edi/test/StaEDITestEvent.java 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 da16e570..4d47b4ea 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 @@ -564,72 +564,14 @@ private void enqueueEvent(EDIStreamEvent event, Location location) { StreamEvent target = getPooledEvent(); - EDIStreamEvent associatedEvent = eventQueue.isEmpty() ? null : getAssociatedEvent(error); - - if (eventExists(associatedEvent)) { - /* - * Ensure segment errors occur before other event types - * when the array has other events already present. - */ - int offset = eventQueue.size(); - boolean complete = false; - - while (!complete) { - StreamEvent enqueuedEvent = eventQueue.get(offset - 1); - - if (enqueuedEvent.type == associatedEvent) { - complete = true; - } else { - if (eventQueue.size() == offset) { - eventQueue.add(offset, enqueuedEvent); - } else { - eventQueue.set(offset, enqueuedEvent); - } - offset--; - } - } - - eventQueue.set(offset, target); - } else { - eventQueue.add(target); - } target.type = event; target.errorType = error; target.setData(data); target.setTypeReference(typeReference); target.setLocation(location); - } - private boolean eventExists(EDIStreamEvent associatedEvent) { - int offset = eventQueue.size(); - - while (associatedEvent != null && offset > 0) { - if (eventQueue.get(offset - 1).type == associatedEvent) { - return true; - } - offset--; - } - - return false; + eventQueue.add(target); } - private static EDIStreamEvent getAssociatedEvent(EDIStreamValidationError error) { - final EDIStreamEvent event; - - switch (error) { - case IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES: - event = EDIStreamEvent.END_LOOP; - break; - case MANDATORY_SEGMENT_MISSING: - case IMPLEMENTATION_SEGMENT_BELOW_MINIMUM_USE: - event = null; - break; - default: - event = null; - break; - } - - return event; - } } diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/UsageNode.java b/src/main/java/io/xlate/edi/internal/stream/validation/UsageNode.java index 46a3eb7a..0b34b54a 100644 --- a/src/main/java/io/xlate/edi/internal/stream/validation/UsageNode.java +++ b/src/main/java/io/xlate/edi/internal/stream/validation/UsageNode.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import io.xlate.edi.internal.stream.tokenization.Dialect; @@ -43,10 +44,7 @@ class UsageNode { private int usageCount; UsageNode(UsageNode parent, int depth, EDIReference link, int siblingIndex) { - if (link == null) { - throw new NullPointerException(); - } - + Objects.requireNonNull(link, "link"); this.parent = parent; this.depth = depth; this.link = link; @@ -67,14 +65,21 @@ public static boolean hasMinimumUsage(String version, UsageNode node) { return node == null || node.hasMinimumUsage(version); } - public static UsageNode getParent(UsageNode node) { - return node != null ? node.getParent() : null; - } - public static UsageNode getFirstChild(UsageNode node) { return node != null ? node.getFirstChild() : null; } + public UsageNode getFirstSiblingSameType() { + EDIType type = getReferencedType(); + UsageNode sibling = getFirstSibling(); + + while (!type.equals(sibling.getReferencedType())) { + sibling = sibling.getNextSibling(); + } + + return sibling; + } + public static void resetChildren(UsageNode... nodes) { for (UsageNode node : nodes) { if (node != null) { @@ -228,14 +233,17 @@ public UsageNode getFirstChild() { return getChild(0); } - UsageNode getChildById(CharSequence id) { - return children.stream() - .filter(c -> c != null && c.getId().contentEquals(id)) - .findFirst() - .orElse(null); + private UsageNode getChildById(CharSequence id) { + for (UsageNode child : children) { + if (child.getId().contentEquals(id)) { + return child; + } + } + return null; } UsageNode getSiblingById(CharSequence id) { return parent != null ? parent.getChildById(id) : null; } + } 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 281b36ef..b05a4249 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 @@ -120,10 +120,10 @@ void next(UsageNode nextImpl) { } void nagivateUp(int limit) { - standard = UsageNode.getParent(standard); + standard = standard.getParent(); if (impl != null && impl.getDepth() > limit) { - impl = UsageNode.getParent(impl); + impl = impl.getParent().getFirstSiblingSameType(); } } @@ -439,7 +439,7 @@ public void validateSegment(ValidationEventHandler handler, CharSequence tag) { */ checkMinimumUsage(cursor.standard); - UsageNode nextImpl = checkMinimumImplUsage(cursor.impl, cursor.standard); + UsageNode nextImpl = getNextImplementationNode(cursor.impl, cursor.standard.getReferencedType()); if (cursor.hasNextSibling()) { // Advance to the next segment in the loop @@ -461,16 +461,6 @@ public void validateSegment(ValidationEventHandler handler, CharSequence tag) { handleMissingMandatory(handler); } - UsageNode checkMinimumImplUsage(UsageNode nextImpl, UsageNode current) { - while (nextImpl != null && nextImpl.getReferencedType().equals(current.getReferencedType())) { - // Advance past multiple implementations of the 'current' standard node - checkMinimumUsage(nextImpl); - nextImpl = nextImpl.getNextSibling(); - } - - return nextImpl; - } - boolean handleNode(CharSequence tag, UsageNode current, UsageNode currentImpl, int startDepth, ValidationEventHandler handler) { final boolean handled; @@ -589,6 +579,17 @@ void checkMinimumUsage(UsageNode node) { } } + UsageNode getNextImplementationNode(UsageNode implNode, EDIType type) { + while (implNode != null && implNode.getReferencedType().equals(type)) { + // Advance past multiple implementations of the 'current' standard node + checkMinimumUsage(implNode); + implNode = implNode.getNextSibling(); + } + + // `implNode` will be an implementation of the type following `type` + return implNode; + } + boolean handleLoop(CharSequence tag, UsageNode current, UsageNode currentImpl, int startDepth, ValidationEventHandler handler) { if (!current.getFirstChild().getId().contentEquals(tag)) { return false; @@ -781,7 +782,6 @@ public boolean selectImplementation(Deque eventQueue, ValidationEve } void handleImplementationSelected(UsageNode candidate, UsageNode implSeg, ValidationEventHandler handler) { - checkMinimumImplUsage(implNode, candidate, handler); implSegmentCandidates.clear(); implNode = implSeg; implSegmentSelected = true; @@ -820,14 +820,6 @@ void handleImplementationSelected(UsageNode candidate, UsageNode implSeg, Valida } } - void checkMinimumImplUsage(UsageNode sibling, UsageNode selected, ValidationEventHandler handler) { - while (sibling != null && sibling != selected) { - checkMinimumUsage(sibling); - sibling = sibling.getNextSibling(); - } - handleMissingMandatory(handler); - } - /** * Validate any implementation elements previously skipped while searching * for the loop discriminator element. Validation of enumerated values specified diff --git a/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java b/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java index 74c22516..a8c875c7 100644 --- a/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java +++ b/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java @@ -23,6 +23,9 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.Test; @@ -37,6 +40,8 @@ import io.xlate.edi.stream.EDIStreamFilter; import io.xlate.edi.stream.EDIStreamReader; import io.xlate.edi.stream.EDIStreamValidationError; +import io.xlate.edi.test.StaEDITestEvent; +import io.xlate.edi.test.StaEDITestUtil; @SuppressWarnings("resource") class SegmentValidationTest { @@ -444,7 +449,6 @@ public boolean accept(EDIStreamReader reader) { assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_SEGMENT_BELOW_MINIMUM_USE, "S13"); assertEvent(reader, EDIStreamEvent.END_LOOP, "0000A"); - // Loop B - Occurrence 1 assertEvent(reader, EDIStreamEvent.START_LOOP, "0000B"); assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.SEGMENT_EXCEEDS_MAXIMUM_USE, "S12"); @@ -463,8 +467,6 @@ public boolean accept(EDIStreamReader reader) { assertEvent(reader, EDIStreamEvent.START_LOOP, "0000C"); assertEvent(reader, EDIStreamEvent.END_LOOP, "0000C"); - assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, "S11"); - // Loop D - Occurrence 1 assertEvent(reader, EDIStreamEvent.START_LOOP, "0000D"); @@ -472,6 +474,8 @@ public boolean accept(EDIStreamReader reader) { assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.LOOP_OCCURS_OVER_MAXIMUM_TIMES, "S11", "L0000"); assertEvent(reader, EDIStreamEvent.END_LOOP, "0000D"); + assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, "S11"); + // Loop L0001 has minOccurs=1 in standard (not used in implementation, invalid configuration) assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.MANDATORY_SEGMENT_MISSING, "S20"); @@ -612,20 +616,13 @@ void testImplementation_Only_BHT_HL_Valid() throws EDISchemaException, EDIStream InputStream stream = getClass().getResourceAsStream("/x12/sample837-small.edi"); EDIStreamReader reader = factory.createEDIStreamReader(stream); - reader = factory.createFilteredReader(reader, new EDIStreamFilter() { - @Override - public boolean accept(EDIStreamReader reader) { - switch (reader.getEventType()) { - case START_TRANSACTION: - case SEGMENT_ERROR: - case START_LOOP: - case END_LOOP: - return true; - default: - return false; - } - } - }); + reader = StaEDITestUtil.filterEvents( + factory, + reader, + EDIStreamEvent.START_TRANSACTION, + EDIStreamEvent.SEGMENT_ERROR, + EDIStreamEvent.START_LOOP, + EDIStreamEvent.END_LOOP); assertEvent(reader, EDIStreamEvent.START_TRANSACTION); SchemaFactory schemaFactory = SchemaFactory.newFactory(); @@ -633,27 +630,34 @@ public boolean accept(EDIStreamReader reader) { Schema schema = schemaFactory.createSchema(schemaLocation); reader.setTransactionSchema(schema); - // Occurrence 1 - assertEvent(reader, EDIStreamEvent.START_LOOP, "L0001"); - assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "NM1"); - assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "PER"); - assertEvent(reader, EDIStreamEvent.END_LOOP, "L0001"); - - // Occurrence 2 - assertEvent(reader, EDIStreamEvent.START_LOOP, "L0001"); - assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "NM1"); - assertEvent(reader, EDIStreamEvent.END_LOOP, "L0001"); - - // Loop 2010A - assertEvent(reader, EDIStreamEvent.START_LOOP, "2010A"); - assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "PRV"); - assertEvent(reader, EDIStreamEvent.END_LOOP, "2010A"); - - assertEvent(reader, EDIStreamEvent.START_TRANSACTION); - // Loop 2010A - assertEvent(reader, EDIStreamEvent.START_LOOP, "2010A"); - assertEvent(reader, EDIStreamEvent.END_LOOP, "2010A"); + List expected = Arrays.asList( + StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, "TRANSACTION", "TRANSACTION"), + // Occurrence 1 + StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0001", "L0001"), + StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "NM1", "NM1"), + StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "PER", "PER"), + StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0001", "L0001"), + // Occurrence 2 + StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0001", "L0001"), + StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "NM1", "NM1"), + StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0001", "L0001"), + // Loop 2010A + StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0002", "2010A"), + StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "PRV", "PRV"), + StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0002", "2010A"), + StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, "TRANSACTION", "TRANSACTION"), + // Loop 2010A + StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0002", "2010A"), + StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0002", "2010A")); + + List events = new ArrayList<>(); + events.add(StaEDITestEvent.from(reader, false)); + + while (reader.hasNext()) { + events.add(StaEDITestEvent.from(reader, false)); + } + assertEquals(expected, events); assertTrue(!reader.hasNext(), "Unexpected events exist"); } } diff --git a/src/test/java/io/xlate/edi/test/StaEDITestEvent.java b/src/test/java/io/xlate/edi/test/StaEDITestEvent.java new file mode 100644 index 00000000..ab920de1 --- /dev/null +++ b/src/test/java/io/xlate/edi/test/StaEDITestEvent.java @@ -0,0 +1,85 @@ +package io.xlate.edi.test; + +import java.util.Objects; + +import io.xlate.edi.stream.EDIStreamEvent; +import io.xlate.edi.stream.EDIStreamReader; +import io.xlate.edi.stream.EDIStreamValidationError; +import io.xlate.edi.stream.Location; + +public class StaEDITestEvent { + + public final EDIStreamEvent event; + public final EDIStreamValidationError error; + public final String text; + public final String referenceCode; + public final Location location; + + public StaEDITestEvent(EDIStreamEvent event, EDIStreamValidationError error, String text, String referenceCode, Location location) { + super(); + this.event = event; + this.error = error; + this.text = text; + this.referenceCode = referenceCode; + this.location = location; + } + + public static StaEDITestEvent from(EDIStreamReader reader, boolean includeLocation) { + EDIStreamEvent event = reader.getEventType(); + boolean error; + + switch (event) { + case SEGMENT_ERROR: + case ELEMENT_OCCURRENCE_ERROR: + case ELEMENT_DATA_ERROR: + error = true; + break; + default: + error = false; + break; + } + + return new StaEDITestEvent(reader.getEventType(), + error ? reader.getErrorType() : null, + reader.getText(), + reader.getReferenceCode(), + includeLocation ? reader.getLocation().copy() : null); + } + + public static StaEDITestEvent forError(EDIStreamValidationError error, String text, String referenceCode) { + return new StaEDITestEvent(error.getCategory(), error, text, referenceCode, null); + } + + public static StaEDITestEvent forEvent(EDIStreamEvent event, String text, String referenceCode) { + return new StaEDITestEvent(event, null, text, referenceCode, null); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof StaEDITestEvent) { + StaEDITestEvent other = (StaEDITestEvent) obj; + + return event == other.event + && error == other.error + && Objects.equals(text, other.text) + && Objects.equals(referenceCode, other.referenceCode) + && Objects.equals(location, other.location); + } + + return false; + } + + @Override + public String toString() { + StringBuilder buffer = new StringBuilder("StaEDITestEvent"); + buffer.append('('); + + buffer.append("event=").append(event); + buffer.append(", ").append("error=").append(error); + buffer.append(", ").append("referenceCode=").append(referenceCode); + buffer.append(", ").append("text=").append(text); + buffer.append(", ").append("location=").append(location); + + return buffer.append(')').append('\n').toString(); + } +} diff --git a/src/test/java/io/xlate/edi/test/StaEDITestUtil.java b/src/test/java/io/xlate/edi/test/StaEDITestUtil.java index a142487f..f4762249 100644 --- a/src/test/java/io/xlate/edi/test/StaEDITestUtil.java +++ b/src/test/java/io/xlate/edi/test/StaEDITestUtil.java @@ -87,4 +87,30 @@ public static String[] getJavaVersion() { String versionString = System.getProperty("java.version"); return versionString.split("[\\._]"); } + + public static String toString(EDIStreamReader reader) { + StringBuilder buffer = new StringBuilder(reader.getClass().getName()); + buffer.append('('); + + EDIStreamEvent event = reader.getEventType(); + buffer.append("event=").append(event); + + switch (event) { + case SEGMENT_ERROR: + case ELEMENT_DATA_ERROR: + case ELEMENT_OCCURRENCE_ERROR: + buffer.append(", ").append("error=").append(reader.getErrorType()); + buffer.append(", ").append("referenceCode=").append(reader.getReferenceCode()); + break; + case ELEMENT_DATA: + buffer.append(", ").append("text=").append(reader.getText()); + break; + default: + break; + } + + buffer.append(", ").append("location=").append(reader.getLocation()); + + return buffer.append(')').toString(); + } }