package io.xlate.edi.internal.schema; import static io.xlate.edi.internal.schema.StaEDISchemaFactory.schemaException; import static io.xlate.edi.internal.schema.StaEDISchemaFactory.unexpectedElement; import static io.xlate.edi.internal.schema.StaEDISchemaFactory.unexpectedEvent; import static java.util.stream.Collectors.toList; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.stream.IntStream; import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import io.xlate.edi.schema.EDIComplexType; import io.xlate.edi.schema.EDIReference; import io.xlate.edi.schema.EDISchemaException; import io.xlate.edi.schema.EDISimpleType; import io.xlate.edi.schema.EDISimpleType.Base; import io.xlate.edi.schema.EDISyntaxRule; import io.xlate.edi.schema.EDIType; import io.xlate.edi.schema.EDIType.Type; abstract class SchemaReaderBase implements SchemaReader { static final String REFERR_UNDECLARED = "Type %s references undeclared %s with ref='%s'"; static final String REFERR_ILLEGAL = "Type '%s' must not be referenced as '%s' in definition of type '%s'"; static final String LOCALNAME_ELEMENT = "element"; static final String LOCALNAME_COMPOSITE = "composite"; static final EDIReference ANY_ELEMENT_REF_OPT = new Reference(StaEDISchema.ANY_ELEMENT_ID, LOCALNAME_ELEMENT, 0, 1); static final EDIReference ANY_COMPOSITE_REF_OPT = new Reference(StaEDISchema.ANY_COMPOSITE_ID, LOCALNAME_COMPOSITE, 0, 99); static final EDIReference ANY_ELEMENT_REF_REQ = new Reference(StaEDISchema.ANY_ELEMENT_ID, LOCALNAME_ELEMENT, 1, 1); static final EDIReference ANY_COMPOSITE_REF_REQ = new Reference(StaEDISchema.ANY_COMPOSITE_ID, LOCALNAME_COMPOSITE, 1, 99); static final EDISimpleType ANY_ELEMENT = new ElementType(StaEDISchema.ANY_ELEMENT_ID, Base.STRING, "ANY", 0, 0, 99_999, Collections.emptySet(), Collections.emptyList()); static final EDIComplexType ANY_COMPOSITE = new StructureType(StaEDISchema.ANY_COMPOSITE_ID, Type.COMPOSITE, "ANY", IntStream.rangeClosed(0, 99).mapToObj(i -> ANY_ELEMENT_REF_OPT) .collect(toList()), Collections.emptyList()); final String xmlns; final QName qnSchema; final QName qnInclude; final QName qnDescription; final QName qnInterchange; final QName qnGroup; final QName qnTransaction; final QName qnLoop; final QName qnSegment; final QName qnComposite; final QName qnElement; final QName qnSyntax; final QName qnPosition; final QName qnSequence; final QName qnEnumeration; final QName qnValue; final QName qnVersion; final QName qnAny; final QName qnCompositeType; final QName qnElementType; final QName qnSegmentType; final Map<QName, EDIType.Type> complex; final Map<QName, EDIType.Type> typeDefinitions; final Set<QName> references; protected XMLStreamReader reader; protected Map<String, Object> properties; public SchemaReaderBase(String xmlns, XMLStreamReader reader, Map<String, Object> properties) { this.xmlns = xmlns; qnSchema = new QName(xmlns, "schema"); qnInclude = new QName(xmlns, "include"); qnDescription = new QName(xmlns, "description"); qnInterchange = new QName(xmlns, "interchange"); qnGroup = new QName(xmlns, "group"); qnTransaction = new QName(xmlns, "transaction"); qnLoop = new QName(xmlns, "loop"); qnSegment = new QName(xmlns, "segment"); qnComposite = new QName(xmlns, LOCALNAME_COMPOSITE); qnElement = new QName(xmlns, LOCALNAME_ELEMENT); qnSyntax = new QName(xmlns, "syntax"); qnPosition = new QName(xmlns, "position"); qnSequence = new QName(xmlns, "sequence"); qnEnumeration = new QName(xmlns, "enumeration"); qnValue = new QName(xmlns, "value"); qnVersion = new QName(xmlns, "version"); qnAny = new QName(xmlns, "any"); qnCompositeType = new QName(xmlns, "compositeType"); qnElementType = new QName(xmlns, "elementType"); qnSegmentType = new QName(xmlns, "segmentType"); complex = new HashMap<>(4); complex.put(qnInterchange, EDIType.Type.INTERCHANGE); complex.put(qnGroup, EDIType.Type.GROUP); complex.put(qnTransaction, EDIType.Type.TRANSACTION); complex.put(qnLoop, EDIType.Type.LOOP); complex.put(qnSegmentType, EDIType.Type.SEGMENT); complex.put(qnCompositeType, EDIType.Type.COMPOSITE); typeDefinitions = new HashMap<>(3); typeDefinitions.put(qnSegmentType, EDIType.Type.SEGMENT); typeDefinitions.put(qnCompositeType, EDIType.Type.COMPOSITE); typeDefinitions.put(qnElementType, EDIType.Type.ELEMENT); references = new HashSet<>(4); references.add(qnSegment); references.add(qnComposite); references.add(qnElement); this.reader = reader; this.properties = properties; } @Override public Map<String, EDIType> readTypes() throws EDISchemaException { Map<String, EDIType> types = new HashMap<>(100); types.put(StaEDISchema.ANY_ELEMENT_ID, ANY_ELEMENT); types.put(StaEDISchema.ANY_COMPOSITE_ID, ANY_COMPOSITE); try { reader.nextTag(); QName element = reader.getName(); if (qnInclude.equals(element)) { readInclude(reader, types); readImplementation(reader, types); } else if (qnInterchange.equals(element)) { readInterchange(reader, types); } else if (qnTransaction.equals(element)) { readTransaction(reader, types); readImplementation(reader, types); } else { throw unexpectedElement(element, reader); } readTypeDefinitions(reader, types); setReferences(types); reader.next(); requireEvent(XMLStreamConstants.END_DOCUMENT, reader); } catch (XMLStreamException xse) { throw schemaException("XMLStreamException reading schema", reader, xse); } return types; } String readDescription(XMLStreamReader reader) { nextTag(reader, "seeking description element"); QName element = reader.getName(); String description = null; if (qnDescription.equals(element)) { description = getElementText(reader, "description"); nextTag(reader, "after description element"); } return description; } void readInterchange(XMLStreamReader reader, Map<String, EDIType> types) { QName element; Reference headerRef = createControlReference(reader, "header"); Reference trailerRef = createControlReference(reader, "trailer"); readDescription(reader); element = reader.getName(); if (!qnSequence.equals(element)) { throw unexpectedElement(element, reader); } nextTag(reader, "reading interchange sequence"); element = reader.getName(); List<EDIReference> refs = new ArrayList<>(3); refs.add(headerRef); while (qnSegment.equals(element)) { addReferences(reader, EDIType.Type.SEGMENT, refs, readReference(reader, types)); nextTag(reader, "completing interchange segment"); // Advance to end element nextTag(reader, "reading after interchange segment"); // Advance to next start element element = reader.getName(); } if (qnGroup.equals(element)) { refs.add(readControlStructure(reader, element, qnTransaction, types)); nextTag(reader, "completing group"); // Advance to end element nextTag(reader, "reading after group"); // Advance to next start element element = reader.getName(); } if (qnTransaction.equals(element)) { refs.add(readControlStructure(reader, element, null, types)); nextTag(reader, "completing transaction"); // Advance to end element nextTag(reader, "reading after transaction"); // Advance to next start element } refs.add(trailerRef); StructureType interchange = new StructureType(StaEDISchema.INTERCHANGE_ID, EDIType.Type.INTERCHANGE, "INTERCHANGE", refs, Collections.emptyList()); types.put(interchange.getId(), interchange); } Reference readControlStructure(XMLStreamReader reader, QName element, QName subelement, Map<String, EDIType> types) { int minOccurs = 0; int maxOccurs = 99999; String use = parseAttribute(reader, "use", String::valueOf, "optional"); switch (use) { case "required": minOccurs = 1; break; case "optional": minOccurs = 0; break; case "prohibited": maxOccurs = 0; break; default: throw schemaException("Invalid value for 'use': " + use, reader); } Reference headerRef = createControlReference(reader, "header"); Reference trailerRef = createControlReference(reader, "trailer"); readDescription(reader); if (subelement != null) { requireElementStart(subelement, reader); } List<EDIReference> refs = new ArrayList<>(3); refs.add(headerRef); if (subelement != null) { refs.add(readControlStructure(reader, subelement, null, types)); } refs.add(trailerRef); Type elementType = complex.get(element); String elementId = StaEDISchema.ID_PREFIX + elementType.name(); StructureType struct = new StructureType(elementId, elementType, elementType.toString(), refs, Collections.emptyList()); types.put(struct.getId(), struct); Reference structRef = new Reference(struct.getId(), element.getLocalPart(), minOccurs, maxOccurs); structRef.setReferencedType(struct); return structRef; } Reference createControlReference(XMLStreamReader reader, String attributeName) { final String refId = parseAttribute(reader, attributeName, String::valueOf); return new Reference(refId, "segment", 1, 1); } void readTransaction(XMLStreamReader reader, Map<String, EDIType> types) { QName element = reader.getName(); types.put(StaEDISchema.TRANSACTION_ID, readComplexType(reader, element, types)); } void readTypeDefinitions(XMLStreamReader reader, Map<String, EDIType> types) { boolean schemaEnd = false; // Cursor is already positioned on a type definition (e.g. from an earlier look-ahead) if (typeDefinitions.containsKey(reader.getName()) && reader.getEventType() == XMLStreamConstants.START_ELEMENT) { readTypeDefinition(types, reader); } while (!schemaEnd) { if (nextTag(reader, "reading schema types") == XMLStreamConstants.START_ELEMENT) { readTypeDefinition(types, reader); } else { if (reader.getName().equals(qnSchema)) { schemaEnd = true; } } } } void readTypeDefinition(Map<String, EDIType> types, XMLStreamReader reader) { QName element = reader.getName(); String name; if (complex.containsKey(element)) { name = parseAttribute(reader, "name", String::valueOf); nameCheck(name, types, reader); types.put(name, readComplexType(reader, element, types)); } else if (qnElementType.equals(element)) { name = parseAttribute(reader, "name", String::valueOf); nameCheck(name, types, reader); types.put(name, readSimpleType(reader)); } else { throw unexpectedElement(element, reader); } } void nameCheck(String name, Map<String, EDIType> types, XMLStreamReader reader) { if (types.containsKey(name)) { throw schemaException("duplicate name: " + name, reader); } } StructureType readComplexType(XMLStreamReader reader, QName complexType, Map<String, EDIType> types) { final EDIType.Type type = complex.get(complexType); final String id; String code = parseAttribute(reader, "code", String::valueOf, null); if (qnTransaction.equals(complexType)) { id = StaEDISchema.TRANSACTION_ID; } else if (qnLoop.equals(complexType)) { id = code; } else { id = parseAttribute(reader, "name", String::valueOf); if (type == EDIType.Type.SEGMENT && !id.matches("^[A-Z][A-Z0-9]{1,2}$")) { throw schemaException("Invalid segment name [" + id + ']', reader); } } if (code == null) { code = id; } final List<EDIReference> refs = new ArrayList<>(8); final List<EDISyntaxRule> rules = new ArrayList<>(2); readDescription(reader); requireElementStart(qnSequence, reader); readReferences(reader, types, type, refs); int event = nextTag(reader, "searching for syntax element"); if (event == XMLStreamConstants.START_ELEMENT) { requireElementStart(qnSyntax, reader); do { readSyntax(reader, rules); event = nextTag(reader, "reading syntax elements"); } while (event == XMLStreamConstants.START_ELEMENT && qnSyntax.equals(reader.getName())); } if (event == XMLStreamConstants.END_ELEMENT) { return new StructureType(id, type, code, refs, rules); } else { throw unexpectedEvent(reader); } } void readReferences(XMLStreamReader reader, Map<String, EDIType> types, EDIType.Type parentType, List<EDIReference> refs) { boolean endOfReferences = false; while (!endOfReferences) { int event = nextTag(reader, "reading sequence"); if (event == XMLStreamConstants.START_ELEMENT) { addReferences(reader, parentType, refs, readReference(reader, types)); } else { if (reader.getName().equals(qnSequence)) { endOfReferences = true; } } } } void addReferences(XMLStreamReader reader, EDIType.Type parentType, List<EDIReference> refs, Reference reference) { if ("ANY".equals(reference.getRefId())) { final EDIReference optRef; final EDIReference reqRef; switch (parentType) { case SEGMENT: optRef = ANY_COMPOSITE_REF_OPT; reqRef = ANY_COMPOSITE_REF_REQ; break; case COMPOSITE: optRef = ANY_ELEMENT_REF_OPT; reqRef = ANY_ELEMENT_REF_REQ; break; default: throw schemaException("Element " + qnAny + " may only be present for segmentType and compositeType", reader); } final int min = reference.getMinOccurs(); final int max = reference.getMaxOccurs(); for (int i = 0; i < max; i++) { refs.add(i < min ? reqRef : optRef); } } else { refs.add(reference); } } Reference readReference(XMLStreamReader reader, Map<String, EDIType> types) { QName element = reader.getName(); String refId = null; if (qnAny.equals(element)) { refId = "ANY"; } else if (references.contains(element)) { refId = readReferencedId(reader); Objects.requireNonNull(refId); } else if (qnLoop.equals(element)) { refId = parseAttribute(reader, "code", String::valueOf); } else { throw unexpectedElement(element, reader); } String refTag = element.getLocalPart(); int minOccurs = parseAttribute(reader, "minOccurs", Integer::parseInt, 0); int maxOccurs = parseAttribute(reader, "maxOccurs", Integer::parseInt, 1); Reference ref; if (qnLoop.equals(element)) { StructureType loop = readComplexType(reader, element, types); nameCheck(refId, types, reader); types.put(refId, loop); ref = new Reference(refId, refTag, minOccurs, maxOccurs); ref.setReferencedType(loop); } else if (qnComposite.equals(element) || qnElement.equals(element)) { List<Reference.Version> versions = null; if (nextTag(reader, "reading " + element + " contents") != XMLStreamConstants.END_ELEMENT) { requireElementStart(qnVersion, reader); versions = new ArrayList<>(); do { versions.add(readReferenceVersion(reader)); } while (nextTag(reader, "reading after " + element + " version") != XMLStreamConstants.END_ELEMENT); } else { versions = Collections.emptyList(); } ref = new Reference(refId, refTag, minOccurs, maxOccurs, versions); } else { ref = new Reference(refId, refTag, minOccurs, maxOccurs); } return ref; } Reference.Version readReferenceVersion(XMLStreamReader reader) { requireElementStart(qnVersion, reader); String minVersion = parseAttribute(reader, "minVersion", String::valueOf, ""); String maxVersion = parseAttribute(reader, "maxVersion", String::valueOf, ""); Integer minOccurs = parseAttribute(reader, "minOccurs", Integer::valueOf, null); Integer maxOccurs = parseAttribute(reader, "maxOccurs", Integer::valueOf, null); if (nextTag(reader, "reading version contents") != XMLStreamConstants.END_ELEMENT) { throw unexpectedElement(reader.getName(), reader); } return new Reference.Version(minVersion, maxVersion, minOccurs, maxOccurs); } void readSyntax(XMLStreamReader reader, List<EDISyntaxRule> rules) { String type = parseAttribute(reader, "type", String::valueOf); EDISyntaxRule.Type typeInt = null; try { typeInt = EDISyntaxRule.Type.valueOf(type.toUpperCase()); } catch (IllegalArgumentException e) { throw schemaException("Invalid syntax 'type': [" + type + ']', reader, e); } rules.add(new SyntaxRestriction(typeInt, readSyntaxPositions(reader))); } List<Integer> readSyntaxPositions(XMLStreamReader reader) { final List<Integer> positions = new ArrayList<>(5); boolean endOfSyntax = false; while (!endOfSyntax) { final int event = nextTag(reader, "reading syntax positions"); final QName element = reader.getName(); if (event == XMLStreamConstants.START_ELEMENT) { if (element.equals(qnPosition)) { final String position = getElementText(reader, "syntax position"); try { positions.add(Integer.parseInt(position)); } catch (@SuppressWarnings("unused") NumberFormatException e) { throw schemaException("invalid position [" + position + ']', reader); } } } else { endOfSyntax = true; } } return positions; } ElementType readSimpleType(XMLStreamReader reader) { String name = parseAttribute(reader, "name", String::valueOf); String code = parseAttribute(reader, "code", String::valueOf, name); Base base = parseAttribute(reader, "base", val -> Base.valueOf(val.toUpperCase()), Base.STRING); int number = parseAttribute(reader, "number", Integer::parseInt, -1); long minLength = parseAttribute(reader, "minLength", Long::parseLong, 1L); long maxLength = parseAttribute(reader, "maxLength", Long::parseLong, 1L); final Set<String> values; final List<ElementType.Version> versions; if (nextTag(reader, "reading elementType contents") == XMLStreamConstants.END_ELEMENT) { values = Collections.emptySet(); versions = Collections.emptyList(); } else { if (qnEnumeration.equals(reader.getName())) { values = readEnumerationValues(reader); nextTag(reader, "reading after elementType enumeration"); } else { values = Collections.emptySet(); } if (qnVersion.equals(reader.getName())) { versions = new ArrayList<>(); do { versions.add(readSimpleTypeVersion(reader)); } while (nextTag(reader, "reading after elementType version") != XMLStreamConstants.END_ELEMENT); } else { versions = Collections.emptyList(); } } return new ElementType(name, base, code, number, minLength, maxLength, values, versions); } ElementType.Version readSimpleTypeVersion(XMLStreamReader reader) { requireElementStart(qnVersion, reader); String minVersion = parseAttribute(reader, "minVersion", String::valueOf, ""); String maxVersion = parseAttribute(reader, "maxVersion", String::valueOf, ""); Long minLength = parseAttribute(reader, "minLength", Long::valueOf, null); Long maxLength = parseAttribute(reader, "maxLength", Long::valueOf, null); Set<String> values; if (nextTag(reader, "reading elementType version contents") == XMLStreamConstants.END_ELEMENT) { // Set to null instead of empty to indicate that no enumeration is present in this version values = null; } else if (qnEnumeration.equals(reader.getName())) { values = readEnumerationValues(reader); nextTag(reader, "reading after elementType version enumeration"); } else { throw unexpectedElement(reader.getName(), reader); } return new ElementType.Version(minVersion, maxVersion, minLength, maxLength, values); } Set<String> readEnumerationValues(XMLStreamReader reader) { Set<String> values = null; boolean endOfEnumeration = false; while (!endOfEnumeration) { final int event = nextTag(reader, "reading enumeration"); final QName element = reader.getName(); if (event == XMLStreamConstants.START_ELEMENT) { if (element.equals(qnValue)) { values = readEnumerationValue(reader, values); } else { throw unexpectedElement(element, reader); } } else { endOfEnumeration = true; } } return values != null ? values : Collections.emptySet(); } Set<String> readEnumerationValue(XMLStreamReader reader, Set<String> values) { if (values == null) { values = new LinkedHashSet<>(); } values.add(getElementText(reader, "enumeration value")); return values; } <T> T parseAttribute(XMLStreamReader reader, String attrName, Function<String, T> converter, T defaultValue) { String attr = reader.getAttributeValue(null, attrName); try { return attr != null ? converter.apply(attr) : defaultValue; } catch (Exception e) { throw schemaException("Invalid " + attrName, reader, e); } } <T> T parseAttribute(XMLStreamReader reader, String attrName, Function<String, T> converter) { String attr = reader.getAttributeValue(null, attrName); if (attr != null) { try { return converter.apply(attr); } catch (Exception e) { throw schemaException("Invalid " + attrName, reader, e); } } else { throw schemaException("Missing required attribute: [" + attrName + ']', reader); } } int nextTag(XMLStreamReader reader, String activity) { try { if (!reader.hasNext()) { throw schemaException("End of stream reached while " + activity, reader, null); } return reader.nextTag(); } catch (XMLStreamException xse) { throw schemaException("XMLStreamException while " + activity, reader, xse); } } String getElementText(XMLStreamReader reader, String context) { try { return reader.getElementText(); } catch (XMLStreamException xse) { throw schemaException("XMLStreamException reading element text: " + context, reader, xse); } } void requireEvent(int eventId, XMLStreamReader reader) { Integer event = reader.getEventType(); if (event != eventId) { throw unexpectedEvent(reader); } } void requireElement(QName element, XMLStreamReader reader) { Integer event = reader.getEventType(); if (event != XMLStreamConstants.START_ELEMENT && event != XMLStreamConstants.END_ELEMENT) { throw unexpectedEvent(reader); } if (!element.equals(reader.getName())) { throw unexpectedElement(reader.getName(), reader); } } void requireElementStart(QName element, XMLStreamReader reader) { Integer event = reader.getEventType(); if (event != XMLStreamConstants.START_ELEMENT) { throw schemaException("Expected XML element [" + element + "] not found", reader); } if (!element.equals(reader.getName())) { throw unexpectedElement(reader.getName(), reader); } } void setReferences(Map<String, EDIType> types) { types.values() .stream() .filter(type -> type instanceof StructureType) .forEach(struct -> setReferences((StructureType) struct, types)); } void setReferences(StructureType struct, Map<String, EDIType> types) { for (EDIReference ref : struct.getReferences()) { Reference impl = (Reference) ref; EDIType target = types.get(impl.getRefId()); if (target == null) { throw schemaException(String.format(REFERR_UNDECLARED, struct.getId(), impl.getRefTag(), impl.getRefId())); } final EDIType.Type refType = target.getType(); if (refType != EDIType.Type.valueOf(impl.getRefTag().toUpperCase())) { throw schemaException(String.format(REFERR_ILLEGAL, impl.getRefId(), impl.getRefTag(), struct.getId())); } impl.setReferencedType(target); } } protected abstract String readReferencedId(XMLStreamReader reader); protected abstract void readInclude(XMLStreamReader reader, Map<String, EDIType> types) throws EDISchemaException; protected abstract void readImplementation(XMLStreamReader reader, Map<String, EDIType> types); }