diff --git a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java index 46af46ae..184f7168 100644 --- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java +++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java @@ -307,6 +307,10 @@ private Optional validator() { } private void write(int output) throws EDIStreamException { + write(output, false); + } + + private void write(int output, boolean isPrettyPrint) throws EDIStreamException { CharacterClass clazz; clazz = characters.getClass(output); @@ -322,7 +326,7 @@ private void write(int output) throws EDIStreamException { case HEADER_EDIFACT_U: // U(NA) or U(NB) case HEADER_TRADACOMS_S: // S(TX) unconfirmedBuffer.clear(); - writeHeader((char) output); + writeHeader((char) output, isPrettyPrint); break; case HEADER_X12_S: case HEADER_EDIFACT_N: @@ -332,7 +336,7 @@ private void write(int output) throws EDIStreamException { case HEADER_ELEMENT_END: case HEADER_COMPONENT_END: case HEADER_SEGMENT_END: - writeHeader((char) output); + writeHeader((char) output, isPrettyPrint); break; case INVALID: throw new EDIException(String.format("Invalid state: %s; output 0x%04X", state, output)); @@ -342,8 +346,8 @@ private void write(int output) throws EDIStreamException { } } - void writeHeader(char output) throws EDIStreamException { - if (!dialect.appendHeader(characters, output)) { + void writeHeader(char output, boolean isPrettyPrint) throws EDIStreamException { + if (!isPrettyPrint && !dialect.appendHeader(characters, output)) { throw new EDIStreamException(String.format("Unexpected header character: 0x%04X [%s]", (int) output, output), location); } @@ -381,6 +385,9 @@ void writeHeader(char output) throws EDIStreamException { while (unconfirmedBuffer.hasRemaining()) { writeOutput(unconfirmedBuffer.get()); } + } else if (dialect.isRejected()) { + state = State.INITIAL; + throw new EDIStreamException(dialect.getRejectionMessage(), location); } } @@ -499,7 +506,9 @@ void writeSegmentTerminator() throws EDIStreamException { write(this.segmentTerminator); if (prettyPrint) { - writeString(prettyPrintString); + for (int i = 0, m = prettyPrintString.length(); i < m; i++) { + write(prettyPrintString.charAt(i), true); + } } } @@ -522,6 +531,22 @@ public EDIStreamWriter writeEndSegment() throws EDIStreamException { writeSegmentTerminator(); + switch (state) { + case SEGMENT_END: + case HEADER_SEGMENT_END: + case INITIAL: // Ending final segment of the interchange + break; + default: + if (state.isHeaderState()) { + Optional invalidHeaderReason = dialect.assertValidHeaderEnd(); + + if (invalidHeaderReason.isPresent()) { + throw new EDIStreamException(invalidHeaderReason.get()); + } + } + break; + } + level = LEVEL_INTERCHANGE; location.clearSegmentLocations(); transactionSchemaAllowed = false; diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/Dialect.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/Dialect.java index f0eb95e5..773a32f2 100644 --- a/src/main/java/io/xlate/edi/internal/stream/tokenization/Dialect.java +++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/Dialect.java @@ -15,6 +15,8 @@ ******************************************************************************/ package io.xlate.edi.internal.stream.tokenization; +import java.util.Optional; + import io.xlate.edi.stream.Location; public abstract class Dialect { @@ -117,6 +119,12 @@ public boolean isServiceAdviceSegment(CharSequence segmentTag) { public abstract boolean appendHeader(CharacterSet characters, char value); + /** + * Check if the current state of the dialect is at a valid end of segment position. + * If false, the reason will be given to the provided reasonHandler. + */ + public abstract Optional assertValidHeaderEnd(); + /** * Notify the dialect of element data and its location in the stream. Does * not support binary elements. diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/EDIFACTDialect.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/EDIFACTDialect.java index cb04d0e0..1f7f3c9a 100644 --- a/src/main/java/io/xlate/edi/internal/stream/tokenization/EDIFACTDialect.java +++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/EDIFACTDialect.java @@ -15,6 +15,8 @@ ******************************************************************************/ package io.xlate.edi.internal.stream.tokenization; +import java.util.Optional; + import io.xlate.edi.stream.EDIStreamConstants.Standards; import io.xlate.edi.stream.Location; @@ -180,6 +182,26 @@ public boolean appendHeader(CharacterSet characters, char value) { return proceed; } + @Override + public Optional assertValidHeaderEnd() { + if (isRejected()) { + return Optional.of(getRejectionMessage()); + } else if (!isConfirmed()) { + if (UNB.equals(headerTag)) { + return Optional.of("EDIFACT UNB segment incomplete"); + } else { + int length = header.length(); + + if (length < EDIFACT_UNA_LENGTH) { + return Optional.of("EDIFACT UNA segment incomplete"); + } else if (length > EDIFACT_UNA_LENGTH) { + return Optional.of("EDIFACT UNB segment incomplete following UNA service advice segment"); + } + } + } + return Optional.empty(); + } + boolean processInterchangeHeader(CharacterSet characters, char value) { if (index == 0) { characters.setClass(componentDelimiter, CharacterClass.COMPONENT_DELIMITER); @@ -228,7 +250,7 @@ boolean processServiceStringAdvice(CharacterSet characters, char value) { break; } - if (index > EDIFACT_UNA_LENGTH) { + if (index >= EDIFACT_UNA_LENGTH) { if (characters.isIgnored(value)) { header.deleteCharAt(index--); } else if (isIndexBeyondUNBFirstElement()) { diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/State.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/State.java index 62ff9bc4..448ce6ca 100644 --- a/src/main/java/io/xlate/edi/internal/stream/tokenization/State.java +++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/State.java @@ -357,4 +357,8 @@ public State transition(int dialect, CharacterClass clazz) { return TRANSITIONS[dialect][code][clazz.code]; } + public boolean isHeaderState() { + return Category.HEADER == code; + } + } diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/TradacomsDialect.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/TradacomsDialect.java index b8ce9bad..08ce4680 100644 --- a/src/main/java/io/xlate/edi/internal/stream/tokenization/TradacomsDialect.java +++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/TradacomsDialect.java @@ -15,6 +15,8 @@ ******************************************************************************/ package io.xlate.edi.internal.stream.tokenization; +import java.util.Optional; + import io.xlate.edi.stream.EDIStreamConstants.Standards; import io.xlate.edi.stream.Location; @@ -128,6 +130,16 @@ public boolean appendHeader(CharacterSet characters, char value) { return proceed; } + @Override + public Optional assertValidHeaderEnd() { + if (isRejected()) { + return Optional.of(getRejectionMessage()); + } else if (!isConfirmed()) { + return Optional.of("TRADACOMS STX segment incomplete"); + } + return Optional.empty(); + } + boolean processInterchangeHeader(CharacterSet characters, char value) { if (segmentDelimiter == value) { initialize(characters); diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/X12Dialect.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/X12Dialect.java index 3fd46bdc..2e92522a 100644 --- a/src/main/java/io/xlate/edi/internal/stream/tokenization/X12Dialect.java +++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/X12Dialect.java @@ -15,6 +15,8 @@ ******************************************************************************/ package io.xlate.edi.internal.stream.tokenization; +import java.util.Optional; + import io.xlate.edi.stream.EDIStreamConstants.Standards; import io.xlate.edi.stream.Location; @@ -158,6 +160,16 @@ public boolean appendHeader(CharacterSet characters, char value) { return proceed; } + @Override + public Optional assertValidHeaderEnd() { + if (isRejected()) { + return Optional.of(getRejectionMessage()); + } else if (index < X12_SEGMENT_OFFSET) { + return Optional.of("Invalid X12 ISA segment: too short or elements missing"); + } + return Optional.empty(); + } + @Override public void clearTransactionVersion() { agencyCode = ""; diff --git a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamWriterTest.java b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamWriterTest.java index 66541924..1a20e6e8 100644 --- a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamWriterTest.java +++ b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamWriterTest.java @@ -219,7 +219,7 @@ void testWriteEndSegment() throws EDIStreamException { writer.startInterchange(); writer.writeStartSegment("ISA"); writer.writeStartElement().writeElementData("E1").endElement(); - writer.writeEndSegment(); + assertThrows(EDIStreamException.class, () -> writer.writeEndSegment()); writer.flush(); unconfirmedBufferEquals("ISA*E1~", writer); } @@ -349,8 +349,8 @@ void testComponent() throws IllegalStateException, EDIStreamException { writer.writeStartElement() .startComponent() .endComponent() - .startComponent() - .writeEndSegment(); + .startComponent(); + assertThrows(EDIStreamException.class, () -> writer.writeEndSegment()); writer.flush(); unconfirmedBufferEquals("ISA*:~", writer); } @@ -369,8 +369,8 @@ void testComponent_EmptyTruncated() throws IllegalStateException, EDIStreamExcep .startComponent() .endComponent() .endElement() - .writeEmptyElement() - .writeEndSegment(); + .writeEmptyElement(); + assertThrows(EDIStreamException.class, () -> writer.writeEndSegment()); writer.flush(); unconfirmedBufferEquals("ISA~", writer); @@ -418,7 +418,7 @@ void testWriteEmptyElement() throws IllegalStateException, EDIStreamException { writer.writeEmptyElement(); writer.writeEmptyElement(); writer.writeEmptyElement(); - writer.writeEndSegment(); + assertThrows(EDIStreamException.class, () -> writer.writeEndSegment()); writer.flush(); unconfirmedBufferEquals("ISA****~", writer); } @@ -435,7 +435,7 @@ void testWriteEmptyComponent() throws IllegalStateException, EDIStreamException writer.writeEmptyComponent(); writer.writeEmptyComponent(); writer.writeEmptyComponent(); - writer.writeEndSegment(); + assertThrows(EDIStreamException.class, () -> writer.writeEndSegment()); writer.flush(); unconfirmedBufferEquals("ISA*:::~", writer); } @@ -454,7 +454,7 @@ void testWriteEmptyElements_EmptyTruncated() throws IllegalStateException, EDISt writer.writeEmptyComponent(); writer.writeEmptyComponent(); writer.endElement(); - writer.writeEndSegment(); + assertThrows(EDIStreamException.class, () -> writer.writeEndSegment()); writer.flush(); unconfirmedBufferEquals("ISA~", writer); } @@ -513,7 +513,7 @@ void testWriteTruncatedElements() throws IllegalStateException, EDIStreamExcepti writer.endElement(); writer.writeEmptyElement(); - writer.writeEndSegment(); + assertThrows(EDIStreamException.class, () -> writer.writeEndSegment()); writer.flush(); unconfirmedBufferEquals("ISA**:LAST*:::LAST*:SECOND::LAST***LAST~", writer); @@ -528,7 +528,7 @@ void testWriteElementDataCharSequence() throws EDIStreamException { writer.writeStartSegment("ISA"); writer.writeStartElement(); writer.writeElementData("TEST-ELEMENT"); - writer.writeEndSegment(); + assertThrows(EDIStreamException.class, () -> writer.writeEndSegment()); writer.flush(); unconfirmedBufferEquals("ISA*TEST-ELEMENT~", writer); } @@ -554,7 +554,7 @@ void testWriteElementDataCharArray() throws EDIStreamException { writer.writeStartSegment("ISA"); writer.writeStartElement(); writer.writeElementData(new char[] { 'C', 'H', 'A', 'R', 'S' }, 0, 5); - writer.writeEndSegment(); + assertThrows(EDIStreamException.class, () -> writer.writeEndSegment()); writer.flush(); unconfirmedBufferEquals("ISA*CHARS~", writer); }