Skip to content

Commit

Permalink
Add namespaces to XML reader
Browse files Browse the repository at this point in the history
  • Loading branch information
MikeEdgar committed Mar 18, 2020
1 parent 080ddb9 commit bee9e12
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 136 deletions.
106 changes: 87 additions & 19 deletions src/main/java/io/xlate/edi/internal/stream/StaEDIXMLStreamReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Queue;

Expand All @@ -31,6 +36,7 @@
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import io.xlate.edi.stream.EDIStreamConstants.Namespaces;
import io.xlate.edi.stream.EDIStreamEvent;
import io.xlate.edi.stream.EDIStreamException;
import io.xlate.edi.stream.EDIStreamReader;
Expand All @@ -39,13 +45,15 @@ public class StaEDIXMLStreamReader implements XMLStreamReader {

private static final Location location = new DefaultLocation();
private static final QName DUMMY_QNAME = new QName("DUMMY");
private static final QName INTERCHANGE = new QName("INTERCHANGE");
private static final QName INTERCHANGE = new QName(Namespaces.LOOPS, "INTERCHANGE", prefixOf(Namespaces.LOOPS));

private final EDIStreamReader ediReader;
private final Queue<Integer> eventQueue = new ArrayDeque<>(3);
private final Queue<QName> elementQueue = new ArrayDeque<>(3);

private final Deque<QName> elementStack = new ArrayDeque<>();

private NamespaceContext namespaceContext;
private final StringBuilder cdataBuilder = new StringBuilder();
private char[] cdata;

Expand All @@ -66,24 +74,28 @@ public Object getProperty(String name) {
}

private boolean isEvent(int... eventTypes) {
return Arrays.stream(eventTypes).anyMatch(eventQueue.element()::equals);
return Arrays.stream(eventTypes).anyMatch(eventQueue.element()::equals);
}

private QName buildName(QName parent, String namespace) {
return buildName(parent, namespace, null);
}

private QName deriveName(QName parent, String hint) {
String name = hint;
private QName buildName(QName parent, String namespace, String name) {
String prefix = prefixOf(namespace);

if (name == null) {
final io.xlate.edi.stream.Location l = ediReader.getLocation();
final int componentPosition = l.getComponentPosition();

if (componentPosition > 0) {
name = String.format("%s-%d", parent, componentPosition);
name = String.format("%s-%d", parent.getLocalPart(), componentPosition);
} else {
name = String.format("%s%02d", parent, l.getElementPosition());
name = String.format("%s%02d", parent.getLocalPart(), l.getElementPosition());
}
}

return new QName(name);
return new QName(namespace, name, prefix);
}

private void enqueueEvent(int xmlEvent, QName element, boolean remember) {
Expand All @@ -107,7 +119,7 @@ private void enqueueEvent(EDIStreamEvent ediEvent) throws XMLStreamException {

switch (ediEvent) {
case ELEMENT_DATA:
name = deriveName(elementStack.getFirst(), null);
name = buildName(elementStack.getFirst(), Namespaces.ELEMENTS);
enqueueEvent(START_ELEMENT, name, false);
enqueueEvent(CHARACTERS, DUMMY_QNAME, false);
enqueueEvent(END_ELEMENT, name, false);
Expand All @@ -118,7 +130,7 @@ private void enqueueEvent(EDIStreamEvent ediEvent) throws XMLStreamException {
* This section will read the binary data and Base64 the stream
* into an XML CDATA section.
* */
name = deriveName(elementStack.getFirst(), null);
name = buildName(elementStack.getFirst(), Namespaces.ELEMENTS);
enqueueEvent(START_ELEMENT, name, false);
enqueueEvent(CDATA, DUMMY_QNAME, false);

Expand Down Expand Up @@ -149,26 +161,29 @@ public void write(int b) throws IOException {
case START_INTERCHANGE:
enqueueEvent(START_DOCUMENT, DUMMY_QNAME, false);
enqueueEvent(START_ELEMENT, INTERCHANGE, true);
namespaceContext = new DocumentNamespaceContext();
break;

case START_SEGMENT:
enqueueEvent(START_ELEMENT, new QName(ediReader.getText()), true);
name = buildName(elementStack.getFirst(), Namespaces.SEGMENTS, ediReader.getText());
enqueueEvent(START_ELEMENT, name, true);
break;

case START_GROUP:
case START_TRANSACTION:
case START_LOOP:
name = deriveName(elementStack.getFirst(), ediReader.getText());
name = buildName(elementStack.getFirst(), Namespaces.LOOPS, ediReader.getText());
enqueueEvent(START_ELEMENT, name, true);
break;

case START_COMPOSITE:
name = deriveName(elementStack.getFirst(), ediReader.getReferenceCode());
name = buildName(elementStack.getFirst(), Namespaces.COMPOSITES, ediReader.getReferenceCode());
enqueueEvent(START_ELEMENT, name, true);
break;

case END_INTERCHANGE:
enqueueEvent(END_ELEMENT, elementStack.removeFirst(), false);
namespaceContext = null;
enqueueEvent(END_DOCUMENT, DUMMY_QNAME, false);
break;

Expand All @@ -181,7 +196,9 @@ public void write(int b) throws IOException {
break;

case SEGMENT_ERROR:
throw new XMLStreamException(String.format("Segment %s has error %s", ediReader.getText(), ediReader.getErrorType()));
throw new XMLStreamException(String.format("Segment %s has error %s",
ediReader.getText(),
ediReader.getErrorType()));

default:
throw new IllegalStateException("Unknown state: " + ediEvent);
Expand Down Expand Up @@ -224,7 +241,8 @@ public void require(int type, String namespaceURI, String localName) throws XMLS
final String currentLocalPart = name.getLocalPart();

if (!localName.equals(currentLocalPart)) {
throw new XMLStreamException("Current localPart " + currentLocalPart + " does not match required localName " + localName);
throw new XMLStreamException("Current localPart " + currentLocalPart
+ " does not match required localName " + localName);
}
}

Expand Down Expand Up @@ -364,22 +382,32 @@ public boolean isAttributeSpecified(int index) {

@Override
public int getNamespaceCount() {
if (INTERCHANGE.equals(elementQueue.element())) {
return Namespaces.all().size();
}
return 0;
}

@Override
public String getNamespacePrefix(int index) {
throw new UnsupportedOperationException();
if (INTERCHANGE.equals(elementQueue.element())) {
String namespace = Namespaces.all().get(index);
return prefixOf(namespace);
}
return null;
}

@Override
public String getNamespaceURI(int index) {
throw new UnsupportedOperationException();
if (INTERCHANGE.equals(elementQueue.element())) {
return Namespaces.all().get(index);
}
return null;
}

@Override
public NamespaceContext getNamespaceContext() {
throw new UnsupportedOperationException();
return this.namespaceContext;
}

@Override
Expand All @@ -392,7 +420,7 @@ public String getText() {
requireCharacters();

if (cdataBuilder.length() > 0) {
if (cdata == null) {
if (cdata == null) {
cdata = new char[cdataBuilder.length()];
cdataBuilder.getChars(0, cdataBuilder.length(), cdata, 0);
}
Expand Down Expand Up @@ -505,7 +533,10 @@ public boolean hasName() {

@Override
public String getNamespaceURI() {
throw new UnsupportedOperationException();
if (hasName()) {
return elementQueue.element().getNamespaceURI();
}
return null;
}

@Override
Expand Down Expand Up @@ -543,6 +574,10 @@ public String getPIData() {
throw new UnsupportedOperationException();
}

static String prefixOf(String namespace) {
return String.valueOf(namespace.substring(namespace.lastIndexOf(':') + 1).charAt(0));
}

private static class DefaultLocation implements Location {
@Override
public int getLineNumber() {
Expand All @@ -569,4 +604,37 @@ public String getSystemId() {
return null;
}
}

private static class DocumentNamespaceContext implements NamespaceContext {
private final Map<String, String> namespaces;

DocumentNamespaceContext() {
List<String> names = Namespaces.all();
namespaces = new HashMap<>(names.size());
for (String namespace : names) {
String prefix = prefixOf(namespace);
namespaces.put(namespace, prefix);
}
}

@Override
public String getNamespaceURI(String prefix) {
return namespaces.entrySet()
.stream()
.filter(e -> e.getValue().equals(prefix))
.map(Map.Entry::getKey)
.findFirst()
.orElse(null);
}

@Override
public String getPrefix(String namespaceURI) {
return namespaces.get(namespaceURI);
}

@Override
public Iterator<String> getPrefixes(String namespaceURI) {
return Collections.singletonList(namespaces.get(namespaceURI)).iterator();
}
}
}
17 changes: 17 additions & 0 deletions src/main/java/io/xlate/edi/stream/EDIStreamConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
******************************************************************************/
package io.xlate.edi.stream;

import java.util.Arrays;
import java.util.List;

public interface EDIStreamConstants {

public static class Standards {
Expand All @@ -36,4 +39,18 @@ private Delimiters() {
public static final String DECIMAL = "io.xlate.edi.stream.delim.decimal";
public static final String RELEASE = "io.xlate.edi.stream.delim.release";
}

public static class Namespaces {
private Namespaces() {
}

public static final String LOOPS = "urn:xlate.io:staedi:names:loops";
public static final String SEGMENTS = "urn:xlate.io:staedi:names:segments";
public static final String COMPOSITES = "urn:xlate.io:staedi:names:composites";
public static final String ELEMENTS = "urn:xlate.io:staedi:names:elements";

public static final List<String> all() {
return Arrays.asList(LOOPS, SEGMENTS, COMPOSITES, ELEMENTS);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.Base64;
import java.util.concurrent.atomic.AtomicReference;

import javax.xml.namespace.NamespaceContext;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
Expand All @@ -50,6 +51,7 @@
import io.xlate.edi.schema.Schema;
import io.xlate.edi.schema.SchemaFactory;
import io.xlate.edi.stream.EDIInputFactory;
import io.xlate.edi.stream.EDIStreamConstants.Namespaces;
import io.xlate.edi.stream.EDIStreamFilter;
import io.xlate.edi.stream.EDIStreamReader;

Expand Down Expand Up @@ -150,6 +152,61 @@ public void testGetElementText() throws Exception {
assertEquals("00", xmlReader.getElementText());
}

@Test
public void testNamespaces() throws Exception {
XMLStreamReader xmlReader = getXmlReader(TINY_X12);

assertEquals(XMLStreamConstants.START_DOCUMENT, xmlReader.next());
assertEquals(XMLStreamConstants.START_ELEMENT, xmlReader.next());
assertEquals("INTERCHANGE", xmlReader.getLocalName());
assertEquals(Namespaces.LOOPS, xmlReader.getName().getNamespaceURI());
assertEquals("l", xmlReader.getName().getPrefix());
assertEquals(4, xmlReader.getNamespaceCount());
assertEquals(Namespaces.LOOPS, xmlReader.getNamespaceURI());

assertSegmentBoundaries(xmlReader, "ISA", 16);

NamespaceContext context = xmlReader.getNamespaceContext();
assertEquals("s", context.getPrefix(Namespaces.SEGMENTS));
assertEquals("s", context.getPrefixes(Namespaces.SEGMENTS).next());
assertEquals(Namespaces.SEGMENTS, context.getNamespaceURI("s"));
assertNull(context.getNamespaceURI("x"));
assertEquals(Namespaces.SEGMENTS, xmlReader.getNamespaceURI());

assertEquals(XMLStreamConstants.START_ELEMENT, xmlReader.next());
assertEquals("IEA", xmlReader.getLocalName());
assertEquals(Namespaces.SEGMENTS, xmlReader.getNamespaceURI());

// No namespaces declared on the segment
assertEquals(0, xmlReader.getNamespaceCount());
assertNull(xmlReader.getNamespacePrefix(0));
assertNull(xmlReader.getNamespaceURI(0));

assertElement(xmlReader, "IEA01", "1");
assertEquals(Namespaces.ELEMENTS, xmlReader.getNamespaceURI());

// No namespaces declared on the element
assertEquals(0, xmlReader.getNamespaceCount());
assertNull(xmlReader.getNamespacePrefix(0));
assertNull(xmlReader.getNamespaceURI(0));

assertElement(xmlReader, "IEA02", "508121953");
assertEquals(Namespaces.ELEMENTS, xmlReader.getNamespaceURI());

assertEquals(XMLStreamConstants.END_ELEMENT, xmlReader.next());
assertEquals("IEA", xmlReader.getLocalName());
assertEquals(Namespaces.SEGMENTS, xmlReader.getNamespaceURI());

assertEquals(XMLStreamConstants.END_ELEMENT, xmlReader.next());
assertEquals("INTERCHANGE", xmlReader.getLocalName());
assertEquals(Namespaces.LOOPS, xmlReader.getName().getNamespaceURI());
assertEquals("l", xmlReader.getName().getPrefix());
assertEquals(4, xmlReader.getNamespaceCount());
assertEquals(Namespaces.LOOPS, xmlReader.getNamespaceURI());

assertEquals(XMLStreamConstants.END_DOCUMENT, xmlReader.next());
}

private void assertElement(XMLStreamReader xmlReader, String tag, String value) throws Exception {
assertEquals(XMLStreamConstants.START_ELEMENT, xmlReader.next());
assertEquals(tag, xmlReader.getLocalName());
Expand Down Expand Up @@ -283,10 +340,6 @@ public boolean accept(EDIStreamReader reader) {
public void testUnsupportedOperations() throws Exception {
EDIStreamReader ediReader = Mockito.mock(EDIStreamReader.class);
XMLStreamReader xmlReader = new StaEDIXMLStreamReader(ediReader);
try {
xmlReader.getNamespaceURI();
fail("UnsupportedOperationExpected");
} catch (UnsupportedOperationException e) {}
try {
xmlReader.getAttributeValue("", "");
fail("UnsupportedOperationExpected");
Expand Down Expand Up @@ -319,22 +372,6 @@ public void testUnsupportedOperations() throws Exception {
xmlReader.isAttributeSpecified(0);
fail("UnsupportedOperationExpected");
} catch (UnsupportedOperationException e) {}
try {
xmlReader.getNamespacePrefix(0);
fail("UnsupportedOperationExpected");
} catch (UnsupportedOperationException e) {}
try {
xmlReader.getNamespaceURI(0);
fail("UnsupportedOperationExpected");
} catch (UnsupportedOperationException e) {}
try {
xmlReader.getNamespaceContext();
fail("UnsupportedOperationExpected");
} catch (UnsupportedOperationException e) {}
try {
xmlReader.getNamespaceURI("");
fail("UnsupportedOperationExpected");
} catch (UnsupportedOperationException e) {}
try {
xmlReader.getPITarget();
fail("UnsupportedOperationExpected");
Expand Down

0 comments on commit bee9e12

Please sign in to comment.