From 40524431cccb694a0429a233d36487e36af1b08f Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Thu, 20 Apr 2023 05:34:36 -0400 Subject: [PATCH] Push writer output through lexer and proxy handler for validation Fixes #283 Fixes #286 Signed-off-by: Michael Edgar --- pom.xml | 2 +- .../internal/stream/StaEDIOutputFactory.java | 1 + .../internal/stream/StaEDIStreamWriter.java | 1033 ++++++++++------- .../stream/tokenization/EDIException.java | 10 +- .../internal/stream/tokenization/Lexer.java | 306 +++-- .../tokenization/ProxyEventHandler.java | 13 +- .../stream/tokenization/StreamEvent.java | 2 +- .../validation/AlphaNumericValidator.java | 28 +- .../internal/stream/validation/Validator.java | 7 + .../io/xlate/edi/stream/EDIInputFactory.java | 13 + .../io/xlate/edi/stream/EDIOutputFactory.java | 19 + .../xlate/edi/stream/EDIStreamException.java | 12 +- .../edi/stream/EDIValidationException.java | 2 +- .../stream/StaEDIStreamWriterTest.java | 188 ++- .../stream/StaEDIXMLStreamWriterTest.java | 7 +- .../WriteInterchangeAcknowledgementTest.java | 2 +- .../resources/wiki/x12_interchange_ack.txt | 2 +- 17 files changed, 1029 insertions(+), 618 deletions(-) diff --git a/pom.xml b/pom.xml index aeb958ae..ea59c396 100644 --- a/pom.xml +++ b/pom.xml @@ -352,7 +352,7 @@ (,11) - 2.0.1 + 2.0.2 1.0.2 3.0.1 3.0.2 diff --git a/src/main/java/io/xlate/edi/internal/stream/StaEDIOutputFactory.java b/src/main/java/io/xlate/edi/internal/stream/StaEDIOutputFactory.java index 1c3ccd5a..13a6d454 100644 --- a/src/main/java/io/xlate/edi/internal/stream/StaEDIOutputFactory.java +++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIOutputFactory.java @@ -41,6 +41,7 @@ public StaEDIOutputFactory() { supportedProperties.add(PRETTY_PRINT); supportedProperties.add(TRUNCATE_EMPTY_ELEMENTS); supportedProperties.add(FORMAT_ELEMENTS); + supportedProperties.add(EDI_VALIDATE_CONTROL_STRUCTURE); properties.put(PRETTY_PRINT, Boolean.FALSE); } 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 39de8e44..c279f6af 100644 --- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java +++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java @@ -15,6 +15,7 @@ ******************************************************************************/ package io.xlate.edi.internal.stream; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -28,27 +29,21 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; -import io.xlate.edi.internal.stream.tokenization.CharacterClass; -import io.xlate.edi.internal.stream.tokenization.CharacterSet; +import io.xlate.edi.internal.schema.SchemaUtils; import io.xlate.edi.internal.stream.tokenization.Dialect; import io.xlate.edi.internal.stream.tokenization.DialectFactory; import io.xlate.edi.internal.stream.tokenization.EDIException; import io.xlate.edi.internal.stream.tokenization.EDIFACTDialect; -import io.xlate.edi.internal.stream.tokenization.ElementDataHandler; -import io.xlate.edi.internal.stream.tokenization.State; -import io.xlate.edi.internal.stream.tokenization.ValidationEventHandler; -import io.xlate.edi.internal.stream.tokenization.X12Dialect; -import io.xlate.edi.internal.stream.validation.UsageError; -import io.xlate.edi.internal.stream.validation.Validator; -import io.xlate.edi.schema.EDIReference; -import io.xlate.edi.schema.EDIType; +import io.xlate.edi.internal.stream.tokenization.Lexer; +import io.xlate.edi.internal.stream.tokenization.ProxyEventHandler; +import io.xlate.edi.schema.EDISchemaException; import io.xlate.edi.schema.Schema; +import io.xlate.edi.stream.EDIInputFactory; import io.xlate.edi.stream.EDIOutputErrorReporter; import io.xlate.edi.stream.EDIOutputFactory; import io.xlate.edi.stream.EDIStreamConstants.Delimiters; @@ -59,7 +54,7 @@ import io.xlate.edi.stream.EDIValidationException; import io.xlate.edi.stream.Location; -public class StaEDIStreamWriter implements EDIStreamWriter, ElementDataHandler, ValidationEventHandler { +public class StaEDIStreamWriter implements EDIStreamWriter, Configurable /*ElementDataHandler, ValidationEventHandler*/ { static final Logger LOGGER = Logger.getLogger(StaEDIStreamWriter.class.getName()); @@ -71,30 +66,36 @@ public class StaEDIStreamWriter implements EDIStreamWriter, ElementDataHandler, private static final int LEVEL_COMPONENT = 5; private int level; + private boolean dialectHeaderReceived = false; + private boolean dialectHeaderProcessed = false; + private boolean inBinaryElement = false; - private State state = State.INITIAL; - private CharacterSet characters = new CharacterSet(); + //private State state = State.INITIAL; + //private CharacterSet characters = new CharacterSet(); private final OutputStream stream; private final OutputStreamWriter writer; + private final ProxyEventHandler proxy; + private final Lexer lexer; private final Map properties; private final EDIOutputErrorReporter reporter; private Dialect dialect; - CharBuffer unconfirmedBuffer = CharBuffer.allocate(500); + //CharBuffer unconfirmedBuffer = CharBuffer.allocate(500); - private final StaEDIStreamLocation location; + private StaEDIStreamLocation location; private Schema controlSchema; - private Validator controlValidator; - private boolean transactionSchemaAllowed = false; - private boolean transaction = false; - private Schema transactionSchema; - private Validator transactionValidator; - private CharArraySequence dataHolder = new CharArraySequence(); - private boolean atomicElementWrite = false; - private CharBuffer elementBuffer = CharBuffer.allocate(500); - private final StringBuilder formattedElement = new StringBuilder(); + //private Validator controlValidator; + //private boolean transactionSchemaAllowed = false; + //private boolean transaction = false; + //private Schema transactionSchema; + //private Validator transactionValidator; + //private CharArraySequence dataHolder = new CharArraySequence(); + //private boolean atomicElementWrite = false; + //private CharBuffer elementBuffer = CharBuffer.allocate(500); + //private final StringBuilder formattedElement = new StringBuilder(); private List errors = new ArrayList<>(); - private CharArraySequence elementHolder = new CharArraySequence(); + //private CharArraySequence elementHolder = new CharArraySequence(); + CharBuffer outputBuffer = (CharBuffer) CharBuffer.allocate(500).clear(); private char segmentTerminator; private char segmentTagTerminator; @@ -122,34 +123,24 @@ public StaEDIStreamWriter(OutputStream stream, Charset charset, Map(properties); this.reporter = reporter; - this.emptyElementTruncation = booleanValue(properties.get(EDIOutputFactory.TRUNCATE_EMPTY_ELEMENTS)); - this.prettyPrint = booleanValue(properties.get(EDIOutputFactory.PRETTY_PRINT)); - this.formatElements = booleanValue(properties.get(EDIOutputFactory.FORMAT_ELEMENTS)); + this.emptyElementTruncation = getProperty(EDIOutputFactory.TRUNCATE_EMPTY_ELEMENTS, Boolean::parseBoolean, false); + this.prettyPrint = getProperty(EDIOutputFactory.PRETTY_PRINT, Boolean::parseBoolean, false); + this.formatElements = getProperty(EDIOutputFactory.FORMAT_ELEMENTS, Boolean::parseBoolean, false); this.location = new StaEDIStreamLocation(); - } - boolean booleanValue(Object value) { - if (value instanceof Boolean) { - return (Boolean) value; - } - if (value instanceof String) { - return Boolean.valueOf(value.toString()); - } - if (value == null) { - return false; - } - LOGGER.warning(() -> "Value [" + value + "] could not be converted to boolean"); - return false; + StaEDIStreamLocation outLocation = new StaEDIStreamLocation(); + this.proxy = new ProxyEventHandler(outLocation, null, false); + this.lexer = new Lexer(new ByteArrayInputStream(new byte[0]), Charset.defaultCharset(), proxy, outLocation, false); } - private void setupDelimiters() { - segmentTerminator = getDelimiter(properties, Delimiters.SEGMENT, dialect::getSegmentTerminator); + private void setupDelimiters(Dialect dialect) { + segmentTerminator = getDelimiter(dialect.isConfirmed(), properties, Delimiters.SEGMENT, dialect::getSegmentTerminator); segmentTagTerminator = dialect.getSegmentTagTerminator(); // Not configurable - TRADACOMS - dataElementSeparator = getDelimiter(properties, Delimiters.DATA_ELEMENT, dialect::getDataElementSeparator); - componentElementSeparator = getDelimiter(properties, Delimiters.COMPONENT_ELEMENT, dialect::getComponentElementSeparator); - decimalMark = getDelimiter(properties, Delimiters.DECIMAL, dialect::getDecimalMark); - releaseIndicator = getDelimiter(properties, Delimiters.RELEASE, dialect::getReleaseIndicator); - repetitionSeparator = getDelimiter(properties, Delimiters.REPETITION, dialect::getRepetitionSeparator); + dataElementSeparator = getDelimiter(dialect.isConfirmed(), properties, Delimiters.DATA_ELEMENT, dialect::getDataElementSeparator); + componentElementSeparator = getDelimiter(dialect.isConfirmed(), properties, Delimiters.COMPONENT_ELEMENT, dialect::getComponentElementSeparator); + decimalMark = getDelimiter(dialect.isConfirmed(), properties, Delimiters.DECIMAL, dialect::getDecimalMark); + releaseIndicator = getDelimiter(dialect.isConfirmed(), properties, Delimiters.RELEASE, dialect::getReleaseIndicator); + repetitionSeparator = getDelimiter(dialect.isConfirmed(), properties, Delimiters.REPETITION, dialect::getRepetitionSeparator); String lineSeparator = System.getProperty("line.separator"); @@ -172,8 +163,8 @@ private boolean areDelimitersSpecified() { .anyMatch(properties::containsKey); } - char getDelimiter(Map properties, String key, Supplier dialectSupplier) { - if (properties.containsKey(key) && !dialect.isConfirmed()) { + char getDelimiter(boolean confirmed, Map properties, String key, Supplier dialectSupplier) { + if (properties.containsKey(key) && !confirmed) { return (char) properties.get(key); } return dialectSupplier.get(); @@ -201,9 +192,9 @@ private void ensureFalse(boolean illegalState) { } } - private void ensureState(State s) { - ensureFalse(this.state != s); - } +// private void ensureState(State s) { +// ensureFalse(this.state != s); +// } private void ensureLevel(int l) { ensureFalse(this.level != l); @@ -235,7 +226,6 @@ public void close() throws EDIStreamException { public void flush() throws EDIStreamException { try { writer.flush(); - stream.flush(); } catch (IOException e) { throw new EDIStreamException("Exception flushing output stream", location, e); } @@ -249,21 +239,23 @@ public Schema getControlSchema() { @Override public void setControlSchema(Schema controlSchema) { ensureLevel(LEVEL_INITIAL); + proxy.setControlSchema(controlSchema, true); this.controlSchema = controlSchema; - controlValidator = Validator.forSchema(controlSchema, null, true, formatElements); +// controlValidator = Validator.forSchema(controlSchema, null, true, formatElements); } @Override public void setTransactionSchema(Schema transactionSchema) { - if (!Objects.equals(this.transactionSchema, transactionSchema)) { - this.transactionSchema = transactionSchema; - transactionValidator = Validator.forSchema(transactionSchema, controlSchema, true, formatElements); - } + proxy.setTransactionSchema(transactionSchema); +// if (!Objects.equals(this.transactionSchema, transactionSchema)) { +// this.transactionSchema = transactionSchema; +// transactionValidator = Validator.forSchema(transactionSchema, controlSchema, true, formatElements); +// } } @Override public Location getLocation() { - return location; + return location != null ? location : lexer.getLocation(); } @Override @@ -292,111 +284,122 @@ public Map getDelimiters() { return delimiters; } - private Optional validator() { - Validator validator; - - // Do not use the transactionValidator in the period where it may be set/mutated by the user - if (transaction && !transactionSchemaAllowed) { - validator = transactionValidator; - } else { - validator = controlValidator; - } - - return Optional.ofNullable(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); - - if (clazz == CharacterClass.INVALID) { - throw new EDIStreamException(String.format("Invalid character: 0x%04X", output), location); - } - - state = State.transition(state, dialect, clazz); - - switch (state) { - case HEADER_X12_I: // I(SA) - case HEADER_EDIFACT_U: // U(NA) or U(NB) - case HEADER_TRADACOMS_S: // S(TX) - unconfirmedBuffer.clear(); - writeHeader((char) output, isPrettyPrint); - break; - case HEADER_X12_S: - case HEADER_EDIFACT_N: - case HEADER_TRADACOMS_T: - case INTERCHANGE_CANDIDATE: - case HEADER_DATA: - case HEADER_ELEMENT_END: - case HEADER_COMPONENT_END: - case HEADER_SEGMENT_END: - writeHeader((char) output, isPrettyPrint); - break; - case INVALID: - throw new EDIException(String.format("Invalid state: %s; output 0x%04X", state, output)); - default: - writeOutput(output); - break; - } - } - - void writeHeader(char output, boolean isPrettyPrint) throws EDIStreamException { - if (!isPrettyPrint && !dialect.appendHeader(characters, output)) { - throw new EDIStreamException(String.format("Failed writing %s header: %s", dialect.getStandard(), dialect.getRejectionMessage())); - } - - unconfirmedBuffer.append(output); - - if (dialect.isConfirmed()) { - // Set up the delimiters again once the dialect has confirmed them - setupDelimiters(); - - // Switching to non-header states to proceed after dialect is confirmed - switch (state) { - case HEADER_DATA: - state = State.TAG_SEARCH; - break; - case HEADER_ELEMENT_END: - state = State.ELEMENT_END; - break; - case HEADER_SEGMENT_END: - state = State.SEGMENT_END; - break; - default: - throw new IllegalStateException("Confirmed at state " + state); - } - - unconfirmedBuffer.flip(); - - if (EDIFACTDialect.UNA.equals(dialect.getHeaderTag())) { - // Overlay the UNA segment repetition separator now that it has be confirmed - unconfirmedBuffer.put(7, this.repetitionSeparator > 0 ? this.repetitionSeparator : ' '); - } - - while (unconfirmedBuffer.hasRemaining()) { - writeOutput(unconfirmedBuffer.get()); - } - } - } - - void writeOutput(int output) throws EDIStreamException { +// private Optional validator() { +// Validator validator; +// +// // Do not use the transactionValidator in the period where it may be set/mutated by the user +// if (transaction && !transactionSchemaAllowed) { +// validator = transactionValidator; +// } else { +// validator = controlValidator; +// } +// +// return Optional.ofNullable(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); +// +// if (clazz == CharacterClass.INVALID) { +// throw new EDIStreamException(String.format("Invalid character: 0x%04X", output), location); +// } +// +// state = State.transition(state, dialect, clazz); +// +// switch (state) { +// case HEADER_X12_I: // I(SA) +// case HEADER_EDIFACT_U: // U(NA) or U(NB) +// case HEADER_TRADACOMS_S: // S(TX) +// unconfirmedBuffer.clear(); +// writeHeader((char) output, isPrettyPrint); +// break; +// case HEADER_X12_S: +// case HEADER_EDIFACT_N: +// case HEADER_TRADACOMS_T: +// case INTERCHANGE_CANDIDATE: +// case HEADER_DATA: +// case HEADER_ELEMENT_END: +// case HEADER_COMPONENT_END: +// case HEADER_SEGMENT_END: +// writeHeader((char) output, isPrettyPrint); +// break; +// case INVALID: +// throw new EDIException(String.format("Invalid state: %s; output 0x%04X", state, output)); +// default: +// writeOutput(output); +// break; +// } +// } +// +// void writeHeader(char output, boolean isPrettyPrint) throws EDIStreamException { +// if (!isPrettyPrint && !dialect.appendHeader(characters, output)) { +// throw new EDIStreamException(String.format("Failed writing %s header: %s", dialect.getStandard(), dialect.getRejectionMessage())); +// } +// +// unconfirmedBuffer.append(output); +// +// if (dialect.isConfirmed()) { +// // Set up the delimiters again once the dialect has confirmed them +// setupDelimiters(dialect); +// +// // Switching to non-header states to proceed after dialect is confirmed +// switch (state) { +// case HEADER_DATA: +// state = State.TAG_SEARCH; +// break; +// case HEADER_ELEMENT_END: +// state = State.ELEMENT_END; +// break; +// case HEADER_SEGMENT_END: +// state = State.SEGMENT_END; +// break; +// default: +// throw new IllegalStateException("Confirmed at state " + state); +// } +// +// unconfirmedBuffer.flip(); +// +// if (EDIFACTDialect.UNA.equals(dialect.getHeaderTag())) { +// // Overlay the UNA segment repetition separator now that it has be confirmed +// unconfirmedBuffer.put(7, this.repetitionSeparator > 0 ? this.repetitionSeparator : ' '); +// } +// +// while (unconfirmedBuffer.hasRemaining()) { +// writeOutput(unconfirmedBuffer.get()); +// } +// } +// } +// +// void writeOutput(int output) throws EDIStreamException { +// try { +// location.incrementOffset(output); +// outputBuffer.append((char) output); +// //writer.write(output); +// } catch (Exception e) { +// throw new EDIStreamException("Exception to output stream", location, e); +// } +// } + + void write(int output) throws EDIStreamException { try { - location.incrementOffset(output); - writer.write(output); - } catch (IOException e) { - throw new EDIStreamException("Exception to output stream", location, e); +// location.incrementOffset(output); + outputBuffer.append((char) output); + //writer.write(output); + } catch (Exception e) { + throw new EDIStreamException("Exception writing to output stream", location, e); } } @Override public EDIStreamWriter startInterchange() { ensureLevel(LEVEL_INITIAL); - ensureState(State.INITIAL); + //ensureState(State.INITIAL); level = LEVEL_INTERCHANGE; if (controlSchema == null) { LOGGER.warning("Starting interchange without control structure validation. See EDIStreamWriter#setControlSchema"); @@ -409,48 +412,64 @@ public EDIStreamWriter endInterchange() throws EDIStreamException { ensureLevel(LEVEL_INTERCHANGE); level = LEVEL_INITIAL; flush(); + dialectHeaderReceived = false; + dialectHeaderProcessed = false; + location = null; + dialect = null; return this; } @Override public EDIStreamWriter writeStartSegment(String name) throws EDIStreamException { ensureLevel(LEVEL_INTERCHANGE); - location.incrementSegmentPosition(name); +// location.incrementSegmentPosition(name); + + if (!dialectHeaderReceived) { + dialect = dialect != null ? dialect : DialectFactory.getDialect(name); - if (state == State.INITIAL) { - dialect = DialectFactory.getDialect(name); - setupDelimiters(); + location = lexer.getLocation().copy(); + location.incrementSegmentPosition(name); + + setupDelimiters(dialect); if (dialect instanceof EDIFACTDialect) { - if (EDIFACTDialect.UNB.equals(name) && areDelimitersSpecified()) { - /* - * Writing the EDIFACT header when delimiters were given via properties requires that - * a UNA is written first. - */ - dialect = DialectFactory.getDialect(EDIFACTDialect.UNA); - writeServiceAdviceString(); - segmentValidation(name); + if (EDIFACTDialect.UNB.equals(name)) { +// /* +// * Writing the EDIFACT header when delimiters were given via properties requires that +// * a UNA is written first. +// */ +// dialect = DialectFactory.getDialect(EDIFACTDialect.UNA); + if (EDIFACTDialect.UNB.equals(dialect.getHeaderTag()) && areDelimitersSpecified()) { + writeServiceAdviceString(); + } + //segmentValidation(name); // Now write the UNB writeString(name); + dialectHeaderReceived = true; + } else if (EDIFACTDialect.UNA.equals(name)) { + writeString(name); + writeServiceAdviceCharacters(); } else { - if (EDIFACTDialect.UNA.equals(name)) { - writeString(name); - writeServiceAdviceCharacters(); - } else { - segmentValidation(name); - writeString(name); - } + // Unexpected header segment + writeString(name); + dialectHeaderReceived = true; } } else { - segmentValidation(name); + //segmentValidation(name); writeString(name); + dialectHeaderReceived = true; } } else { - segmentValidation(name); + //segmentValidation(name); writeString(name); + if (segmentTagTerminator != '\0') { + signalElementDataCompleteEvent(segmentTagTerminator); + } else { + signalElementDataCompleteEvent(dataElementSeparator); + } } - countSegment(name); + //countSegment(name); level = LEVEL_SEGMENT; emptyElements = 0; terminateSegmentTag(); @@ -458,20 +477,20 @@ public EDIStreamWriter writeStartSegment(String name) throws EDIStreamException return this; } - void countSegment(String name) { - if (controlValidator != null) { - controlValidator.countSegment(name); - } - } +// void countSegment(String name) { +// if (controlValidator != null) { +// controlValidator.countSegment(name); +// } +// } - void segmentValidation(String name) { - validate(validator -> validator.validateSegment(this, name)); - - if (exitTransaction(name)) { - transaction = false; - validate(validator -> validator.validateSegment(this, name)); - } - } +// void segmentValidation(String name) { +// validate(validator -> validator.validateSegment(this, name)); +// +// if (exitTransaction(name)) { +// transaction = false; +// validate(validator -> validator.validateSegment(this, name)); +// } +// } void terminateSegmentTag() throws EDIStreamException { if (this.segmentTagTerminator != '\0') { @@ -487,7 +506,7 @@ void terminateSegmentTag() throws EDIStreamException { void writeServiceAdviceString() throws EDIStreamException { writeString(EDIFACTDialect.UNA); writeServiceAdviceCharacters(); - writeSegmentTerminator(); + writeDelimiter(Delimiters.SEGMENT); } void writeServiceAdviceCharacters() throws EDIStreamException { @@ -505,51 +524,83 @@ private void writeString(String value) throws EDIStreamException { } } - void writeSegmentTerminator() throws EDIStreamException { - write(this.segmentTerminator); + void writeDelimiter(String delimiterType) throws EDIStreamException { + boolean invokeErrorDetection = true; - if (prettyPrint) { - for (int i = 0, m = prettyPrintString.length(); i < m; i++) { - write(prettyPrintString.charAt(i), true); + switch (delimiterType) { + case Delimiters.RELEASE: + write(this.releaseIndicator); + invokeErrorDetection = false; + break; + case Delimiters.COMPONENT_ELEMENT: + write(this.componentElementSeparator); + invokeErrorDetection = dialectHeaderProcessed; + break; + case Delimiters.DATA_ELEMENT: + location.setRepeating(false); + write(this.dataElementSeparator); + invokeErrorDetection = dialectHeaderProcessed; + break; + case Delimiters.REPETITION: + location.setRepeating(true); + write(this.repetitionSeparator); + invokeErrorDetection = dialectHeaderProcessed; + break; + case Delimiters.SEGMENT: + location.setRepeating(false); + write(this.segmentTerminator); + invokeErrorDetection = dialectHeaderReceived; + if (prettyPrint) { + for (int i = 0, m = prettyPrintString.length(); i < m; i++) { + write(prettyPrintString.charAt(i)/*, true*/); + } } + break; + default: + // unexpected + break; } - } - boolean exitTransaction(CharSequence tag) { - return transaction && !transactionSchemaAllowed && controlSchema != null - && controlSchema.containsSegment(tag.toString()); + if (invokeErrorDetection) { + errorDetection(); + } } +// boolean exitTransaction(CharSequence tag) { +// return transaction && !transactionSchemaAllowed && controlSchema != null +// && controlSchema.containsSegment(tag.toString()); +// } + @Override public EDIStreamWriter writeEndSegment() throws EDIStreamException { ensureLevelAtLeast(LEVEL_SEGMENT); - if (level > LEVEL_SEGMENT) { - validateElement(this.elementBuffer::flip, this.elementBuffer); - } - level = LEVEL_SEGMENT; - validate(validator -> validator.validateSyntax(dialect, this, this, location, false)); - - if (state == State.ELEMENT_DATA_BINARY) { - state = State.ELEMENT_END_BINARY; - } - - writeSegmentTerminator(); - - switch (state) { - case SEGMENT_END: - case HEADER_SEGMENT_END: - case INITIAL: // Ending final segment of the interchange - break; - default: - if (state.isHeaderState() && dialect instanceof X12Dialect) { - throw new EDIStreamException("Invalid X12 ISA segment: too short or elements missing"); - } - break; - } +// if (level > LEVEL_SEGMENT) { +// validateElement(this.elementBuffer::flip, this.elementBuffer); +// } +// level = LEVEL_SEGMENT; +// validate(validator -> validator.validateSyntax(dialect, this, this, location, false)); + +// if (state == State.ELEMENT_DATA_BINARY) { +// state = State.ELEMENT_END_BINARY; +// } + + writeDelimiter(Delimiters.SEGMENT); + +// switch (state) { +// case SEGMENT_END: +// case HEADER_SEGMENT_END: +// case INITIAL: // Ending final segment of the interchange +// break; +// default: +// if (state.isHeaderState() && dialect instanceof X12Dialect) { +// throw new EDIStreamException("Invalid X12 ISA segment: too short or elements missing"); +// } +// break; +// } level = LEVEL_INTERCHANGE; - location.clearSegmentLocations(); - transactionSchemaAllowed = false; +// location.clearSegmentLocations(); +// transactionSchemaAllowed = false; return this; } @@ -558,14 +609,16 @@ public EDIStreamWriter writeEndSegment() throws EDIStreamException { public EDIStreamWriter writeStartElement() throws EDIStreamException { ensureLevel(LEVEL_SEGMENT); level = LEVEL_ELEMENT; - location.incrementElementPosition(); - elementBuffer.clear(); + if (!dialectHeaderProcessed) { + location.incrementElement(false); + } + //elementBuffer.clear(); elementLength = 0; emptyComponents = 0; unterminatedComponent = false; if (!emptyElementTruncation && unterminatedElement) { - write(this.dataElementSeparator); + writeDelimiter(Delimiters.DATA_ELEMENT); } return this; @@ -574,18 +627,25 @@ public EDIStreamWriter writeStartElement() throws EDIStreamException { @Override public EDIStreamWriter writeStartElementBinary() throws EDIStreamException { writeStartElement(); - state = State.ELEMENT_DATA_BINARY; +// state = State.ELEMENT_DATA_BINARY; + writeRequiredSeparators(1); + errorDetection(); + flush(); // Write `Writer` buffers to stream before writing binary + // From the Lexer's perspective, writing a binary element will look like an empty element + inBinaryElement = true; + location.incrementElement(false); return this; } @Override public EDIStreamWriter writeRepeatElement() throws EDIStreamException { ensureLevelAtLeast(LEVEL_SEGMENT); - write(this.repetitionSeparator); + // TODO: test writeRequiredSeparators(1); + writeDelimiter(Delimiters.REPETITION); // The repetition separator was used instead of the data element separator unterminatedElement = false; level = LEVEL_ELEMENT; - location.incrementElementOccurrence(); + elementLength = 0; emptyComponents = 0; unterminatedComponent = false; @@ -596,16 +656,21 @@ public EDIStreamWriter writeRepeatElement() throws EDIStreamException { public EDIStreamWriter endElement() throws EDIStreamException { ensureLevelAtLeast(LEVEL_ELEMENT); - if (!atomicElementWrite) { - if (level > LEVEL_ELEMENT) { - validate(validator -> validator.validateSyntax(dialect, this, this, location, true)); - } else { - validateElement(this.elementBuffer::flip, this.elementBuffer); - } +// if (!atomicElementWrite) { +// if (level > LEVEL_ELEMENT) { +// validate(validator -> validator.validateSyntax(dialect, this, this, location, true)); +// } else { +// validateElement(this.elementBuffer::flip, this.elementBuffer); +// } +// } + + signalElementDataCompleteEvent(dataElementSeparator); + if (!dialectHeaderProcessed) { + location.clearComponentPosition(); } - location.clearComponentPosition(); level = LEVEL_SEGMENT; + inBinaryElement = false; if (elementLength > 0) { unterminatedElement = true; @@ -613,9 +678,9 @@ public EDIStreamWriter endElement() throws EDIStreamException { emptyElements++; } - if (state == State.ELEMENT_DATA_BINARY) { - state = State.ELEMENT_END_BINARY; - } +// if (state == State.ELEMENT_DATA_BINARY) { +// state = State.ELEMENT_END_BINARY; +// } return this; } @@ -623,20 +688,24 @@ public EDIStreamWriter endElement() throws EDIStreamException { @Override public EDIStreamWriter startComponent() throws EDIStreamException { ensureLevelBetween(LEVEL_ELEMENT, LEVEL_COMPOSITE); - ensureFalse(state == State.ELEMENT_DATA_BINARY); + ensureFalse(inBinaryElement); - if (LEVEL_ELEMENT == level) { - // Level is LEVEL_ELEMENT only for the first component - validateCompositeOccurrence(); - } +// if (LEVEL_ELEMENT == level) { +// // Level is LEVEL_ELEMENT only for the first component +// validateCompositeOccurrence(); +// } if (LEVEL_COMPOSITE == level && !emptyElementTruncation) { - write(this.componentElementSeparator); + writeDelimiter(Delimiters.COMPONENT_ELEMENT); } level = LEVEL_COMPONENT; - location.incrementComponentPosition(); - elementBuffer.clear(); + if (!dialectHeaderProcessed) { + location.setComposite(true); + location.incrementElement(false); + } +// location.incrementComponentPosition(); + //elementBuffer.clear(); elementLength = 0; return this; } @@ -645,9 +714,9 @@ public EDIStreamWriter startComponent() throws EDIStreamException { public EDIStreamWriter endComponent() throws EDIStreamException { ensureLevel(LEVEL_COMPONENT); - if (!atomicElementWrite) { - validateElement(this.elementBuffer::flip, this.elementBuffer); - } +// if (!atomicElementWrite) { +// validateElement(this.elementBuffer::flip, this.elementBuffer); +// } if (elementLength > 0) { unterminatedComponent = true; @@ -655,73 +724,79 @@ public EDIStreamWriter endComponent() throws EDIStreamException { emptyComponents++; } + signalElementDataCompleteEvent(componentElementSeparator); level = LEVEL_COMPOSITE; + inBinaryElement = false; return this; } @Override public EDIStreamWriter writeElement(CharSequence text) throws EDIStreamException { - atomicElementWrite = true; +// atomicElementWrite = true; writeStartElement(); - CharSequence value = validateElement(() -> {}, text); - writeElementData(value); +// CharSequence value = validateElement(() -> {}, text); +// writeElementData(value); + writeElementData(text); endElement(); - atomicElementWrite = false; +// atomicElementWrite = false; return this; } @Override public EDIStreamWriter writeElement(char[] text, int start, int end) throws EDIStreamException { - atomicElementWrite = true; +// atomicElementWrite = true; writeStartElement(); - CharSequence value = validateElement(() -> dataHolder.set(text, start, start + end), dataHolder); - writeElementData(value); +// CharSequence value = validateElement(() -> dataHolder.set(text, start, start + end), dataHolder); +// writeElementData(value); + writeElementData(text, start, end); endElement(); - atomicElementWrite = false; +// atomicElementWrite = false; return this; } @Override public EDIStreamWriter writeEmptyElement() throws EDIStreamException { - atomicElementWrite = true; +// atomicElementWrite = true; writeStartElement(); // Ignore possibly-formatted value - validateElement(dataHolder::clear, dataHolder); +// validateElement(dataHolder::clear, dataHolder); endElement(); - atomicElementWrite = false; +// atomicElementWrite = false; return this; } @Override public EDIStreamWriter writeComponent(CharSequence text) throws EDIStreamException { - atomicElementWrite = true; +// atomicElementWrite = true; startComponent(); - CharSequence value = validateElement(() -> {}, text); - writeElementData(value); +// CharSequence value = validateElement(() -> {}, text); +// writeElementData(value); + writeElementData(text); endComponent(); - atomicElementWrite = false; +// atomicElementWrite = false; return this; } @Override public EDIStreamWriter writeComponent(char[] text, int start, int end) throws EDIStreamException { - atomicElementWrite = true; +// atomicElementWrite = true; startComponent(); - CharSequence value = validateElement(() -> dataHolder.set(text, start, start + end), dataHolder); - writeElementData(value); +// CharSequence value = validateElement(() -> dataHolder.set(text, start, start + end), dataHolder); +// writeElementData(value); + writeElementData(text, start, end); endComponent(); - atomicElementWrite = false; +// atomicElementWrite = false; return this; } @Override public EDIStreamWriter writeEmptyComponent() throws EDIStreamException { - atomicElementWrite = true; +// atomicElementWrite = true; startComponent(); // Ignore possibly-formatted value - validateElement(dataHolder::clear, dataHolder); +// validateElement(dataHolder::clear, dataHolder); endComponent(); - atomicElementWrite = false; +// atomicElementWrite = false; return this; } @@ -730,24 +805,24 @@ void writeRequiredSeparators(int dataLength) throws EDIStreamException { return; } - writeRequiredSeparator(emptyElements, unterminatedElement, this.dataElementSeparator); + writeRequiredSeparator(emptyElements, unterminatedElement, Delimiters.DATA_ELEMENT); emptyElements = 0; unterminatedElement = false; if (level == LEVEL_COMPONENT) { - writeRequiredSeparator(emptyComponents, unterminatedComponent, this.componentElementSeparator); + writeRequiredSeparator(emptyComponents, unterminatedComponent, Delimiters.COMPONENT_ELEMENT); emptyComponents = 0; unterminatedComponent = false; } } - void writeRequiredSeparator(int emptyCount, boolean unterminated, char separator) throws EDIStreamException { + void writeRequiredSeparator(int emptyCount, boolean unterminated, String delimiterType) throws EDIStreamException { for (int i = 0; i < emptyCount; i++) { - write(separator); + writeDelimiter(delimiterType); } if (unterminated) { - write(separator); + writeDelimiter(delimiterType); } } @@ -759,16 +834,16 @@ public EDIStreamWriter writeElementData(CharSequence text) throws EDIStreamExcep for (int i = 0, m = text.length(); i < m; i++) { char curr = text.charAt(i); - if (characters.isDelimiter(curr)) { + if (lexer.getCharacterSet().isDelimiter(curr)) { if (releaseIndicator > 0) { - write(releaseIndicator); + writeDelimiter(Delimiters.RELEASE); } else { throw new IllegalArgumentException("Value contains separator: " + curr); } } write(curr); - elementBuffer.put(curr); + //elementBuffer.put(curr); elementLength++; } return this; @@ -782,11 +857,11 @@ public EDIStreamWriter writeElementData(char[] text, int start, int end) throws for (int i = start, m = end; i < m; i++) { char curr = text[i]; - if (characters.isDelimiter(curr)) { + if (lexer.getCharacterSet().isDelimiter(curr)) { throw new IllegalArgumentException("Value contains separator"); } write(curr); - elementBuffer.put(curr); + //elementBuffer.put(curr); elementLength++; } @@ -796,14 +871,10 @@ public EDIStreamWriter writeElementData(char[] text, int start, int end) throws @Override public EDIStreamWriter writeBinaryData(InputStream binaryStream) throws EDIStreamException { ensureLevel(LEVEL_ELEMENT); - ensureState(State.ELEMENT_DATA_BINARY); + ensureFalse(!inBinaryElement); int output; try { - writeRequiredSeparators(binaryStream.available()); - - flush(); // Write `Writer` buffers to stream before writing binary - while ((output = binaryStream.read()) != -1) { location.incrementOffset(output); stream.write(output); @@ -819,13 +890,10 @@ public EDIStreamWriter writeBinaryData(InputStream binaryStream) throws EDIStrea @Override public EDIStreamWriter writeBinaryData(byte[] binary, int start, int end) throws EDIStreamException { ensureLevel(LEVEL_ELEMENT); - ensureState(State.ELEMENT_DATA_BINARY); + ensureFalse(!inBinaryElement); ensureArgs(binary.length, start, end); - writeRequiredSeparators(end - start); try { - flush(); // Write `Writer` buffers to stream before writing binary - for (int i = start; i < end; i++) { location.incrementOffset(binary[i]); stream.write(binary[i]); @@ -841,166 +909,277 @@ public EDIStreamWriter writeBinaryData(byte[] binary, int start, int end) throws @Override public EDIStreamWriter writeBinaryData(ByteBuffer binary) throws EDIStreamException { ensureLevel(LEVEL_ELEMENT); - ensureState(State.ELEMENT_DATA_BINARY); - writeRequiredSeparators(binary.remaining()); + ensureFalse(!inBinaryElement); - while (binary.hasRemaining()) { - write(binary.get()); - elementLength++; + try { + while (binary.hasRemaining()) { + byte out = binary.get(); + location.incrementOffset(out); + stream.write(out); + elementLength++; + } + } catch (IOException e) { + throw new EDIStreamException("Exception writing binary element data", location, e); } return this; } - @Override - public boolean binaryData(InputStream binary) { - // No operation - return true; - } - - @Override - public boolean elementData(CharSequence text, boolean fromStream) { - if (level > LEVEL_ELEMENT) { - location.incrementComponentPosition(); - } else { - location.incrementElementPosition(); + void signalElementDataCompleteEvent(int delimiter) throws EDIStreamException { + if (dialectHeaderProcessed) { + errorDetection(); + lexer.signalElementDataCompleteEvent(delimiter); + outputBuffer.flip(); + processProxyEvents(); + outputBuffer.clear(); } + } - dialect.elementData(elementHolder, location); + void errorDetection() throws EDIStreamException { + errors.clear(); + outputBuffer.flip(); + lexer.parse(outputBuffer); + processProxyEvents(); - validator().ifPresent(validator -> { - if (!validator.validateElement(dialect, location, elementHolder, null)) { - reportElementErrors(validator, elementHolder); - } - }); + outputBuffer.rewind(); - return true; - } + if (!dialectHeaderProcessed) { + dialect = lexer.getDialect(); + location = lexer.getLocation(); - @Override - public void loopBegin(EDIReference typeReference) { - final String loopCode = typeReference.getReferencedType().getCode(); - - if (EDIType.Type.TRANSACTION.toString().equals(loopCode)) { - transaction = true; - transactionSchemaAllowed = true; - if (transactionValidator != null) { - transactionValidator.reset(); + if (!dialect.isConfirmed()) { + throw new EDIStreamException("Header data could not be processed as a valid " + dialect.getStandard() + " header. Data: [" + outputBuffer + "]"); } - } - } - @Override - public void loopEnd(EDIReference typeReference) { - final String loopCode = typeReference.getReferencedType().getCode(); + setupDelimiters(dialect); - if (EDIType.Type.TRANSACTION.toString().equals(loopCode)) { - transaction = false; - dialect.transactionEnd(); - } else if (EDIType.Type.GROUP.toString().equals(loopCode)) { - dialect.groupEnd(); - } - } + if (EDIFACTDialect.UNA.equals(dialect.getHeaderTag())) { + // Overlay the UNA segment repetition separator now that it has be confirmed + outputBuffer.put(7, this.repetitionSeparator > 0 ? this.repetitionSeparator : ' '); + } - @Override - public void elementError(EDIStreamEvent event, - EDIStreamValidationError error, - EDIReference typeReference, - CharSequence data, - int element, - int component, - int repetition) { - - StaEDIStreamLocation copy = location.copy(); - copy.setElementPosition(element); - copy.setElementOccurrence(repetition); - copy.setComponentPosition(component); - - if (this.reporter != null) { - this.reporter.report(error, this, copy, data, typeReference); - } else { - errors.add(new EDIValidationException(event, error, copy, data)); + dialectHeaderProcessed = true; } - } - @Override - public void segmentError(CharSequence token, EDIReference typeReference, EDIStreamValidationError error) { - if (this.reporter != null) { - this.reporter.report(error, this, this.getLocation(), token, typeReference); - } else { - errors.add(new EDIValidationException(EDIStreamEvent.SEGMENT_ERROR, error, location, token)); + try { + writer.write(outputBuffer.array(), outputBuffer.arrayOffset(), outputBuffer.length()); + } catch (IOException e) { + throw new EDIStreamException("", location, e); } + outputBuffer.clear(); } - private void validate(Consumer command) { - validator().ifPresent(validator -> { - errors.clear(); - command.accept(validator); - - if (!errors.isEmpty()) { - throw validationExceptionChain(errors); + void processProxyEvents() throws EDIStreamException { + errors.clear(); + EDIStreamEvent event; + + while ((event = nextEvent()) != null) { + if (EDIStreamEvent.START_INTERCHANGE == event && useInternalControlSchema()) { + try { + LOGGER.finer(() -> "Setting control schema: " + lexer.getDialect().getStandard() + ", " + lexer.getDialect().getVersion()); + this.controlSchema = SchemaUtils.getControlSchema(lexer.getDialect().getStandard(), lexer.getDialect().getVersion()); + proxy.setControlSchema(controlSchema, true); + LOGGER.finer(() -> "Done setting control schema: " + lexer.getDialect().getStandard() + ", " + lexer.getDialect().getVersion()); + } catch (EDISchemaException e) { + LOGGER.log(Level.WARNING, + String.format("Exception loading controlSchema for standard %s, version %s: %s", + getStandard(), + Arrays.stream(lexer.getDialect().getVersion()).map(Object::toString) + .collect(Collectors.joining(", ")), + e.getMessage()), + e); + } } - }); - } - private void validateCompositeOccurrence() { - validator().ifPresent(validator -> { - errors.clear(); + if (event.isError()) { + Location errLocation = proxy.getLocation().copy(); + EDIStreamValidationError error = proxy.getErrorType(); + CharSequence data = proxy.getCharacters(); //.toString(); + data = data != null ? data.toString() : null; - if (!validator.validCompositeOccurrences(dialect, location)) { - reportElementErrors(validator, ""); - } - - if (!errors.isEmpty()) { - throw validationExceptionChain(errors); + if (this.reporter != null) { + this.reporter.report(error, this, errLocation, data, proxy.getSchemaTypeReference()); + } else { + errors.add(new EDIValidationException(event, error, errLocation, data)); + } } - }); - } - - private CharSequence validateElement(Runnable setupCommand, CharSequence data) { - return validator() - .map(validator -> validateElement(setupCommand, data, validator)) - .orElse(data); - } - - CharSequence validateElement(Runnable setupCommand, CharSequence data, Validator validator) { - CharSequence elementData; - - if (this.formatElements) { - elementData = this.formattedElement; - this.formattedElement.setLength(0); - this.formattedElement.append(data); // Validator will clear and re-format if configured - } else { - elementData = data; - } - errors.clear(); - setupCommand.run(); - - if (!validator.validateElement(dialect, location, data, this.formattedElement)) { - reportElementErrors(validator, elementData); + advanceProxyQueue(); } if (!errors.isEmpty()) { throw validationExceptionChain(errors); } + } - dialect.elementData(elementData, location); - return elementData; + EDIStreamEvent nextEvent() throws EDIException { + EDIStreamEvent event = proxy.getEvent(); + + if (event == null) { + advanceProxyQueue(); + event = proxy.getEvent(); + } + + return event; } - void reportElementErrors(Validator validator, CharSequence data) { - for (UsageError error : validator.getElementErrors()) { - elementError(error.getError().getCategory(), - error.getError(), - error.getTypeReference(), - data, - location.getElementPosition(), - location.getComponentPosition(), - location.getElementOccurrence()); + void advanceProxyQueue() throws EDIException { + if (!proxy.nextEvent()) { + proxy.resetEvents(); + lexer.parse(outputBuffer); } } +// @Override +// public boolean binaryData(InputStream binary) { +// // No operation +// return true; +// } +// +// @Override +// public boolean elementData(char[] text, int start, int length) { +// if (level > LEVEL_ELEMENT) { +// location.incrementComponentPosition(); +// } else { +// location.incrementElementPosition(); +// } +// +// elementHolder.set(text, start, length); +// dialect.elementData(elementHolder, location); +// +// validator().ifPresent(validator -> { +// if (!validator.validateElement(dialect, location, elementHolder, null)) { +// reportElementErrors(validator, elementHolder); +// } +// }); +// +// return true; +// } +// +// @Override +// public void loopBegin(EDIReference typeReference) { +// final String loopCode = typeReference.getReferencedType().getCode(); +// +// if (EDIType.Type.TRANSACTION.toString().equals(loopCode)) { +// transaction = true; +// transactionSchemaAllowed = true; +// if (transactionValidator != null) { +// transactionValidator.reset(); +// } +// } +// } +// +// @Override +// public void loopEnd(EDIReference typeReference) { +// final String loopCode = typeReference.getReferencedType().getCode(); +// +// if (EDIType.Type.TRANSACTION.toString().equals(loopCode)) { +// transaction = false; +// dialect.transactionEnd(); +// } else if (EDIType.Type.GROUP.toString().equals(loopCode)) { +// dialect.groupEnd(); +// } +// } +// +// @Override +// public void elementError(EDIStreamEvent event, +// EDIStreamValidationError error, +// EDIReference typeReference, +// CharSequence data, +// int element, +// int component, +// int repetition) { +// +// StaEDIStreamLocation copy = location.copy(); +// copy.setElementPosition(element); +// copy.setElementOccurrence(repetition); +// copy.setComponentPosition(component); +// +// if (this.reporter != null) { +// this.reporter.report(error, this, copy, data, typeReference); +// } else { +// errors.add(new EDIValidationException(event, error, copy, data)); +// } +// } +// +// @Override +// public void segmentError(CharSequence token, EDIReference typeReference, EDIStreamValidationError error) { +// if (this.reporter != null) { +// this.reporter.report(error, this, this.getLocation(), token, typeReference); +// } else { +// errors.add(new EDIValidationException(EDIStreamEvent.SEGMENT_ERROR, error, location, token)); +// } +// } +// +// private void validate(Consumer command) { +// validator().ifPresent(validator -> { +// errors.clear(); +// command.accept(validator); +// +// if (!errors.isEmpty()) { +// throw validationExceptionChain(errors); +// } +// }); +// } +// +// private void validateCompositeOccurrence() { +// validator().ifPresent(validator -> { +// errors.clear(); +// +// if (!validator.validCompositeOccurrences(dialect, location)) { +// reportElementErrors(validator, ""); +// } +// +// if (!errors.isEmpty()) { +// throw validationExceptionChain(errors); +// } +// }); +// } +// +// private CharSequence validateElement(Runnable setupCommand, CharSequence data) { +// return validator() +// .map(validator -> validateElement(setupCommand, data, validator)) +// .orElse(data); +// } +// +// CharSequence validateElement(Runnable setupCommand, CharSequence data, Validator validator) { +// CharSequence elementData; +// +// if (this.formatElements) { +// elementData = this.formattedElement; +// this.formattedElement.setLength(0); +// this.formattedElement.append(data); // Validator will clear and re-format if configured +// } else { +// elementData = data; +// } +// +// errors.clear(); +// setupCommand.run(); +// +// if (!validator.validateElement(dialect, location, data, this.formattedElement)) { +// reportElementErrors(validator, elementData); +// } +// +// if (!errors.isEmpty()) { +// throw validationExceptionChain(errors); +// } +// +// dialect.elementData(elementData, location); +// return elementData; +// } +// +// void reportElementErrors(Validator validator, CharSequence data) { +// for (UsageError error : validator.getElementErrors()) { +// elementError(error.getError().getCategory(), +// error.getError(), +// error.getTypeReference(), +// data, +// location.getElementPosition(), +// location.getComponentPosition(), +// location.getElementOccurrence()); +// } +// } + EDIValidationException validationExceptionChain(List errors) { Iterator iter = errors.iterator(); EDIValidationException first = iter.next(); @@ -1014,4 +1193,12 @@ EDIValidationException validationExceptionChain(List err return first; } + + boolean useInternalControlSchema() { + if (this.controlSchema != null) { + return false; + } + + return getProperty(EDIInputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, Boolean::parseBoolean, false); + } } diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/EDIException.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/EDIException.java index 68a1be70..f1f74d48 100644 --- a/src/main/java/io/xlate/edi/internal/stream/tokenization/EDIException.java +++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/EDIException.java @@ -15,12 +15,12 @@ ******************************************************************************/ package io.xlate.edi.internal.stream.tokenization; -import io.xlate.edi.stream.EDIStreamException; -import io.xlate.edi.stream.Location; - import java.util.HashMap; import java.util.Map; +import io.xlate.edi.stream.EDIStreamException; +import io.xlate.edi.stream.Location; + public class EDIException extends EDIStreamException { private static final long serialVersionUID = -2724168743697298348L; @@ -41,7 +41,7 @@ public class EDIException extends EDIStreamException { exceptionMessages.put(INVALID_STATE, "EDIE003 - Invalid processing state"); exceptionMessages.put(INVALID_CHARACTER, - "EDIE004 - Invalid input character"); + "EDIE004 - Invalid character"); exceptionMessages.put(INCOMPLETE_STREAM, "EDIE005 - Unexpected end of stream"); } @@ -55,7 +55,7 @@ public EDIException(String message) { } EDIException(Integer id, String message, Location location) { - super(exceptionMessages.get(id) + "; " + message, location); + super(buildMessage(exceptionMessages.get(id), location) + "; " + message, location); } public EDIException(Integer id, String message) { diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/Lexer.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/Lexer.java index d15fd530..582cf8dc 100644 --- a/src/main/java/io/xlate/edi/internal/stream/tokenization/Lexer.java +++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/Lexer.java @@ -45,9 +45,10 @@ private enum Mode { } private final Deque modes = new ArrayDeque<>(); - private int input = 0; private State state = State.INITIAL; + private int previousInput = 0; private State previous; + private boolean dataEventNotified = false; private interface Notifier { boolean execute(State state, int start, int length); @@ -107,6 +108,10 @@ public Lexer(InputStream stream, Charset charset, EventHandler handler, StaEDISt }; ssn = (notifyState, start, length) -> { + if (dataEventNotified) { + dataEventNotified = false; + return false; + } String segmentTag = new String(buffer.array(), start, length); return handler.segmentBegin(segmentTag); }; @@ -117,6 +122,10 @@ public Lexer(InputStream stream, Charset charset, EventHandler handler, StaEDISt bn = (notifyState, start, length) -> handler.binaryData(binaryStream); en = (notifyState, start, length) -> { + if (dataEventNotified) { + dataEventNotified = false; + return false; + } elementHolder.set(buffer.array(), start, length); return handler.elementData(elementHolder, true); }; @@ -127,6 +136,14 @@ public Dialect getDialect() { return dialect; } + public StaEDIStreamLocation getLocation() { + return location; + } + + public CharacterSet getCharacterSet() { + return characters; + } + public void invalidate() { if (state != State.INVALID) { previous = state; @@ -171,141 +188,188 @@ public boolean hasRemaining() throws IOException { public void parse() throws IOException, EDIException { try { - parse(this::readCharacterUnchecked); + parse(this::readCharacterUnchecked, false); } catch (UncheckedIOException e) { throw e.getCause(); } } - void parse(IntSupplier inputSource) throws EDIException { + public void parse(CharBuffer buffer) throws EDIException { + IntSupplier inputSource = () -> buffer.hasRemaining() ? buffer.get() : -1; + parse(inputSource, true); + } + + public void signalElementDataCompleteEvent(int delimiter) throws EDIException { + CharacterClass clazz = characters.getClass(delimiter); + State dataCompleteState = State.transition(state, dialect, clazz); + + switch (dataCompleteState) { + case SEGMENT_BEGIN: + case TRAILER_BEGIN: + openSegment(); + nextEvent(); + dataEventNotified = true; + break; + case COMPONENT_END: + handleComponent(); + nextEvent(); + dataEventNotified = true; + break; + case ELEMENT_END: + case TRAILER_ELEMENT_END: + handleElement(); + nextEvent(); + dataEventNotified = true; + break; + default: + throw new IllegalStateException(dataCompleteState.toString()); + } + } + + void parse(IntSupplier inputSource, boolean allowPartialStream) throws EDIException { if (nextEvent()) { return; } if (state == State.INVALID) { // Unable to proceed once the state becomes invalid - throw invalidStateError(); + throw invalidStateError(previousInput); } + int input = 0; boolean eventsReady = false; while (!eventsReady && (input = inputSource.getAsInt()) > -1) { - location.incrementOffset(input); + eventsReady = processInputCharacter(input); + } - CharacterClass clazz = characters.getClass(input); - previous = state; - state = State.transition(state, dialect, clazz); - LOGGER.finer(() -> String.format("%s + (%s, '%s', %s) -> %s", previous, Dialect.getStandard(dialect), (char) input, clazz, state)); - - switch (state) { - case INITIAL: - case TAG_SEARCH: - case HEADER_EDIFACT_UNB_SEARCH: - break; - case HEADER_X12_I: - case HEADER_X12_S: - case HEADER_EDIFACT_N: - case HEADER_EDIFACT_U: - case HEADER_TRADACOMS_S: - case HEADER_TRADACOMS_T: - case TAG_1: - case TAG_2: - case TAG_3: - case TRAILER_X12_I: - case TRAILER_X12_E: - case TRAILER_X12_A: - case TRAILER_EDIFACT_U: - case TRAILER_EDIFACT_N: - case TRAILER_EDIFACT_Z: - case TRAILER_TRADACOMS_E: - case TRAILER_TRADACOMS_N: - case TRAILER_TRADACOMS_D: - case ELEMENT_DATA: - case TRAILER_ELEMENT_DATA: + if (input < 0 && !allowPartialStream) { + throw error(EDIException.INCOMPLETE_STREAM); + } + } + + boolean processInputCharacter(int input) throws EDIException { + boolean eventsReady = false; + location.incrementOffset(input); + + CharacterClass clazz = characters.getClass(input); + previous = state; + previousInput = input; + + state = State.transition(state, dialect, clazz); + LOGGER.finer(() -> String.format("%s + (%s, '%s', %s) -> %s", previous, Dialect.getStandard(dialect), (char) input, clazz, state)); + + switch (state) { + case INITIAL: + case TAG_SEARCH: + case HEADER_EDIFACT_UNB_SEARCH: + break; + case HEADER_X12_I: + case HEADER_X12_S: + case HEADER_EDIFACT_N: + case HEADER_EDIFACT_U: + case HEADER_TRADACOMS_S: + case HEADER_TRADACOMS_T: + case TAG_1: + case TAG_2: + case TAG_3: + case TRAILER_X12_I: + case TRAILER_X12_E: + case TRAILER_X12_A: + case TRAILER_EDIFACT_U: + case TRAILER_EDIFACT_N: + case TRAILER_EDIFACT_Z: + case TRAILER_TRADACOMS_E: + case TRAILER_TRADACOMS_N: + case TRAILER_TRADACOMS_D: + case ELEMENT_DATA: + case TRAILER_ELEMENT_DATA: + buffer.put((char) input); + break; + case ELEMENT_INVALID_DATA: + if (!characters.isIgnored(input)) { buffer.put((char) input); - break; - case ELEMENT_INVALID_DATA: - if (!characters.isIgnored(input)) { - buffer.put((char) input); - } - break; - case HEADER_EDIFACT_UNB_1: // U - When UNA is present - case HEADER_EDIFACT_UNB_2: // N - When UNA is present - case HEADER_EDIFACT_UNB_3: // B - When UNA is present - handleStateHeaderTag(input); - break; - case HEADER_RELEASE: - case DATA_RELEASE: - // Skip this character - next character will be literal value - break; - case ELEMENT_DATA_BINARY: - handleStateElementDataBinary(); - break; - case INTERCHANGE_CANDIDATE: - // ISA, UNA, or UNB was found - handleStateInterchangeCandidate(input); - break; - case HEADER_DATA: - case HEADER_INVALID_DATA: - handleStateHeaderData(input); - eventsReady = dialectConfirmed(State.TAG_SEARCH); - break; - case HEADER_SEGMENT_BEGIN: - dialect.appendHeader(characters, (char) input); - openSegment(); - eventsReady = dialectConfirmed(State.ELEMENT_END); - break; - case HEADER_ELEMENT_END: - dialect.appendHeader(characters, (char) input); - handleElement(); - eventsReady = dialectConfirmed(State.ELEMENT_END); - break; - case HEADER_COMPONENT_END: - dialect.appendHeader(characters, (char) input); - handleComponent(); - eventsReady = dialectConfirmed(State.COMPONENT_END); - break; - case SEGMENT_BEGIN: - case TRAILER_BEGIN: - openSegment(); - eventsReady = nextEvent(); - break; - case SEGMENT_END: - closeSegment(); - eventsReady = nextEvent(); - break; - case SEGMENT_EMPTY: - emptySegment(); - eventsReady = nextEvent(); - break; - case COMPONENT_END: - handleComponent(); - eventsReady = nextEvent(); - break; - case ELEMENT_END: - case TRAILER_ELEMENT_END: - case ELEMENT_REPEAT: - handleElement(); - eventsReady = nextEvent(); - break; - case INTERCHANGE_END: - closeInterchange(); - eventsReady = nextEvent(); - break; - default: - if (characters.isIgnored(input)) { - state = previous; - } else if (clazz != CharacterClass.INVALID) { - throw invalidStateError(); - } else { - throw error(EDIException.INVALID_CHARACTER); - } + } + break; + case HEADER_EDIFACT_UNB_1: // U - When UNA is present + case HEADER_EDIFACT_UNB_2: // N - When UNA is present + case HEADER_EDIFACT_UNB_3: // B - When UNA is present + handleStateHeaderTag(input); + break; + case HEADER_RELEASE: + case DATA_RELEASE: + // Skip this character - next character will be literal value + break; + case ELEMENT_DATA_BINARY: + handleStateElementDataBinary(); + break; + case INTERCHANGE_CANDIDATE: + // ISA, UNA, or UNB was found + handleStateInterchangeCandidate(input); + break; + case HEADER_DATA: + case HEADER_INVALID_DATA: + handleStateHeaderData(input); + eventsReady = dialectConfirmed(State.TAG_SEARCH); + break; + case HEADER_SEGMENT_BEGIN: + dialect.appendHeader(characters, (char) input); + openSegment(); + eventsReady = dialectConfirmed(State.ELEMENT_END); + break; + case HEADER_SEGMENT_END: + dialect.appendHeader(characters, (char) input); + closeSegment(); + eventsReady = dialectConfirmed(State.SEGMENT_END); + break; + case HEADER_ELEMENT_END: + dialect.appendHeader(characters, (char) input); + handleElement(); + eventsReady = dialectConfirmed(State.ELEMENT_END); + break; + case HEADER_COMPONENT_END: + dialect.appendHeader(characters, (char) input); + handleComponent(); + eventsReady = dialectConfirmed(State.COMPONENT_END); + break; + case SEGMENT_BEGIN: + case TRAILER_BEGIN: + openSegment(); + eventsReady = nextEvent(); + break; + case SEGMENT_END: + closeSegment(); + eventsReady = nextEvent(); + break; + case SEGMENT_EMPTY: + emptySegment(); + eventsReady = nextEvent(); + break; + case COMPONENT_END: + handleComponent(); + eventsReady = nextEvent(); + break; + case ELEMENT_END: + case TRAILER_ELEMENT_END: + case ELEMENT_REPEAT: + handleElement(); + eventsReady = nextEvent(); + break; + case INTERCHANGE_END: + closeInterchange(); + eventsReady = nextEvent(); + break; + default: + if (characters.isIgnored(input)) { + state = previous; + } else if (clazz != CharacterClass.INVALID) { + throw invalidStateError(input); + } else { + throw error(EDIException.INVALID_CHARACTER); } } - if (input < 0) { - throw error(EDIException.INCOMPLETE_STREAM); - } + return eventsReady; } int readCharacterUnchecked() { @@ -445,7 +509,7 @@ private boolean dialectConfirmed(State confirmed) throws EDIException { return false; } - private EDIException invalidStateError() { + private EDIException invalidStateError(int input) { StringBuilder message = new StringBuilder(); message.append(state); message.append(" (previous: "); @@ -476,10 +540,10 @@ private boolean nextEvent() { int start = startQueue.remove(); int length = lengthQueue.remove(); eventsReady = event.execute(nextState, start, length); - } - if (events.isEmpty()) { - buffer.clear(); + if (events.isEmpty()) { + buffer.clear(); + } } return eventsReady; @@ -522,7 +586,9 @@ private void closeInterchange() throws EDIException { } private void openSegment() { - modes.push(Mode.SEGMENT); + if (modes.peek() != Mode.SEGMENT) { + modes.push(Mode.SEGMENT); + } enqueue(ssn, buffer.position()); } 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 0238d550..caa455fe 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 @@ -153,7 +153,7 @@ public String getReferenceCode() { return current(StreamEvent::getReferenceCode, null); } - public Location getLocation() { + public StaEDIStreamLocation getLocation() { return current(StreamEvent::getLocation, this.location); } @@ -388,6 +388,7 @@ public boolean elementData(CharSequence text, boolean fromStream) { * and the composite begin/end events must be generated. **/ final boolean componentReceivedAsSimple; + final List errors; if (validator != null) { derivedComposite = !compositeFromStream && validator.isComposite(dialect, location); @@ -400,15 +401,17 @@ public boolean elementData(CharSequence text, boolean fromStream) { valid = validator.validateElement(dialect, location, text, null); typeReference = validator.getElementReference(); + errors = validator.getElementErrors(); enqueueElementOccurrenceErrors(text, validator, valid); } else { - valid = true; + errors = Validator.validateCharacters(text); + valid = errors.isEmpty(); derivedComposite = false; componentReceivedAsSimple = false; typeReference = null; } - enqueueElementErrors(text, validator, valid); + enqueueElementErrors(text, errors, valid); boolean eventsReady = true; @@ -532,13 +535,11 @@ void enqueueElementOccurrenceErrors(CharSequence text, Validator validator, bool } } - void enqueueElementErrors(CharSequence text, Validator validator, boolean valid) { + void enqueueElementErrors(CharSequence text, List errors, boolean valid) { if (valid) { return; } - List errors = validator.getElementErrors(); - for (UsageError error : errors) { enqueueEvent(error.getError().getCategory(), error.getError(), diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/StreamEvent.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/StreamEvent.java index 812b7fb3..13050efc 100644 --- a/src/main/java/io/xlate/edi/internal/stream/tokenization/StreamEvent.java +++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/StreamEvent.java @@ -70,7 +70,7 @@ public void setTypeReference(EDIReference typeReference) { this.typeReference = typeReference; } - public Location getLocation() { + public StaEDIStreamLocation getLocation() { return location; } diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/AlphaNumericValidator.java b/src/main/java/io/xlate/edi/internal/stream/validation/AlphaNumericValidator.java index f94a8b80..ff7523a1 100644 --- a/src/main/java/io/xlate/edi/internal/stream/validation/AlphaNumericValidator.java +++ b/src/main/java/io/xlate/edi/internal/stream/validation/AlphaNumericValidator.java @@ -45,18 +45,30 @@ void validate(Dialect dialect, Set valueSet = element.getValueSet(dialect.getTransactionVersionString()); - if (!valueSet.isEmpty() && !valueSet.contains(value.toString())) { - errors.add(EDIStreamValidationError.INVALID_CODE_VALUE); + if (valueSet.isEmpty() || valueSet.contains(value.toString())) { + if (valueSet.isEmpty()) { + // Only validate the characters if not explicitly listed in the set of allowed values + validateCharacters(value, length, errors); + } } else { - for (int i = 0; i < length; i++) { - char character = value.charAt(i); + validateCharacters(value, length, errors); + errors.add(EDIStreamValidationError.INVALID_CODE_VALUE); + } + } - if (!CharacterSet.isValid(character)) { - errors.add(EDIStreamValidationError.INVALID_CHARACTER_DATA); - break; - } + static void validateCharacters(CharSequence value, int length, List errors) { + if (!validCharacters(value, length)) { + errors.add(EDIStreamValidationError.INVALID_CHARACTER_DATA); + } + } + + static boolean validCharacters(CharSequence value, int length) { + for (int i = 0; i < length; i++) { + if (!CharacterSet.isValid(value.charAt(i))) { + return false; } } + return true; } @Override 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 e0ad7f89..272e4f0e 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 @@ -1263,6 +1263,13 @@ void validateElementValue(Dialect dialect, StaEDIStreamLocation position, UsageN } } + public static List validateCharacters(CharSequence value) { + if (AlphaNumericValidator.validCharacters(value, value.length())) { + return Collections.emptyList(); + } + return Collections.singletonList(new UsageError(EDIStreamValidationError.INVALID_CHARACTER_DATA)); + } + void validateControlValue(UsageNode loop, StaEDIStreamLocation position, CharSequence value, List errors) { if (loop instanceof ControlUsageNode) { ((ControlUsageNode) loop).validateReference(position, value, errors); diff --git a/src/main/java/io/xlate/edi/stream/EDIInputFactory.java b/src/main/java/io/xlate/edi/stream/EDIInputFactory.java index f074c429..b1e996a1 100644 --- a/src/main/java/io/xlate/edi/stream/EDIInputFactory.java +++ b/src/main/java/io/xlate/edi/stream/EDIInputFactory.java @@ -24,6 +24,19 @@ public abstract class EDIInputFactory extends PropertySupport { + /** + * When set to false, control structures, segments, elements, and codes will + * not be validated unless a user-provided control schema has been set using + * {@link EDIStreamReader#setControlSchema(Schema)}. + * + * When set to true AND no user-provided control schema has been set, the + * reader will attempt to find a known control schema for the detected EDI + * dialect and version to be used for control structure validation. + * + * Default value: true + * + * @see EDIOutputFactory#EDI_VALIDATE_CONTROL_STRUCTURE + */ public static final String EDI_VALIDATE_CONTROL_STRUCTURE = "io.xlate.edi.stream.EDI_VALIDATE_CONTROL_STRUCTURE"; /** diff --git a/src/main/java/io/xlate/edi/stream/EDIOutputFactory.java b/src/main/java/io/xlate/edi/stream/EDIOutputFactory.java index b1b1635a..230de65c 100644 --- a/src/main/java/io/xlate/edi/stream/EDIOutputFactory.java +++ b/src/main/java/io/xlate/edi/stream/EDIOutputFactory.java @@ -19,6 +19,8 @@ import javax.xml.stream.XMLStreamWriter; +import io.xlate.edi.schema.Schema; + public abstract class EDIOutputFactory extends PropertySupport { /** @@ -54,6 +56,23 @@ public abstract class EDIOutputFactory extends PropertySupport { */ public static final String FORMAT_ELEMENTS = "io.xlate.edi.stream.FORMAT_ELEMENTS"; + /** + * When set to false, control structures, segments, elements, and codes will + * not be validated unless a user-provided control schema has been set using + * {@link EDIStreamWriter#setControlSchema(Schema)}. + * + * When set to true AND no user-provided control schema has been set, the + * writer will attempt to find a known control schema for the detected EDI + * dialect and version to be used for control structure validation. + * + * Default value: false + * + * @see EDIInputFactory#EDI_VALIDATE_CONTROL_STRUCTURE + * + * @since 2.0 + */ + public static final String EDI_VALIDATE_CONTROL_STRUCTURE = "io.xlate.edi.stream.EDI_VALIDATE_CONTROL_STRUCTURE"; + /** * Create a new instance of the factory. This static method creates a new * factory instance. diff --git a/src/main/java/io/xlate/edi/stream/EDIStreamException.java b/src/main/java/io/xlate/edi/stream/EDIStreamException.java index c1ae9172..d4ea71bb 100644 --- a/src/main/java/io/xlate/edi/stream/EDIStreamException.java +++ b/src/main/java/io/xlate/edi/stream/EDIStreamException.java @@ -21,6 +21,14 @@ public class EDIStreamException extends Exception { protected final transient Location location; + protected static String buildMessage(String message, Location location) { + String locationString = location.toString(); + if (message.contains(locationString)) { + return message; + } + return message + " " + locationString; + } + /** * Construct an exception with the associated message. * @@ -58,7 +66,7 @@ public EDIStreamException(Throwable cause) { * a nested error / exception */ public EDIStreamException(String message, Location location, Throwable cause) { - super(message + " " + location.toString(), cause); + super(buildMessage(message, location), cause); this.location = location; } @@ -72,7 +80,7 @@ public EDIStreamException(String message, Location location, Throwable cause) { * the location of the error */ public EDIStreamException(String message, Location location) { - super(message + " " + location.toString()); + super(buildMessage(message, location)); this.location = location; } diff --git a/src/main/java/io/xlate/edi/stream/EDIValidationException.java b/src/main/java/io/xlate/edi/stream/EDIValidationException.java index 7563ae29..dfce3048 100644 --- a/src/main/java/io/xlate/edi/stream/EDIValidationException.java +++ b/src/main/java/io/xlate/edi/stream/EDIValidationException.java @@ -16,7 +16,7 @@ public EDIValidationException(EDIStreamEvent event, EDIStreamValidationError error, Location location, CharSequence data) { - super("Encountered " + event + " [" + error + "]" + (location != null ? " " + location.toString() : "")); + super("Encountered " + event + " [" + error + "] for data={" + data + "} " + (location != null ? " " + location.toString() : "")); this.event = event; this.error = error; this.location = location != null ? location.copy() : null; 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 c965b5d7..98606eee 100644 --- a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamWriterTest.java +++ b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamWriterTest.java @@ -39,6 +39,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -92,9 +93,10 @@ private void writeHeader(EDIStreamWriter writer) throws EDIStreamException { static void unconfirmedBufferEquals(String expected, EDIStreamWriter writer) { StaEDIStreamWriter writerImpl = (StaEDIStreamWriter) writer; - writerImpl.unconfirmedBuffer.mark(); - writerImpl.unconfirmedBuffer.flip(); - assertEquals(expected, writerImpl.unconfirmedBuffer.toString()); + if (writerImpl.outputBuffer.position() > 0) { + writerImpl.outputBuffer.flip(); + } + assertEquals(expected, writerImpl.outputBuffer.toString()); } @Test @@ -185,7 +187,6 @@ void testWriteStartSegment() throws EDIStreamException { EDIStreamWriter writer = factory.createEDIStreamWriter(stream); writer.startInterchange(); writer.writeStartSegment("ISA"); - writer.flush(); unconfirmedBufferEquals("ISA", writer); } @@ -207,8 +208,9 @@ void testWriteInvalidHeaderElement() throws EDIStreamException { writer.writeElement("508121953"); writer.writeElement("0"); writer.writeElement("P"); - EDIStreamException thrown = assertThrows(EDIStreamException.class, () -> writer.writeElement(":")); - assertEquals("Failed writing X12 header: Element delimiter '*' required in position 18 of X12 header but not found", thrown.getMessage()); + writer.writeElement(":"); + EDIStreamException thrown = assertThrows(EDIStreamException.class, () -> writer.writeEndSegment()); + assertEquals("EDIE003 - Invalid processing state at offset 106; Element delimiter '*' required in position 18 of X12 header but not found", thrown.getMessage()); } @Test @@ -287,14 +289,27 @@ void testWriteStartElementIllegal() throws EDIStreamException { @Test void testWriteInvalidCharacter() throws EDIStreamException { EDIOutputFactory factory = EDIOutputFactory.newFactory(); + factory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); OutputStream stream = new ByteArrayOutputStream(4096); EDIStreamWriter writer = factory.createEDIStreamWriter(stream); writer.startInterchange(); - writer.writeStartSegment("ISA"); - writer.writeStartElement(); - EDIStreamException thrown = assertThrows(EDIStreamException.class, - () -> writer.writeElementData("\u0008\u0010")); - assertEquals("Invalid character: 0x0008 in segment ISA at position 1, element 1", thrown.getMessage()); + writeHeader(writer); + writer.writeStartSegment("GS"); + EDIValidationException thrown = assertThrows(EDIValidationException.class, + () -> writer.writeElement("\u0008\u0010")); + + assertEquals(EDIStreamEvent.ELEMENT_DATA_ERROR, thrown.getEvent()); + assertEquals(EDIStreamValidationError.INVALID_CHARACTER_DATA, thrown.getError()); + assertEquals("\u0008\u0010", thrown.getData().toString()); + assertEquals(2, thrown.getLocation().getSegmentPosition()); + assertEquals(1, thrown.getLocation().getElementPosition()); + + assertNotNull(thrown = thrown.getNextException()); + assertEquals(EDIStreamEvent.ELEMENT_DATA_ERROR, thrown.getEvent()); + assertEquals(EDIStreamValidationError.INVALID_CODE_VALUE, thrown.getError()); + assertEquals("\u0008\u0010", thrown.getData().toString()); + assertEquals(2, thrown.getLocation().getSegmentPosition()); + assertEquals(1, thrown.getLocation().getElementPosition()); } @Test @@ -309,9 +324,12 @@ void testWriteInvalidCharacterRepeatedComposite() throws EDIStreamException { writer.writeRepeatElement(); // starts new element writer.writeComponent("BAR2"); writer.writeComponent("BAR3"); - EDIStreamException thrown = assertThrows(EDIStreamException.class, + EDIValidationException thrown = assertThrows(EDIValidationException.class, () -> writer.writeComponent("\u0008\u0010")); - assertEquals("Invalid character: 0x0008 in segment FOO at position 2, element 1 (occurrence 2), component 3", thrown.getMessage()); + + assertEquals(EDIStreamEvent.ELEMENT_DATA_ERROR, thrown.getEvent()); + assertEquals(EDIStreamValidationError.INVALID_CHARACTER_DATA, thrown.getError()); + assertEquals("\u0008\u0010", thrown.getData().toString()); Location l = thrown.getLocation(); assertEquals("FOO", l.getSegmentTag()); assertEquals(2, l.getSegmentPosition()); @@ -328,14 +346,15 @@ void testWriteInvalidSegmentTag() throws EDIStreamException { writer.startInterchange(); writeHeader(writer); writer.writeStartSegment("G"); - EDIStreamException thrown = assertThrows(EDIStreamException.class, - () -> writer.writeElement("FOO")); - assertEquals("Invalid state: INVALID; output 0x002A", thrown.getMessage()); + // Illegal transition from segment tag position 1 to element delimiter + assertThrows(EDIStreamException.class, () -> writer.writeElement("FOO")); } @Test void testWriteStartElementBinary() throws IllegalStateException, EDIStreamException { EDIOutputFactory factory = EDIOutputFactory.newFactory(); + factory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); + ListErrorReporter reporter = setErrorReporter(factory); ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); EDIStreamWriter writer = factory.createEDIStreamWriter(stream); writer.startInterchange(); @@ -346,6 +365,8 @@ void testWriteStartElementBinary() throws IllegalStateException, EDIStreamExcept writer.writeStartElementBinary().writeEndSegment(); writer.flush(); assertEquals("BIN*~", stream.toString()); + assertEquals(1, reporter.errors.size()); + assertTrue(reporter.hasError(0, EDIStreamValidationError.SEGMENT_NOT_IN_DEFINED_TRANSACTION_SET, "BIN")); } @Test @@ -374,6 +395,8 @@ void testWriteBinaryDataIllegal() throws IllegalStateException, EDIStreamExcepti @Test void testStartComponentIllegalInElementBinary() throws IllegalStateException, EDIStreamException { EDIOutputFactory factory = EDIOutputFactory.newFactory(); + factory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); + ListErrorReporter reporter = setErrorReporter(factory); ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); EDIStreamWriter writer = factory.createEDIStreamWriter(stream); writer.startInterchange(); @@ -383,6 +406,7 @@ void testStartComponentIllegalInElementBinary() throws IllegalStateException, ED writer.writeStartSegment("BIN"); writer.writeStartElementBinary(); assertThrows(IllegalStateException.class, () -> writer.startComponent()); + assertTrue(reporter.hasError(0, EDIStreamValidationError.SEGMENT_NOT_IN_DEFINED_TRANSACTION_SET, "BIN")); } @Test @@ -437,6 +461,8 @@ void testComponentIllegal() throws IllegalStateException, EDIStreamException { @Test void testWriteRepeatElement() throws EDIStreamException { EDIOutputFactory factory = EDIOutputFactory.newFactory(); + factory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); + ListErrorReporter reporter = setErrorReporter(factory); ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); EDIStreamWriter writer = factory.createEDIStreamWriter(stream); writer.startInterchange(); @@ -451,6 +477,7 @@ void testWriteRepeatElement() throws EDIStreamException { .writeEndSegment(); writer.flush(); assertEquals("SEG*R1^R2~", stream.toString()); + assertTrue(reporter.hasError(0, EDIStreamValidationError.SEGMENT_NOT_IN_DEFINED_TRANSACTION_SET, "SEG")); } @Test @@ -575,8 +602,6 @@ void testWriteElementDataCharSequence() throws EDIStreamException { writer.writeStartElement(); writer.writeElementData("TEST-ELEMENT"); assertThrows(EDIStreamException.class, () -> writer.writeEndSegment()); - writer.flush(); - unconfirmedBufferEquals("ISA*TEST-ELEMENT~", writer); } @Test @@ -635,6 +660,8 @@ void testWriteElementDataCharArrayIllegal() throws EDIStreamException { @Test void testWriteBinaryDataInputStream() throws EDIStreamException { EDIOutputFactory factory = EDIOutputFactory.newFactory(); + factory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); + ListErrorReporter reporter = setErrorReporter(factory); ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); EDIStreamWriter writer = factory.createEDIStreamWriter(stream); byte[] binary = { '\n', 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, '\t' }; @@ -652,12 +679,15 @@ void testWriteBinaryDataInputStream() throws EDIStreamException { writer.endElement(); writer.writeEndSegment(); writer.flush(); + assertTrue(reporter.hasError(0, EDIStreamValidationError.SEGMENT_NOT_IN_DEFINED_TRANSACTION_SET, "BIN")); assertEquals("BIN*8*\n\u0000\u0001\u0002\u0003\u0004\u0005\t~", stream.toString()); } @Test void testWriteBinaryDataInputStreamIOException() throws Exception { EDIOutputFactory factory = EDIOutputFactory.newFactory(); + factory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); + ListErrorReporter reporter = setErrorReporter(factory); ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); EDIStreamWriter writer = factory.createEDIStreamWriter(stream); InputStream binaryStream = Mockito.mock(InputStream.class); @@ -676,11 +706,14 @@ void testWriteBinaryDataInputStreamIOException() throws Exception { assertEquals("Exception writing binary element data in segment BIN at position 2, element 2", thrown.getMessage()); assertSame(ioException, thrown.getCause()); + assertTrue(reporter.hasError(0, EDIStreamValidationError.SEGMENT_NOT_IN_DEFINED_TRANSACTION_SET, "BIN")); } @Test void testWriteBinaryDataByteArray() throws EDIStreamException { EDIOutputFactory factory = EDIOutputFactory.newFactory(); + factory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); + ListErrorReporter reporter = setErrorReporter(factory); ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); EDIStreamWriter writer = factory.createEDIStreamWriter(stream); byte[] binary = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A' }; @@ -698,13 +731,17 @@ void testWriteBinaryDataByteArray() throws EDIStreamException { writer.writeEndSegment(); writer.flush(); assertEquals("BIN*11*0123456789A~", stream.toString()); + assertTrue(reporter.hasError(0, EDIStreamValidationError.SEGMENT_NOT_IN_DEFINED_TRANSACTION_SET, "BIN")); } @Test void testWriteBinaryDataByteBuffer() throws EDIStreamException { EDIOutputFactory factory = EDIOutputFactory.newFactory(); + factory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); + ListErrorReporter reporter = setErrorReporter(factory); ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); EDIStreamWriter writer = factory.createEDIStreamWriter(stream); + byte[] binary = { 'B', 'U', 'S', 'T', 'M', 'Y', 'B', 'U', 'F', 'F', 'E', 'R', 'S', '\n' }; ByteBuffer buffer = ByteBuffer.wrap(binary); writer.startInterchange(); @@ -721,6 +758,8 @@ void testWriteBinaryDataByteBuffer() throws EDIStreamException { writer.writeEndSegment(); writer.flush(); assertEquals("BIN*14*BUSTMYBUFFERS\n~", stream.toString()); + assertEquals(1, reporter.errors.size()); + assertTrue(reporter.hasError(0, EDIStreamValidationError.SEGMENT_NOT_IN_DEFINED_TRANSACTION_SET, "BIN")); } @ParameterizedTest @@ -998,7 +1037,9 @@ public int read() throws IOException { EDIOutputFactory outputFactory = EDIOutputFactory.newFactory(); outputFactory.setProperty(EDIOutputFactory.PRETTY_PRINT, true); + outputFactory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); outputFactory.setProperty(Delimiters.REPETITION, '*'); + ListErrorReporter reporter = setErrorReporter(outputFactory); ByteArrayOutputStream result = new ByteArrayOutputStream(16384); EDIStreamWriter writer = null; @@ -1080,6 +1121,7 @@ public int read() throws IOException { } assertEquals(normalizeLines(expected.toString().trim()), normalizeLines(result.toString().trim())); + assertTrue(reporter.hasError(0, EDIStreamValidationError.INVALID_CODE_VALUE, "IATA")); } @Test @@ -1198,12 +1240,14 @@ void testValidatedSegmentTagsExceptionThrown() throws EDISchemaException, EDIStr writer.startInterchange(); writeHeader(writer); - EDIValidationException e = assertThrows(EDIValidationException.class, () -> writer.writeStartSegment("ST")); + writer.writeStartSegment("ST"); + EDIValidationException e = assertThrows(EDIValidationException.class, () -> writer.writeStartElement()); assertEquals(EDIStreamEvent.SEGMENT_ERROR, e.getEvent()); assertEquals(EDIStreamValidationError.LOOP_OCCURS_OVER_MAXIMUM_TIMES, e.getError()); assertEquals("ST", e.getData().toString()); assertEquals("ST", e.getLocation().getSegmentTag()); assertEquals(2, e.getLocation().getSegmentPosition()); + assertEquals(-1, e.getLocation().getElementPosition()); } @Test @@ -1223,12 +1267,15 @@ void testValidatedSegmentTagsReporterInvoked() throws EDISchemaException, EDIStr writer.startInterchange(); writeHeader(writer); - writer.writeStartSegment("ST"); + writer.writeStartSegment("ST") + .writeElement("000") + .writeElement("0001") + .writeEndSegment(); assertEquals(5, actual.size()); assertEquals(EDIStreamEvent.SEGMENT_ERROR, actual.get(0)); assertEquals(EDIStreamValidationError.LOOP_OCCURS_OVER_MAXIMUM_TIMES, actual.get(1)); assertEquals("in segment ST at position 2", actual.get(2)); - assertEquals("ST", actual.get(3)); + assertEquals("ST", actual.get(3).toString()); assertEquals("TRANSACTION", ((EDIReference) actual.get(4)).getReferencedType().getCode()); } @@ -1240,19 +1287,21 @@ void testElementValidationReporterInvoked() throws EDISchemaException, EDIStream }; EDIOutputFactory outputFactory = EDIOutputFactory.newFactory(); outputFactory.setProperty(EDIOutputFactory.PRETTY_PRINT, true); + outputFactory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); outputFactory.setErrorReporter(reporter); assertSame(reporter, outputFactory.getErrorReporter()); ByteArrayOutputStream result = new ByteArrayOutputStream(16384); EDIStreamWriter writer = outputFactory.createEDIStreamWriter(result); - Schema control = SchemaUtils.getControlSchema("X12", new String[] { "00501" }); - writer.setControlSchema(control); + //Schema control = SchemaUtils.getControlSchema("X12", new String[] { "00501" }); + //writer.setControlSchema(control); writer.startInterchange(); writeHeader(writer); writer.writeStartSegment("GS"); writer.writeElement("AAA"); + writer.writeElement("SENDERID"); assertEquals(2, actual.size()); List e1 = actual.get(0); @@ -1289,14 +1338,14 @@ void testElementValidationThrown() throws EDISchemaException, EDIStreamException assertEquals(EDIStreamEvent.ELEMENT_DATA_ERROR, e.getEvent()); assertEquals(EDIStreamValidationError.DATA_ELEMENT_TOO_LONG, e.getError()); - //assertEquals("AAA", e.getData().toString()); + assertEquals("AAA", e.getData().toString()); assertEquals(2, e.getLocation().getSegmentPosition()); assertEquals(1, e.getLocation().getElementPosition()); assertNotNull(e = e.getNextException()); assertEquals(EDIStreamEvent.ELEMENT_DATA_ERROR, e.getEvent()); assertEquals(EDIStreamValidationError.INVALID_CODE_VALUE, e.getError()); - //assertEquals("AAA", e.getData().toString()); + assertEquals("AAA", e.getData().toString()); assertEquals(2, e.getLocation().getSegmentPosition()); assertEquals(1, e.getLocation().getElementPosition()); } @@ -1445,16 +1494,16 @@ public int read() throws IOException { assertEquals(reader.getLocation().getSegmentPosition(), writer.getLocation().getSegmentPosition(), - () -> "Segment position mismatch at " + reader.getLocation()); + () -> "Position mismatch for event " + reader.getEventType() + ", reader " + reader.getLocation() + ", writer " + writer.getLocation()); assertEquals(reader.getLocation().getElementPosition(), writer.getLocation().getElementPosition(), - () -> "Element position mismatch at " + reader.getLocation()); + () -> "Position mismatch for event " + reader.getEventType() + ", reader " + reader.getLocation() + ", writer " + writer.getLocation()); assertEquals(reader.getLocation().getElementOccurrence(), writer.getLocation().getElementOccurrence(), - () -> "Element occurrence mismatch at " + reader.getLocation()); + () -> "Position mismatch for event " + reader.getEventType() + ", reader " + reader.getLocation() + ", writer " + writer.getLocation()); assertEquals(reader.getLocation().getComponentPosition(), writer.getLocation().getComponentPosition(), - () -> "Component position mismatch at " + reader.getLocation()); + () -> "Position mismatch for event " + reader.getEventType() + ", reader " + reader.getLocation() + ", writer " + writer.getLocation()); } } finally { reader.close(); @@ -1492,7 +1541,7 @@ public int read() throws IOException { EDIOutputFactory outputFactory = EDIOutputFactory.newFactory(); outputFactory.setProperty(EDIOutputFactory.PRETTY_PRINT, true); ByteArrayOutputStream result = new ByteArrayOutputStream(16384); - EDIStreamWriter writer = null; + EDIStreamWriter writer = outputFactory.createEDIStreamWriter(result); EDIStreamEvent event; String tag = null; @@ -1512,7 +1561,6 @@ public int read() throws IOException { for (Map.Entry delim : reader.getDelimiters().entrySet()) { outputFactory.setProperty(delim.getKey(), delim.getValue()); } - writer = outputFactory.createEDIStreamWriter(result); writer.setControlSchema(control); writer.startInterchange(); break; @@ -1631,16 +1679,16 @@ public int read() throws IOException { assertEquals(reader.getLocation().getSegmentPosition(), writer.getLocation().getSegmentPosition(), - () -> "Segment position mismatch"); + () -> "Position mismatch for event " + reader.getEventType() + ", reader " + reader.getLocation() + ", writer " + writer.getLocation()); assertEquals(reader.getLocation().getElementPosition(), writer.getLocation().getElementPosition(), - () -> "Element position mismatch"); + () -> "Position mismatch for event " + reader.getEventType() + ", reader " + reader.getLocation() + ", writer " + writer.getLocation()); assertEquals(reader.getLocation().getElementOccurrence(), writer.getLocation().getElementOccurrence(), - () -> "Element occurrence mismatch"); + () -> "Position mismatch for event " + reader.getEventType() + ", reader " + reader.getLocation() + ", writer " + writer.getLocation()); assertEquals(reader.getLocation().getComponentPosition(), writer.getLocation().getComponentPosition(), - () -> "Component position mismatch"); + () -> "Position mismatch for event " + reader.getEventType() + ", reader " + reader.getLocation() + ", writer " + writer.getLocation()); } } finally { reader.close(); @@ -1671,6 +1719,8 @@ void testGetStandardX12() throws EDIStreamException { @Test void testWriteAlternateEncodedElement() throws Exception { EDIOutputFactory factory = EDIOutputFactory.newFactory(); + factory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); + ListErrorReporter reporter = setErrorReporter(factory); ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); EDIStreamWriter writer = factory.createEDIStreamWriter(stream); @@ -1684,6 +1734,7 @@ void testWriteAlternateEncodedElement() throws Exception { .writeEndSegment(); writer.flush(); assertEquals("SEG*BÜTTNER~", stream.toString("UTF-8")); + assertTrue(reporter.hasError(0, EDIStreamValidationError.SEGMENT_NOT_IN_DEFINED_TRANSACTION_SET, "SEG")); } @Test @@ -1720,6 +1771,8 @@ void testGetDelimitersX12Pre00402() throws EDIStreamException { @Test void testGetDelimitersX12BadVersion() throws EDIStreamException { EDIOutputFactory factory = EDIOutputFactory.newFactory(); + factory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); + ListErrorReporter reporter = setErrorReporter(factory); ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); EDIStreamWriter writer = factory.createEDIStreamWriter(stream); @@ -1739,6 +1792,9 @@ void testGetDelimitersX12BadVersion() throws EDIStreamException { writer.writeElement(":"); writer.writeEndSegment(); + assertEquals(1, reporter.errors.size()); + assertTrue(reporter.hasError(0, EDIStreamValidationError.INVALID_CODE_VALUE, "0050X")); + Map delimiters = writer.getDelimiters(); assertEquals('~', delimiters.get(Delimiters.SEGMENT)); assertEquals('*', delimiters.get(Delimiters.DATA_ELEMENT)); @@ -2000,7 +2056,7 @@ void testVersionReleaseEDIFACTHeader() throws EDIStreamException { writer.writeComponent("4"); writer.writeEmptyComponent(); writer.writeEmptyComponent(); - writer.writeComponent("2"); + writer.writeComponent("02"); writer.endElement(); writer.writeStartElement(); @@ -2014,7 +2070,7 @@ void testVersionReleaseEDIFACTHeader() throws EDIStreamException { writer.endElement(); writer.writeStartElement(); - writer.writeComponent("060515"); + writer.writeComponent("20060515"); writer.writeComponent("1434"); writer.endElement(); @@ -2022,7 +2078,7 @@ void testVersionReleaseEDIFACTHeader() throws EDIStreamException { writer.writeEndSegment(); writer.flush(); - assertEquals("UNA:+.?*'UNB+UNOA:4:::2+005435656:1+006415160:1+060515:1434+00000000000778'", + assertEquals("UNA:+.?*'UNB+UNOA:4:::02+005435656:1+006415160:1+20060515:1434+00000000000778'", new String(stream.toByteArray())); } @@ -2116,14 +2172,23 @@ void testOutputCompositeTooManyRepetitions() throws Exception { .writeComponent("UN") .endElement(); writer.writeRepeatElement(); + EDIValidationException thrown = assertThrows(EDIValidationException.class, () -> writer.writeComponent("ANY")); - EDIStreamValidationError error = thrown.getError(); - assertEquals(EDIStreamValidationError.TOO_MANY_REPETITIONS, error); + + assertEquals(EDIStreamEvent.ELEMENT_OCCURRENCE_ERROR, thrown.getEvent()); + assertEquals(EDIStreamValidationError.TOO_MANY_REPETITIONS, thrown.getError()); + assertEquals("UCM", thrown.getLocation().getSegmentTag()); + assertEquals(4, thrown.getLocation().getSegmentPosition()); + assertEquals(2, thrown.getLocation().getElementPosition()); + assertEquals(2, thrown.getLocation().getElementOccurrence()); + assertEquals(-1, thrown.getLocation().getComponentPosition()); } @Test void testEscapeCharEscaped() throws IOException, EDIStreamException { final EDIOutputFactory factory = EDIOutputFactory.newFactory(); + factory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); + ListErrorReporter reporter = setErrorReporter(factory); ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); try (EDIStreamWriter writer = factory.createEDIStreamWriter(stream)){ @@ -2147,11 +2212,14 @@ void testEscapeCharEscaped() throws IOException, EDIStreamException { } assertEquals("UNB+UNO1:1++++'ISA+??????'", new String(stream.toByteArray())); + assertTrue(reporter.hasError(0, EDIStreamValidationError.INVALID_CODE_VALUE, "UNO1")); } @Test void testIncompleteUNB() throws IOException, EDIStreamException, EDISchemaException { final EDIOutputFactory factory = EDIOutputFactory.newFactory(); + factory.setProperty(EDIOutputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true); + ListErrorReporter reporter = setErrorReporter(factory); ByteArrayOutputStream stream = new ByteArrayOutputStream(4096); try (EDIStreamWriter writer = factory.createEDIStreamWriter(stream)) { @@ -2170,6 +2238,10 @@ void testIncompleteUNB() throws IOException, EDIStreamException, EDISchemaExcept } assertEquals("UNA:+.? 'UNB+UNOA:3'UNH", new String(stream.toByteArray())); + assertTrue(reporter.hasError(0, EDIStreamValidationError.REQUIRED_DATA_ELEMENT_MISSING, "")); + assertTrue(reporter.hasError(1, EDIStreamValidationError.REQUIRED_DATA_ELEMENT_MISSING, "")); + assertTrue(reporter.hasError(2, EDIStreamValidationError.REQUIRED_DATA_ELEMENT_MISSING, "")); + assertTrue(reporter.hasError(3, EDIStreamValidationError.REQUIRED_DATA_ELEMENT_MISSING, "")); } @Test @@ -2215,11 +2287,9 @@ void testUnexpectedHeaderEDIFACT(String segmentTag) throws IOException, EDIStrea writer.startInterchange(); writer.writeStartSegment("UNA") .writeEndSegment(); - thrown = assertThrows(EDIStreamException.class, () -> { // NOSONAR - // Exception thrown at different positions depending on the segment tag - writer.writeStartSegment(segmentTag); - writer.writeEndSegment(); - }); + writer.writeStartSegment(segmentTag) + .writeStartElement(); + thrown = assertThrows(EDIStreamException.class, () -> writer.writeEndSegment()); } assertEquals("Failed writing EDIFACT header: Expected UNB segment following UNA but received " + segmentTag, thrown.getMessage()); @@ -2239,8 +2309,9 @@ void testMismatchedTrailerControlReferenceThrowsError() throws EDIStreamExceptio writer.setControlSchema(schema); writer.startInterchange(); writeHeader(writer); - writer.writeStartSegment("IEA").writeElement("0"); + writer.writeStartSegment("IEA"); + writer.writeElement("0"); thrown = assertThrows(EDIValidationException.class, () -> writer.writeElement("123456789")); } @@ -2251,4 +2322,29 @@ void testMismatchedTrailerControlReferenceThrowsError() throws EDIStreamExceptio assertEquals(2, thrown.getLocation().getElementPosition()); } + static ListErrorReporter setErrorReporter(EDIOutputFactory factory) { + ListErrorReporter reporter = new ListErrorReporter(); + factory.setErrorReporter(reporter); + return reporter; + } + + static class ListErrorReporter implements EDIOutputErrorReporter { + List references = new ArrayList<>(); + List errors = new ArrayList<>(); + + @Override + public void report(EDIStreamValidationError errorType, + EDIStreamWriter writer, + Location location, + CharSequence data, + EDIReference typeReference) { + errors.add(new EDIValidationException(errorType.getCategory(), errorType, location, data != null ? data.toString() : null)); + references.add(typeReference); + } + + boolean hasError(int index, EDIStreamValidationError error, String data) { + EDIValidationException errorException = errors.get(index); + return errorException.getError() == error && Objects.equals(errorException.getData(), data); + } + } } diff --git a/src/test/java/io/xlate/edi/internal/stream/StaEDIXMLStreamWriterTest.java b/src/test/java/io/xlate/edi/internal/stream/StaEDIXMLStreamWriterTest.java index 4728f9f4..f90a6725 100644 --- a/src/test/java/io/xlate/edi/internal/stream/StaEDIXMLStreamWriterTest.java +++ b/src/test/java/io/xlate/edi/internal/stream/StaEDIXMLStreamWriterTest.java @@ -65,9 +65,10 @@ void testRepeatedElement() { static void unconfirmedBufferEquals(String expected, EDIStreamWriter writer) { StaEDIStreamWriter writerImpl = (StaEDIStreamWriter) writer; - writerImpl.unconfirmedBuffer.mark(); - writerImpl.unconfirmedBuffer.flip(); - assertEquals(expected, writerImpl.unconfirmedBuffer.toString()); + if (writerImpl.outputBuffer.position() > 0 || writerImpl.outputBuffer.limit() == writerImpl.outputBuffer.capacity()) { + writerImpl.outputBuffer.flip(); + } + assertEquals(expected, writerImpl.outputBuffer.toString()); } @Test diff --git a/src/test/java/io/xlate/edi/internal/wiki/WriteInterchangeAcknowledgementTest.java b/src/test/java/io/xlate/edi/internal/wiki/WriteInterchangeAcknowledgementTest.java index 765441a8..5fbcd32f 100644 --- a/src/test/java/io/xlate/edi/internal/wiki/WriteInterchangeAcknowledgementTest.java +++ b/src/test/java/io/xlate/edi/internal/wiki/WriteInterchangeAcknowledgementTest.java @@ -54,7 +54,7 @@ void testAcknowledgementWrite() throws Exception { .writeEndSegment(); writer.writeStartSegment("IEA") - .writeElement("1") + .writeElement("0") // No included functional groups .writeElement("000000001") .writeEndSegment(); diff --git a/src/test/resources/wiki/x12_interchange_ack.txt b/src/test/resources/wiki/x12_interchange_ack.txt index c3b4f411..943ce541 100644 --- a/src/test/resources/wiki/x12_interchange_ack.txt +++ b/src/test/resources/wiki/x12_interchange_ack.txt @@ -1,3 +1,3 @@ ISA*00* *00* *ZZ*Receiver *ZZ*Sender *200301*1430*^*00501*000000001*0*P*:~ TA1*000000050*200229*1200*A*000~ -IEA*1*000000001~ \ No newline at end of file +IEA*0*000000001~ \ No newline at end of file