Repository: xlate/staedi (java)

Issue:
Option to disable validation of code values in a control schema
Provide an option to disable validation of the value enumerations defined in a control schema. The option/property will be a `boolean` and can be set on an `EDIInputFactory` instance using the constant `EDIInputFactory.EDI_VALIDATE_CONTROL_CODE_VALUES`. The default value should be `true`.

This option/property will allow a user to utilize control schema validation for structural and data type constraints, but still provide support for custom values that may not be defined in the list of enumerated values for an element in the control schema.

Pull request description:
Add EDIInputFactory property to disable control schema code values
Fixes #16 

Add EDIInputFactory property to disable control schema code values
- Minor re-factoring and test improvements

Patch:
diff --git a/src/main/java/io/xlate/edi/internal/schema/implementation/BaseImpl.java b/src/main/java/io/xlate/edi/internal/schema/implementation/BaseImpl.java
index 25a34f5..e06793c 100644
--- a/src/main/java/io/xlate/edi/internal/schema/implementation/BaseImpl.java
+++ b/src/main/java/io/xlate/edi/internal/schema/implementation/BaseImpl.java
@@ -26,7 +26,7 @@ public abstract class BaseImpl<T extends EDIType> implements EDITypeImplementati
 
     @Override
     public EDIType getReferencedType() {
-        return standard;
+        return getStandard();
     }
 
     @Override
diff --git a/src/main/java/io/xlate/edi/internal/stream/StaEDIInputFactory.java b/src/main/java/io/xlate/edi/internal/stream/StaEDIInputFactory.java
index cfe8a04..dcf1044 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIInputFactory.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIInputFactory.java
@@ -39,6 +39,7 @@ public class StaEDIInputFactory extends EDIInputFactory {
 
     public StaEDIInputFactory() {
         supportedProperties.add(EDI_VALIDATE_CONTROL_STRUCTURE);
+        supportedProperties.add(EDI_VALIDATE_CONTROL_CODE_VALUES);
 
         supportedCharsets = new HashSet<>();
         supportedCharsets.add(DEFAULT_ENCODING);
diff --git a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java
index 9fddf63..d4f32eb 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamReader.java
@@ -259,7 +259,7 @@ public class StaEDIStreamReader implements EDIStreamReader {
         }
 
         this.controlSchema = schema;
-        proxy.setControlSchema(schema);
+        proxy.setControlSchema(schema, validateControlCodeValues());
     }
 
     @Override
@@ -423,15 +423,23 @@ public class StaEDIStreamReader implements EDIStreamReader {
 
     /**************************************************************************/
 
+    boolean validateControlCodeValues() {
+        return getBooleanProperty(EDIInputFactory.EDI_VALIDATE_CONTROL_CODE_VALUES, true);
+    }
+
     boolean useInternalControlSchema() {
         if (this.controlSchema != null) {
             return false;
         }
 
-        Object property = properties.get(EDIInputFactory.EDI_VALIDATE_CONTROL_STRUCTURE);
+        return getBooleanProperty(EDIInputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true);
+    }
+
+    boolean getBooleanProperty(String propertyName, boolean defaultValue) {
+        Object property = properties.get(propertyName);
 
         if (property == null) {
-            return true;
+            return defaultValue;
         }
 
         return Boolean.parseBoolean(property.toString());
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 536aedd..b0bb312 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIStreamWriter.java
@@ -192,12 +192,12 @@ public class StaEDIStreamWriter implements EDIStreamWriter, ElementDataHandler,
     public void setControlSchema(Schema controlSchema) {
         ensureLevel(LEVEL_INITIAL);
         this.controlSchema = controlSchema;
-        controlValidator = controlSchema != null ? new Validator(controlSchema, null) : null;
+        controlValidator = controlSchema != null ? new Validator(controlSchema, true, null) : null;
     }
 
     @Override
     public void setTransactionSchema(Schema transactionSchema) {
-        transactionValidator = transactionSchema != null ? new Validator(transactionSchema, controlSchema) : null;
+        transactionValidator = transactionSchema != null ? new Validator(transactionSchema, true, controlSchema) : null;
     }
 
     @Override
diff --git a/src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java b/src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java
index 7c11ef2..5c4bc69 100644
--- a/src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java
+++ b/src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java
@@ -292,14 +292,9 @@ final class StaEDIXMLStreamReader implements XMLStreamReader {
             throw streamException("Element text only available on START_ELEMENT");
         }
 
-        int eventType = next();
-
-        if (eventType != CHARACTERS) {
-            throw streamException("Unexpected event type: " + eventType);
-        }
-
+        next(); // Advance to the text/CDATA
         final String text = getText();
-        eventType = next();
+        int eventType = next();
 
         if (eventType != END_ELEMENT) {
             throw streamException("Unexpected event type after text " + eventType);
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 b19bc18..2210b06 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
@@ -53,19 +53,19 @@ public class ProxyEventHandler implements EventHandler {
 
     public ProxyEventHandler(StaEDIStreamLocation location, Schema controlSchema) {
         this.location = location;
-        setControlSchema(controlSchema);
+        setControlSchema(controlSchema, true);
         for (int i = 0; i < 99; i++) {
             events[i] = new StreamEvent();
         }
     }
 
-    public void setControlSchema(Schema controlSchema) {
+    public void setControlSchema(Schema controlSchema, boolean validateCodeValues) {
         if (controlValidator != null) {
             throw new IllegalStateException("control validator already created");
         }
 
         this.controlSchema = controlSchema;
-        controlValidator = controlSchema != null ? new Validator(controlSchema, null) : null;
+        controlValidator = controlSchema != null ? new Validator(controlSchema, validateCodeValues, null) : null;
     }
 
     public boolean isTransactionSchemaAllowed() {
@@ -73,7 +73,7 @@ public class ProxyEventHandler implements EventHandler {
     }
 
     public void setTransactionSchema(Schema transactionSchema) {
-        transactionValidator = transactionSchema != null ? new Validator(transactionSchema, controlSchema) : null;
+        transactionValidator = transactionSchema != null ? new Validator(transactionSchema, true, controlSchema) : null;
     }
 
     public void resetEvents() {
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/UsageError.java b/src/main/java/io/xlate/edi/internal/stream/validation/UsageError.java
index cef8e84..71653a2 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/UsageError.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/UsageError.java
@@ -30,6 +30,10 @@ public class UsageError {
         this.error = error;
     }
 
+    boolean isDepthGreaterThan(int depth) {
+        return this.depth > depth;
+    }
+
     void handle(BiConsumer<String, EDIStreamValidationError> handler) {
         handler.accept(code, error);
     }
@@ -41,8 +45,4 @@ public class UsageError {
     public EDIStreamValidationError getError() {
         return error;
     }
-
-    public int getDepth() {
-        return depth;
-    }
 }
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/UsageNode.java b/src/main/java/io/xlate/edi/internal/stream/validation/UsageNode.java
index c929dfa..bef7632 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/UsageNode.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/UsageNode.java
@@ -18,6 +18,7 @@ package io.xlate.edi.internal.stream.validation;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 import io.xlate.edi.internal.stream.tokenization.Dialect;
 import io.xlate.edi.schema.EDIComplexType;
@@ -137,7 +138,7 @@ class UsageNode {
         return referencedNode.getId();
     }
 
-    void validate(Dialect dialect, CharSequence value, List<EDIStreamValidationError> errors) {
+    void validate(Dialect dialect, CharSequence value, boolean validateCodeValues, List<EDIStreamValidationError> errors) {
         if (validator == null) {
             throw new UnsupportedOperationException("simple type only");
         }
@@ -150,7 +151,11 @@ class UsageNode {
             element = (EDISimpleType) link.getReferencedType();
         }
 
-        validator.validate(dialect, element, value, errors);
+        if (validateCodeValues) {
+            validator.validate(dialect, element, value, errors);
+        } else {
+            validator.validate(dialect, UnenumeratedElement.from(element), value, errors);
+        }
     }
 
     List<EDISyntaxRule> getSyntaxRules() {
@@ -236,4 +241,46 @@ class UsageNode {
     UsageNode getSiblingById(CharSequence id) {
         return parent != null ? parent.getChildById(id) : null;
     }
+
+    private static class UnenumeratedElement implements EDISimpleType {
+        final EDISimpleType target;
+
+        static EDISimpleType from(EDISimpleType target) {
+            return new UnenumeratedElement(target);
+        }
+
+        private UnenumeratedElement(EDISimpleType target) {
+            this.target = target;
+        }
+
+        @Override
+        public String getId() {
+            return target.getId();
+        }
+
+        @Override
+        public Base getBase() {
+            return target.getBase();
+        }
+
+        @Override
+        public int getNumber() {
+            return target.getNumber();
+        }
+
+        @Override
+        public long getMinLength() {
+            return target.getMinLength();
+        }
+
+        @Override
+        public long getMaxLength() {
+            return target.getMaxLength();
+        }
+
+        @Override
+        public Set<String> getValueSet() {
+            return Collections.emptySet();
+        }
+    }
 }
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 eccca3a..ef49018 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
@@ -48,6 +48,7 @@ public class Validator {
 
     private Schema containerSchema;
     private Schema schema;
+    private final boolean validateCodeValues;
 
     private final UsageNode root;
     private final UsageNode implRoot;
@@ -96,8 +97,9 @@ public class Validator {
         }
     }
 
-    public Validator(Schema schema, Schema containerSchema) {
+    public Validator(Schema schema, boolean validateCodeValues, Schema containerSchema) {
         this.schema = schema;
+        this.validateCodeValues = validateCodeValues;
         this.containerSchema = containerSchema;
 
         root = buildTree(schema.getStandard());
@@ -562,7 +564,7 @@ public class Validator {
 
         while (errors.hasNext()) {
             UsageError e = errors.next();
-            if (e.depth > depth) {
+            if (e.isDepthGreaterThan(depth)) {
                 e.handle(handler::segmentError);
                 errors.remove();
             }
@@ -868,14 +870,14 @@ public class Validator {
         }
 
         List<EDIStreamValidationError> errors = new ArrayList<>();
-        this.element.validate(dialect, value, errors);
+        this.element.validate(dialect, value, this.validateCodeValues, errors);
 
         for (EDIStreamValidationError error : errors) {
             elementErrors.add(new UsageError(this.element, error));
         }
 
         if (errors.isEmpty() && implSegmentSelected && implElement != null) {
-            this.implElement.validate(dialect, value, errors);
+            this.implElement.validate(dialect, value, this.validateCodeValues, errors);
 
             for (EDIStreamValidationError error : errors) {
                 if (error == INVALID_CODE_VALUE) {
diff --git a/src/main/java/io/xlate/edi/stream/EDIInputFactory.java b/src/main/java/io/xlate/edi/stream/EDIInputFactory.java
index aaa749a..32c57d5 100644
--- a/src/main/java/io/xlate/edi/stream/EDIInputFactory.java
+++ b/src/main/java/io/xlate/edi/stream/EDIInputFactory.java
@@ -25,6 +25,7 @@ import io.xlate.edi.schema.Schema;
 public abstract class EDIInputFactory extends PropertySupport {
 
     public static final String EDI_VALIDATE_CONTROL_STRUCTURE = "io.xlate.edi.stream.EDI_VALIDATE_CONTROL_STRUCTURE";
+    public static final String EDI_VALIDATE_CONTROL_CODE_VALUES = "io.xlate.edi.stream.EDI_VALIDATE_CONTROL_CODE_VALUES";
 
     /**
      * Create a new instance of the factory. This static method creates a new


Test patch:
diff --git a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java
index de8d0ad..34fd082 100644
--- a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamReaderTest.java
@@ -944,9 +944,11 @@ public class StaEDIStreamReaderTest implements ConstantsTest {
     public void testEmptySegmentValidation() throws Exception {
 
         EDIInputFactory factory = EDIInputFactory.newFactory();
+        factory.setProperty(EDIInputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true);
+        factory.setProperty(EDIInputFactory.EDI_VALIDATE_CONTROL_CODE_VALUES, false);
+
         Schema transSchema = SchemaFactory.newFactory()
                                           .createSchema(getClass().getResourceAsStream("/EDIFACT/empty-segment-schema.xml"));
-        factory.setProperty(EDIInputFactory.EDI_VALIDATE_CONTROL_STRUCTURE, true);
 
         EDIStreamReader reader = factory.createEDIStreamReader(getClass().getResourceAsStream("/EDIFACT/empty-segment-example.edi"));
         String segmentName = null;
@@ -979,24 +981,10 @@ public class StaEDIStreamReaderTest implements ConstantsTest {
                     break;
                 }
                 case ELEMENT_DATA_ERROR:
-                    // TODO Change "control schema", because it does not recognise "IATA" (UNB), "PNRGOV:11" (UNH)
-                    if ("DE0001".equals(reader.getReferenceCode()) && "IATA".equals(reader.getText())) {
-                        break;
-                    }
-                    if ("DE0065".equals(reader.getReferenceCode()) && "PNRGOV".equals(reader.getText())) {
-                        break;
-                    }
-                    if ("DE0052".equals(reader.getReferenceCode())) {
-                        break;
-                    }
-
-                    break;
-
                 case ELEMENT_OCCURRENCE_ERROR: {
                     Location loc = reader.getLocation();
                     EDIStreamValidationError error = reader.getErrorType();
 
-                    // FIXME when the empty segment is reached, TOO_MANY_DATA_ELEMENTS is returned
                     fail(String.format("%s: %s (seg=%s, elemPos=%d, compoPos=%d, textOnError=%s)",
                                        error.getCategory(),
                                        error,
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 833e007..2b0ae3c 100644
--- a/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamWriterTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/StaEDIStreamWriterTest.java
@@ -519,7 +519,16 @@ public class StaEDIStreamWriterTest {
                     writer.writeBinaryData(reader.getBinaryData());
                     writer.endElement();
                     break;
+                case START_TRANSACTION:
+                case START_GROUP:
+                case START_LOOP:
+                case END_LOOP:
+                case END_GROUP:
+                case END_TRANSACTION:
+                    // Ignored
+                    break;
                 default:
+                    fail("Unexpected event type: " + event);
                     break;
                 }
             }
@@ -620,7 +629,16 @@ public class StaEDIStreamWriterTest {
                     writer.writeBinaryData(reader.getBinaryData());
                     writer.endElement();
                     break;
+                case START_TRANSACTION:
+                case START_GROUP:
+                case START_LOOP:
+                case END_LOOP:
+                case END_GROUP:
+                case END_TRANSACTION:
+                    // Ignored
+                    break;
                 default:
+                    fail("Unexpected event type: " + event);
                     break;
                 }
             }
@@ -634,6 +652,7 @@ public class StaEDIStreamWriterTest {
     @Test
     public void testInputEquivalenceEDIFACT_IATA_PNRGOV() throws Exception {
         EDIInputFactory inputFactory = EDIInputFactory.newFactory();
+        inputFactory.setProperty(EDIInputFactory.EDI_VALIDATE_CONTROL_CODE_VALUES, false);
         final ByteArrayOutputStream expected = new ByteArrayOutputStream(16384);
 
         InputStream source = new InputStream() {
@@ -721,7 +740,16 @@ public class StaEDIStreamWriterTest {
                     writer.writeBinaryData(reader.getBinaryData());
                     writer.endElement();
                     break;
+                case START_TRANSACTION:
+                case START_GROUP:
+                case START_LOOP:
+                case END_LOOP:
+                case END_GROUP:
+                case END_TRANSACTION:
+                    // Ignored
+                    break;
                 default:
+                    fail("Unexpected event type: " + event);
                     break;
                 }
             }
@@ -814,7 +842,16 @@ public class StaEDIStreamWriterTest {
                     writer.writeBinaryData(reader.getBinaryData());
                     writer.endElement();
                     break;
+                case START_TRANSACTION:
+                case START_GROUP:
+                case START_LOOP:
+                case END_LOOP:
+                case END_GROUP:
+                case END_TRANSACTION:
+                    // Ignored
+                    break;
                 default:
+                    fail("Unexpected event type: " + event);
                     break;
                 }
             }
diff --git a/src/test/java/io/xlate/edi/internal/stream/validation/ListSyntaxValidatorTest.java b/src/test/java/io/xlate/edi/internal/stream/validation/ListSyntaxValidatorTest.java
index ce558bf..2ad70c9 100644
--- a/src/test/java/io/xlate/edi/internal/stream/validation/ListSyntaxValidatorTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/validation/ListSyntaxValidatorTest.java
@@ -5,6 +5,7 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.util.Arrays;
@@ -16,6 +17,7 @@ import org.junit.jupiter.api.Test;
 import org.mockito.stubbing.Answer;
 
 import io.xlate.edi.schema.EDISyntaxRule.Type;
+import io.xlate.edi.schema.EDIType;
 import io.xlate.edi.stream.EDIStreamEvent;
 import io.xlate.edi.stream.EDIStreamValidationError;
 
@@ -102,4 +104,40 @@ public class ListSyntaxValidatorTest extends SyntaxValidatorTestBase {
         validator.validate(syntax, structure, handler);
         assertEquals(2, count.get()); // Error for both positions 3 and 4
     }
+
+
+    @Test
+    public void testValidateListConditionalMissingRequired_Composite() {
+        when(structure.isNodeType(EDIType.Type.COMPOSITE)).thenReturn(true);
+
+        UsageNode parent = mock(UsageNode.class);
+        when(structure.getParent()).thenReturn(parent);
+        when(parent.getIndex()).thenReturn(0); // composite is the first element in the parent segment
+
+        when(syntax.getPositions()).thenReturn(Arrays.asList(1, 3, 4));
+
+        List<UsageNode> children = Arrays.asList(mockUsageNode(true, 1),
+                                                 mockUsageNode(false, 2),
+                                                 mockUsageNode(false, 3)/*,
+                                                                        mockUsageNode(false, 4)*/);
+        when(structure.getChildren()).thenReturn(children);
+        final AtomicInteger count = new AtomicInteger(0);
+        final AtomicInteger element = new AtomicInteger(0);
+
+        doAnswer((Answer<Void>) invocation -> {
+            count.incrementAndGet();
+            element.set(invocation.getArgument(3));
+            return null;
+        }).when(handler)
+          .elementError(eq(EDIStreamEvent.ELEMENT_OCCURRENCE_ERROR),
+                        eq(EDIStreamValidationError.CONDITIONAL_REQUIRED_DATA_ELEMENT_MISSING),
+                        nullable(CharSequence.class),
+                        any(Integer.class),
+                        any(Integer.class),
+                        any(Integer.class));
+
+        validator.validate(syntax, structure, handler);
+        assertEquals(1, element.get());
+        assertEquals(2, count.get()); // Error for both positions 3 and 4
+    }
 }
diff --git a/src/test/java/io/xlate/edi/internal/stream/validation/SyntaxValidatorTestBase.java b/src/test/java/io/xlate/edi/internal/stream/validation/SyntaxValidatorTestBase.java
index cd6b73b..e8fda22 100644
--- a/src/test/java/io/xlate/edi/internal/stream/validation/SyntaxValidatorTestBase.java
+++ b/src/test/java/io/xlate/edi/internal/stream/validation/SyntaxValidatorTestBase.java
@@ -21,8 +21,6 @@ abstract class SyntaxValidatorTestBase {
     @BeforeEach
     public void setUp() {
         syntax = mock(EDISyntaxRule.class);
-        when(syntax.getType()).thenReturn(EDISyntaxRule.Type.CONDITIONAL);
-
         handler = mock(ValidationEventHandler.class);
 
         structureType = mock(EDIType.class);
@@ -32,7 +30,7 @@ abstract class SyntaxValidatorTestBase {
         when(structureRef.getReferencedType()).thenReturn(structureType);
 
         structure = mock(UsageNode.class);
-        when(structure.getReferencedType()).thenReturn(structureType);
+        when(structure.isNodeType(EDIType.Type.SEGMENT)).thenReturn(true);
     }
 
     protected UsageNode mockUsageNode(String referenceCode, boolean used, int index) {