From c5b5cce8c1f296d45665627fb96e4b2e7fb9b84d Mon Sep 17 00:00:00 2001 From: "Day, Jeremy(jday)" Date: Thu, 17 Feb 2022 13:04:32 -0800 Subject: [PATCH 01/13] Initial Commit Signed-off-by: Day, Jeremy(jday) Signed-off-by: Jem Day --- .../core/impl/BaseCloudEventBuilder.java | 11 + formats/xml/pom.xml | 75 ++++++ .../io/cloudevents/xml/XMLCloudEventData.java | 20 ++ .../io/cloudevents/xml/XMLDataWrapper.java | 29 +++ .../io/cloudevents/xml/XMLDeserializer.java | 241 +++++++++++++++++ .../java/io/cloudevents/xml/XMLFormat.java | 72 +++++ .../io/cloudevents/xml/XMLSerializer.java | 245 ++++++++++++++++++ .../java/io/cloudevents/xml/XMLUtils.java | 57 ++++ .../io.cloudevents.core.format.EventFormat | 1 + .../io/cloudevents/xml/XMLFormatTest.java | 228 ++++++++++++++++ formats/xml/src/test/resources/v03/min.xml | 9 + .../xml/src/test/resources/v1/binary_attr.xml | 10 + .../xml/src/test/resources/v1/json_data.xml | 14 + .../test/resources/v1/json_data_with_ext.xml | 17 ++ formats/xml/src/test/resources/v1/min.xml | 9 + .../xml/src/test/resources/v1/text_data.xml | 13 + .../xml/src/test/resources/v1/xml_data.xml | 16 ++ .../test/resources/v1/xml_data_with_ns1.xml | 19 ++ .../test/resources/v1/xml_data_with_ns2.xml | 19 ++ .../test/resources/v1/xml_data_with_ns3.xml | 24 ++ pom.xml | 1 + 21 files changed, 1130 insertions(+) create mode 100644 formats/xml/pom.xml create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.java create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java create mode 100644 formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java create mode 100644 formats/xml/src/test/resources/v03/min.xml create mode 100644 formats/xml/src/test/resources/v1/binary_attr.xml create mode 100644 formats/xml/src/test/resources/v1/json_data.xml create mode 100644 formats/xml/src/test/resources/v1/json_data_with_ext.xml create mode 100644 formats/xml/src/test/resources/v1/min.xml create mode 100644 formats/xml/src/test/resources/v1/text_data.xml create mode 100644 formats/xml/src/test/resources/v1/xml_data.xml create mode 100644 formats/xml/src/test/resources/v1/xml_data_with_ns1.xml create mode 100644 formats/xml/src/test/resources/v1/xml_data_with_ns2.xml create mode 100644 formats/xml/src/test/resources/v1/xml_data_with_ns3.xml diff --git a/core/src/main/java/io/cloudevents/core/impl/BaseCloudEventBuilder.java b/core/src/main/java/io/cloudevents/core/impl/BaseCloudEventBuilder.java index 69846c8eb..dbc6a5875 100644 --- a/core/src/main/java/io/cloudevents/core/impl/BaseCloudEventBuilder.java +++ b/core/src/main/java/io/cloudevents/core/impl/BaseCloudEventBuilder.java @@ -124,6 +124,9 @@ public SELF withExtension(@Nonnull String key, @Nonnull String value) { return self; } + // @TODO - I think this method should be removed/deprecated + // **Number** Is NOT a valid CE Context atrribute type. + public SELF withExtension(@Nonnull String key, @Nonnull Number value) { if (!isValidExtensionName(key)) { throw CloudEventRWException.newInvalidExtensionName(key); @@ -132,6 +135,14 @@ public SELF withExtension(@Nonnull String key, @Nonnull Number value) { return self; } + public SELF withExtension(@Nonnull String key, @Nonnull Integer value) { + if (!isValidExtensionName(key)) { + throw CloudEventRWException.newInvalidExtensionName(key); + } + this.extensions.put(key, value); + return self; + } + public SELF withExtension(@Nonnull String key, @Nonnull Boolean value) { if (!isValidExtensionName(key)) { throw CloudEventRWException.newInvalidExtensionName(key); diff --git a/formats/xml/pom.xml b/formats/xml/pom.xml new file mode 100644 index 000000000..f2c4628fc --- /dev/null +++ b/formats/xml/pom.xml @@ -0,0 +1,75 @@ + + + + 4.0.0 + + + io.cloudevents + cloudevents-parent + 2.4.0-SNAPSHOT + ../../pom.xml + + + cloudevents-xml + CloudEvents - XML Format + jar + + + + + io.cloudevents + cloudevents-core + ${project.version} + + + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + + io.cloudevents + cloudevents-core + tests + test-jar + ${project.version} + test + + + + org.xmlunit + xmlunit-core + 2.9.0 + test + + + + + diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java new file mode 100644 index 000000000..6414cf2b4 --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java @@ -0,0 +1,20 @@ +package io.cloudevents.xml; + +import io.cloudevents.CloudEventData; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +public interface XMLCloudEventData extends CloudEventData { + + /** + * Get the XML {@link Element} representation of the + * CloudEvent data. + * + * @return The {@link Element} representation. + */ + Document getElement(); + + static CloudEventData wrap(Document xmlDoc) { + return new XMLDataWrapper(xmlDoc); + } +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java new file mode 100644 index 000000000..a11886f06 --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java @@ -0,0 +1,29 @@ +package io.cloudevents.xml; + +import io.cloudevents.rw.CloudEventRWException; +import org.w3c.dom.Document; + +import javax.xml.transform.TransformerException; + +class XMLDataWrapper implements XMLCloudEventData { + + private final Document xmlDoc; + + XMLDataWrapper(Document d) { + this.xmlDoc = d; + } + + @Override + public Document getElement() { + return xmlDoc; + } + + @Override + public byte[] toBytes() { + try { + return XMLUtils.documentToBytes(xmlDoc); + } catch (TransformerException e) { + throw CloudEventRWException.newOther(e); + } + } +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java new file mode 100644 index 000000000..9854a2d45 --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java @@ -0,0 +1,241 @@ +package io.cloudevents.xml; + +import io.cloudevents.CloudEventData; +import io.cloudevents.SpecVersion; +import io.cloudevents.core.data.BytesCloudEventData; +import io.cloudevents.rw.*; +import io.cloudevents.types.Time; +import org.w3c.dom.*; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.net.URI; +import java.util.Base64; + +public class XMLDeserializer implements CloudEventReader { + + static final String CE_NAMESPACE = "http://cloudevents.io/xmlformat/V1"; + static final String XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"; + static final String XS_NAMESPACE = "http://www.w3.org/2001/XMLSchema"; + private final Document xmlDocument; + + XMLDeserializer(Document doc) { + this.xmlDocument = doc; + } + + // CloudEventReader ------------------------------------------------------- + + @Override + public , R> R read( + CloudEventWriterFactory writerFactory, + CloudEventDataMapper mapper) throws CloudEventRWException { + + // Grab the Root and ensure it's what we expect. + final Element root = xmlDocument.getDocumentElement(); + + checkValidRootElement(root); + + // Get the specversion and build the CE Writer + String specVer = root.getAttribute("specversion"); + + if (specVer == null) { + // Error + } + + final SpecVersion specVersion = SpecVersion.parse(specVer); + final CloudEventWriter writer = writerFactory.create(specVersion); + + // Now iterate through the elements + + NodeList nodes = root.getChildNodes(); + Element dataElement = null; + + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + + if (node.getNodeType() == Node.ELEMENT_NODE) { + + Element e = (Element) node; + + // Sanity + if (isValidAttribute(e)) { + + // Grab all the useful markers. + final String attrName = e.getLocalName(); + final String attrType = extractAttributeType(e); + final String attrValue = e.getTextContent(); + + // Check if this is a Required or Optional attribute + if (specVersion.getAllAttributes().contains(attrName)) { + writer.withContextAttribute(attrName, attrValue); + } else { + if ("data".equals(attrName)) { + // Just remember the data node for now.. + dataElement = e; + } else { + // Handle the extension attributes + switch (attrType) { + case "xs:string": + writer.withContextAttribute(attrName, attrValue); + break; + case "xs:int": + writer.withContextAttribute(attrName, Integer.valueOf(attrValue)); + break; + case "xs:dateTime": + writer.withContextAttribute(attrName, Time.parseTime(attrValue)); + break; + case "xs:boolean": + writer.withContextAttribute(attrName, Boolean.valueOf(attrValue)); + break; + case "xs:anyURI": + writer.withContextAttribute(attrName, URI.create(attrValue)); + break; + case "xs:base64Binary": + writer.withContextAttribute(attrName, Base64.getDecoder().decode(attrValue)); + break; + } + } + } + } + } + } + + // And handle any data + + if (dataElement != null) { + return writer.end(processData(dataElement)); + } else { + return writer.end(); + } + + } + + // Private Methods -------------------------------------------------------- + + private Element findFirstElement(Element e) { + + NodeList nodeList = e.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node n = nodeList.item(i); + + if (n.getNodeType() == Node.ELEMENT_NODE) { + return (Element) n; + } + } + + return null; + } + + private CloudEventData processData(Element data) throws CloudEventRWException { + CloudEventData retVal = null; + + final String attrType = extractAttributeType(data); + + switch (attrType) { + case "xs:string": + retVal = new TextCloudEventData(data.getTextContent()); + break; + case "xs:base64Binary": + String eData = data.getTextContent(); + retVal = BytesCloudEventData.wrap(Base64.getDecoder().decode(eData)); + break; + case "xs:any": + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + Document newDoc = dbf.newDocumentBuilder().newDocument(); + + Element eventData = findFirstElement(data); + + Element newRoot = newDoc.createElementNS(eventData.getNamespaceURI(), eventData.getLocalName()); + newDoc.appendChild(newRoot); + + // Copy the children... + NodeList nodesToCopy = eventData.getChildNodes(); + + for (int i = 0; i < nodesToCopy.getLength(); i++) { + Node n = nodesToCopy.item(i); + + if (n.getNodeType() == Node.ELEMENT_NODE) { + Node newNode = newDoc.importNode(n, true); + newRoot.appendChild(newNode); + } + } + + newDoc.normalizeDocument(); + retVal = XMLCloudEventData.wrap(newDoc); + + } catch (ParserConfigurationException e) { + throw CloudEventRWException.newDataConversion(e, null, null); + } + break; + default: + break; + } + + return retVal; + } + + private void checkValidRootElement(Element e) throws CloudEventRWException { + + if (!"event".equals(e.getLocalName())) { + throw CloudEventRWException.newInvalidDataType(e.getLocalName(), "event"); + + } + + if (!CE_NAMESPACE.equalsIgnoreCase(e.getNamespaceURI())) { + throw CloudEventRWException.newInvalidDataType(e.getNamespaceURI(), "Namespace: " + CE_NAMESPACE); + } + } + + private boolean isValidAttribute(Node n) { + + if (!CE_NAMESPACE.equals(n.getNamespaceURI())) { + return false; + } + + return allLowerCase(n.getLocalName()); + + } + + private String extractAttributeType(Element e) { + + Attr a = e.getAttributeNodeNS(XSI_NAMESPACE, "type"); + + if (a != null) { + return a.getValue(); + } else { + return null; + } + } + + private boolean allLowerCase(String s) { + if (s == null) { + return false; + } + + for (int i = 0; i < s.length(); i++) { + if (Character.isUpperCase(s.charAt(i))) { + return false; + } + } + + return true; + } + + // DataWrapper Inner Classes + + public class TextCloudEventData implements CloudEventData { + + private final String text; + + TextCloudEventData(String text) { + this.text = text; + } + + @Override + public byte[] toBytes() { + return text.getBytes(); + } + } + +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.java new file mode 100644 index 000000000..2ab57fee8 --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.java @@ -0,0 +1,72 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.cloudevents.xml; + +import io.cloudevents.CloudEvent; +import io.cloudevents.CloudEventData; +import io.cloudevents.core.builder.CloudEventBuilder; +import io.cloudevents.core.format.EventDeserializationException; +import io.cloudevents.core.format.EventFormat; +import io.cloudevents.core.format.EventSerializationException; +import io.cloudevents.rw.CloudEventDataMapper; +import org.w3c.dom.Document; + +import javax.xml.transform.TransformerException; + +/** + * An implemmentation of {@link EventFormat} for the XML Format. + * This format is resolvable with {@link io.cloudevents.core.provider.EventFormatProvider} using the content type {@link #XML_CONTENT_TYPE}. + *

+ * This {@link EventFormat} only works for {@link io.cloudevents.SpecVersion#V1}, as that was the first version the XML format was defined for. + */ +public class XMLFormat implements EventFormat { + + /** + * The content type for transports sending cloudevents in the protocol buffer format. + */ + public static final String XML_CONTENT_TYPE = "application/cloudevents+xml"; + + + @Override + public byte[] serialize(CloudEvent event) throws EventSerializationException { + + // Convert the CE into an XML Document + Document d = XMLSerializer.toDocument(event); + + try { + // Write out the XML Document + return XMLUtils.documentToBytes(d); + } catch (TransformerException e) { + throw new EventSerializationException(e); + } + } + + @Override + public CloudEvent deserialize(byte[] bytes, CloudEventDataMapper mapper) + throws EventDeserializationException { + + final Document doc = XMLUtils.parseIntoDocument(bytes); + return new XMLDeserializer(doc).read(CloudEventBuilder::fromSpecVersion); + + } + + @Override + public String serializedContentType() { + return XML_CONTENT_TYPE; + } + +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java new file mode 100644 index 000000000..39984c914 --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java @@ -0,0 +1,245 @@ +package io.cloudevents.xml; + +import io.cloudevents.CloudEvent; +import io.cloudevents.CloudEventData; +import io.cloudevents.SpecVersion; +import io.cloudevents.core.CloudEventUtils; +import io.cloudevents.core.format.EventSerializationException; +import io.cloudevents.rw.CloudEventContextReader; +import io.cloudevents.rw.CloudEventContextWriter; +import io.cloudevents.rw.CloudEventRWException; +import io.cloudevents.rw.CloudEventWriter; +import io.cloudevents.types.Time; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.net.URI; +import java.time.OffsetDateTime; +import java.util.Base64; + +class XMLSerializer { + + /** + * Convert the CloudEvent to an XML DOM representation. + * + * @param ce + * @return + */ + static Document toDocument(CloudEvent ce) { + + // Setup the writer + XMLCloudEventWriter eventWriter = new XMLCloudEventWriter(ce.getSpecVersion()); + + // Process the Context Attributes + final CloudEventContextReader cloudEventContextReader = CloudEventUtils.toContextReader(ce); + cloudEventContextReader.readContext(eventWriter); + + // Now handle the Data + + final CloudEventData data = ce.getData(); + if (data != null) { + return eventWriter.end(data); + } else { + return eventWriter.end(); + } + } + + private static class XMLCloudEventWriter implements CloudEventWriter { + + static final String XSI_TYPE = "xsi:type"; + static final String CLOUDEVENT_NAMESPACE = "http://cloudevents.io/xmlformat/V1"; + static final String XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"; + static final String XS_NAMESPACE = "http://www.w3.org/2001/XMLSchema"; + static final String CE_ROOT_ELEMENT = "event"; + + private final Document xmlDocument; + private final Element root; + private final SpecVersion specVersion; + private String dataContentType; + + XMLCloudEventWriter(SpecVersion specVersion) throws EventSerializationException { + + this.specVersion = specVersion; + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + DocumentBuilder xmlBuilder = null; + try { + xmlBuilder = dbf.newDocumentBuilder(); + xmlDocument = xmlBuilder.newDocument(); + } catch (ParserConfigurationException e) { + throw new EventSerializationException(e); + } + + // Start the Document + root = xmlDocument.createElementNS(CLOUDEVENT_NAMESPACE, CE_ROOT_ELEMENT); + root.setAttribute("xmlns:xs", XS_NAMESPACE); + root.setAttribute("xmlns:xsi", XSI_NAMESPACE); + root.setAttribute("specversion", specVersion.toString()); + xmlDocument.appendChild(root); + } + + /** + * Add a context attribute to the root element. + * + * @param name + * @param xsiType + * @param value + */ + private void addElement(String name, String xsiType, String value) { + + Element e = xmlDocument.createElement(name); + + // If this is one of the REQUIRED or OPTIONAL context attributes then we + // don't need to communicate the type information. + + if (!specVersion.getAllAttributes().contains(name)) { + e.setAttribute(XSI_TYPE, xsiType); + } + e.setTextContent(value); + + root.appendChild(e); + + // Look for, and remember, the data content type + if ("datacontenttype".equals(name)) { + dataContentType = value; + } + } + + /** + * Need refactoring.. + * + * @param contentType + * @return + */ + private boolean isTextContent(String contentType) { + + if (contentType == null) { + return false; + } + + return contentType.startsWith("text/") + || "application/json".equals(contentType) + || "application/xml".equals(contentType) + || contentType.endsWith("+json") + || contentType.endsWith("+xml") + ; + } + + private boolean isXMLContent(String contentType) { + if (contentType == null) { + return false; + } + + return "application/xml".equals(contentType) + || "text/xml".equals(contentType) + || contentType.endsWith("+xml") + ; + } + + private void writeBinaryData(byte[] data) { + addElement("data", "xs:base64Binary", Base64.getEncoder().encodeToString(data)); + } + + private void writeXmlData(Document dataDoc) { + + // Create the wrapper + Element e = xmlDocument.createElement("data"); + e.setAttribute(XSI_TYPE, "xs:any"); + root.appendChild(e); + + // Get the Root Element + Element dataRoot = dataDoc.getDocumentElement(); + + // Copy the element into our document + Node newNode = xmlDocument.importNode(dataRoot, true); + + // And add it to data holder. + e.appendChild(newNode); + } + + private void writeXmlData(byte[] data) { + writeXmlData(XMLUtils.parseIntoDocument(data)); + } + + // CloudEvent Writer ------------------------------------------------------------ + + @Override + public CloudEventContextWriter withContextAttribute(String name, String value) throws CloudEventRWException { + + addElement(name, "xs:string", value); + return this; + } + + @Override + public CloudEventContextWriter withContextAttribute(String name, URI value) throws CloudEventRWException { + + addElement(name, "xs:anyURI", value.toString()); + return this; + } + + @Override + public CloudEventContextWriter withContextAttribute(String name, OffsetDateTime value) throws CloudEventRWException { + + addElement(name, "xs:dateTime", Time.writeTime(value)); + return this; + } + + @Override + public CloudEventContextWriter withContextAttribute(String name, Number value) throws CloudEventRWException { + + if (value instanceof Integer) { + return withContextAttribute(name, (Integer) value); + } else { + return withContextAttribute(name, String.valueOf(value)); + } + } + + @Override + public CloudEventContextWriter withContextAttribute(String name, Integer value) throws CloudEventRWException { + + addElement(name, "xs:int", value.toString()); + return this; + } + + @Override + public CloudEventContextWriter withContextAttribute(String name, Boolean value) throws CloudEventRWException { + + addElement(name, "xs:boolean", value.toString()); + return this; + } + + @Override + public CloudEventContextWriter withContextAttribute(String name, byte[] value) throws CloudEventRWException { + + addElement(name, "xs:base64Binary", Base64.getEncoder().encodeToString(value)); + return this; + } + + @Override + public Document end(CloudEventData data) throws CloudEventRWException { + + if (data instanceof XMLCloudEventData) { + writeXmlData(((XMLCloudEventData) data).getElement()); + } else if (isXMLContent(dataContentType)) { + writeXmlData(data.toBytes()); + } else if (isTextContent(dataContentType)) { + // Handle Textual Content + addElement("data", "xs:string", new String(data.toBytes())); + } else { + // Handle Binary Content + writeBinaryData(data.toBytes()); + } + return end(); + } + + @Override + public Document end() throws CloudEventRWException { + return xmlDocument; + } + } +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java new file mode 100644 index 000000000..ffd51b4fa --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java @@ -0,0 +1,57 @@ +package io.cloudevents.xml; + +import io.cloudevents.rw.CloudEventRWException; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +class XMLUtils { + + // Prevent Construction + private XMLUtils() { + } + + static Document parseIntoDocument(byte[] data) throws CloudEventRWException { + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + + try { + DocumentBuilder builder = dbf.newDocumentBuilder(); + return builder.parse(new ByteArrayInputStream(data)); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw CloudEventRWException.newOther(e); + } + + } + + static byte[] documentToBytes(Document doc) throws TransformerException { + + // Build our transformer + TransformerFactory tFactory = TransformerFactory.newInstance(); + Transformer t = tFactory.newTransformer(); + + // Assign the source and result + Source src = new DOMSource(doc); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + StreamResult result = new StreamResult(os); + + // Write out the document + t.transform(src, result); + + // And we're done + return os.toByteArray(); + } +} diff --git a/formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat b/formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat new file mode 100644 index 000000000..ae492de58 --- /dev/null +++ b/formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat @@ -0,0 +1 @@ +io.cloudevents.protobuf.XMLFormat diff --git a/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java b/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java new file mode 100644 index 000000000..67eb84220 --- /dev/null +++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java @@ -0,0 +1,228 @@ +package io.cloudevents.xml; + +import io.cloudevents.CloudEvent; +import io.cloudevents.core.format.EventFormat; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.xmlunit.builder.DiffBuilder; +import org.xmlunit.builder.Input; +import org.xmlunit.diff.*; + +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; +import java.io.*; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.stream.Stream; + +import static io.cloudevents.core.test.Data.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class XMLFormatTest { + + private final EventFormat format = new XMLFormat(); + + @Test + public void testRegistration() { + assertThat(format.serializedContentType()).isNotNull(); + assertThat(format.serializedContentType()).isEqualTo("application/cloudevents+xml"); + } + + @ParameterizedTest + @MethodSource("serializeTestArgumentsDefault") + /** + * 1. Serialized a CloudEvent object into XML. + * 2. Compare the serialized output with the expected (control) content. + */ + public void serialize(io.cloudevents.CloudEvent input, String xmlFile) throws IOException { + + System.out.println("Serialize(" + xmlFile + ")"); + + // Serialize the event. + byte[] raw = format.serialize(input); + + Assertions.assertNotNull(raw); + Assertions.assertTrue(raw.length > 0); + + System.out.println("Serialized Size : " + raw.length + " bytes"); + + if (xmlFile != null) { + + Source expectedSource = getTestSource(xmlFile); + Source actualSource = Input.fromByteArray(raw).build(); + + assertThat(expectedSource).isNotNull(); + assertThat(actualSource).isNotNull(); + + // Now compare the documents + + Diff diff = DiffBuilder.compare(expectedSource) + .withTest(actualSource) + .ignoreComments() + .ignoreElementContentWhitespace() + .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byName)) + .checkForSimilar() + .build(); + + if (diff.hasDifferences()){ + + // Dump what was actually generated. + dumpXml(raw); + + for (Difference d : diff.getDifferences()) { + System.out.println(d); + } + } + Assertions.assertFalse(diff.hasDifferences(), diff.toString()); + } + + } + + public static Stream serializeTestArgumentsDefault() { + return Stream.of( + Arguments.of(V1_MIN, "v1/min.xml"), + Arguments.of(V1_WITH_JSON_DATA, "v1/json_data.xml"), + Arguments.of(V1_WITH_TEXT_DATA, "v1/text_data.xml"), + Arguments.of(V1_WITH_JSON_DATA_WITH_EXT, "v1/json_data_with_ext.xml"), + Arguments.of(V1_WITH_XML_DATA, "v1/xml_data.xml"), + Arguments.of(V1_WITH_BINARY_EXT, "v1/binary_attr.xml"), + + Arguments.of(V03_MIN, "v03/min.xml") + + ); + } + + @ParameterizedTest + @MethodSource("deserializeArgs") + /** + * Basic test to deserialize an XML representation into + * a CloudEvent - no correctness checks. + */ + public void deserialize(String xmlFile) throws IOException { + + // Get the test data + byte[] data = getData(xmlFile); + + assertThat(data).isNotNull(); + assertThat(data).isNotEmpty(); + + // Attempt deserialize + CloudEvent ce = format.deserialize(data); + + // Did we return something + assertThat(ce).isNotNull(); + } + + @ParameterizedTest + @MethodSource("deserializeArgs") + /** + * Round-trip test starting with an XML Formated event + * 1. Deserialize an XML Formated Event into a CE + * 2. Serialize the CE back into XML + * 3. Compare the orginal (expected) and new XML document + */ + public void roundTrip(String fileName) throws IOException { + + byte[] inputData = getData(fileName); + + // (1) DeSerialize + CloudEvent ce = format.deserialize(inputData); + assertThat(ce).isNotNull(); + + // (2) Serialize + byte[] outputData = format.serialize(ce); + assertThat(outputData).isNotNull(); + assertThat(outputData).isNotEmpty(); + + // (3) Compare the two XML Documents + Source expectedSource = getStreamSource(inputData); + Source actualSource = getStreamSource(outputData); + + Diff diff = DiffBuilder.compare(expectedSource) + .withTest(actualSource) + .ignoreComments() + .ignoreElementContentWhitespace() + .checkForSimilar() + .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byName)) + .build(); + + if (diff.hasDifferences()){ + dumpXml(outputData); + + if (diff.hasDifferences()){ + for (Difference d : diff.getDifferences()) { + System.out.println(d); + } + } + } + Assertions.assertFalse(diff.hasDifferences()); + + } + + public static Stream deserializeArgs() { + return Stream.of( + + Arguments.of("v1/min.xml"), + Arguments.of("v1/text_data.xml"), + Arguments.of("v1/json_data.xml"), + Arguments.of("v1/binary_attr.xml"), + Arguments.of("v1/json_data_with_ext.xml"), + Arguments.of("v1/xml_data.xml"), + Arguments.of("v1/xml_data_with_ns1.xml"), + Arguments.of("v1/xml_data_with_ns2.xml"), + Arguments.of("v1/xml_data_with_ns3.xml") + ); + } + + //------------------------------------------------------- + + private static File getFile(String filename) throws IOException { + URL file = Thread.currentThread().getContextClassLoader().getResource(filename); + assertThat(file).isNotNull(); + File dataFile = new File(file.getFile()); + assertThat(dataFile).isNotNull(); + return dataFile; + } + + private static Reader getReader(String filename) throws IOException { + File dataFile = getFile(filename); + return new FileReader(dataFile); + } + + private static byte[] getData(String filename) throws IOException { + File f = getFile(filename); + return Files.readAllBytes(f.toPath()); + } + + private StreamSource getStreamSource(byte[] data) { + ByteArrayInputStream bais = new ByteArrayInputStream(data); + return new StreamSource(bais); + } + + private Source getTestSource(String filename) throws IOException { + return Input.fromFile(getFile(filename)).build(); + } + + private void dumpXml(byte[] data) { + System.out.println(dumpAsString(data)); + } + + private String dumpAsString(byte[] data) { + ByteBuffer bb = ByteBuffer.wrap(data); + return StandardCharsets.UTF_8.decode(bb).toString(); + } + + + static class CustomComparisonFormatter extends DefaultComparisonFormatter { + + @Override + public String getDetails(Comparison.Detail difference, ComparisonType type, boolean formatXml) { + return super.getDetails(difference, type, formatXml); + } + } +} diff --git a/formats/xml/src/test/resources/v03/min.xml b/formats/xml/src/test/resources/v03/min.xml new file mode 100644 index 000000000..7aba7107c --- /dev/null +++ b/formats/xml/src/test/resources/v03/min.xml @@ -0,0 +1,9 @@ + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/v1/binary_attr.xml b/formats/xml/src/test/resources/v1/binary_attr.xml new file mode 100644 index 000000000..7651a227b --- /dev/null +++ b/formats/xml/src/test/resources/v1/binary_attr.xml @@ -0,0 +1,10 @@ + + + 1 + http://localhost/source + mock.test + 4P8ARKo= + diff --git a/formats/xml/src/test/resources/v1/json_data.xml b/formats/xml/src/test/resources/v1/json_data.xml new file mode 100644 index 000000000..bc7199220 --- /dev/null +++ b/formats/xml/src/test/resources/v1/json_data.xml @@ -0,0 +1,14 @@ + + + http://localhost/schema + application/json + {} + sub + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/v1/json_data_with_ext.xml b/formats/xml/src/test/resources/v1/json_data_with_ext.xml new file mode 100644 index 000000000..bbd74bfd7 --- /dev/null +++ b/formats/xml/src/test/resources/v1/json_data_with_ext.xml @@ -0,0 +1,17 @@ + + + http://localhost/schema + application/json + {} + sub + + 1 + http://localhost/source + mock.test + aaa + true + 10 + diff --git a/formats/xml/src/test/resources/v1/min.xml b/formats/xml/src/test/resources/v1/min.xml new file mode 100644 index 000000000..8e5ef5131 --- /dev/null +++ b/formats/xml/src/test/resources/v1/min.xml @@ -0,0 +1,9 @@ + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/v1/text_data.xml b/formats/xml/src/test/resources/v1/text_data.xml new file mode 100644 index 000000000..43f84e5f7 --- /dev/null +++ b/formats/xml/src/test/resources/v1/text_data.xml @@ -0,0 +1,13 @@ + + + 1 + http://localhost/source + mock.test + text/plain + Hello World Lorena! + sub + + diff --git a/formats/xml/src/test/resources/v1/xml_data.xml b/formats/xml/src/test/resources/v1/xml_data.xml new file mode 100644 index 000000000..476c2febf --- /dev/null +++ b/formats/xml/src/test/resources/v1/xml_data.xml @@ -0,0 +1,16 @@ + + + 1 + http://localhost/source + mock.test + application/xml + + + + + sub + + diff --git a/formats/xml/src/test/resources/v1/xml_data_with_ns1.xml b/formats/xml/src/test/resources/v1/xml_data_with_ns1.xml new file mode 100644 index 000000000..9e17f8e94 --- /dev/null +++ b/formats/xml/src/test/resources/v1/xml_data_with_ns1.xml @@ -0,0 +1,19 @@ + + + 1 + http://localhost/source + mock.test + application/xml + + + + 51.509865 + -0.118092 + + + sub + + diff --git a/formats/xml/src/test/resources/v1/xml_data_with_ns2.xml b/formats/xml/src/test/resources/v1/xml_data_with_ns2.xml new file mode 100644 index 000000000..afe389e7b --- /dev/null +++ b/formats/xml/src/test/resources/v1/xml_data_with_ns2.xml @@ -0,0 +1,19 @@ + + + 1 + http://localhost/source + mock.test + application/xml + + + + 51.509865 + -0.118092 + + + sub + + diff --git a/formats/xml/src/test/resources/v1/xml_data_with_ns3.xml b/formats/xml/src/test/resources/v1/xml_data_with_ns3.xml new file mode 100644 index 000000000..0983eb1c8 --- /dev/null +++ b/formats/xml/src/test/resources/v1/xml_data_with_ns3.xml @@ -0,0 +1,24 @@ + + + 1 + http://localhost/source + mock.test + application/xml + + + + 51.509865 + -0.118092 + + + sub + + diff --git a/pom.xml b/pom.xml index 6be267b78..db0575fc1 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,7 @@ core formats/json-jackson formats/protobuf + formats/xml amqp http/basic http/vertx From 7b306cfef214dfccc57186670f52bcead1690ff0 Mon Sep 17 00:00:00 2001 From: "Day, Jeremy(jday)" Date: Thu, 17 Feb 2022 16:33:26 -0800 Subject: [PATCH 02/13] tweaks Signed-off-by: Day, Jeremy(jday) Signed-off-by: Jem Day --- .../io/cloudevents/xml/XMLCloudEventData.java | 33 ++++++++-- .../io/cloudevents/xml/XMLDataWrapper.java | 19 +++++- .../io/cloudevents/xml/XMLDeserializer.java | 16 +++++ .../io/cloudevents/xml/XMLSerializer.java | 26 ++++++-- .../java/io/cloudevents/xml/XMLUtils.java | 17 +++++ .../java/io/cloudevents/xml/TestUtils.java | 31 ++++++++++ .../cloudevents/xml/XMLDataWrapperTest.java | 54 ++++++++++++++++ .../io/cloudevents/xml/XMLFormatTest.java | 62 ++++++++++++------- .../src/test/resources/v1/with_extensions.xml | 12 ++++ 9 files changed, 236 insertions(+), 34 deletions(-) create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/XMLDataWrapperTest.java create mode 100644 formats/xml/src/test/resources/v1/with_extensions.xml diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java index 6414cf2b4..b24727169 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java @@ -1,19 +1,44 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package io.cloudevents.xml; import io.cloudevents.CloudEventData; import org.w3c.dom.Document; -import org.w3c.dom.Element; +/** + * A variant of {@link CloudEventData} support direct access + * to data as an XML Document. + */ public interface XMLCloudEventData extends CloudEventData { /** - * Get the XML {@link Element} representation of the + * Get the XML {@link Document} representation of the * CloudEvent data. * - * @return The {@link Element} representation. + * @return The {@link Document} representation. */ - Document getElement(); + Document getDocument(); + /** + * Wraps an XML {@link Document} + * @param xmlDoc {@link Document} + * @return {@link XMLCloudEventData} + */ static CloudEventData wrap(Document xmlDoc) { return new XMLDataWrapper(xmlDoc); } diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java index a11886f06..7dd19e132 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java @@ -1,3 +1,20 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package io.cloudevents.xml; import io.cloudevents.rw.CloudEventRWException; @@ -14,7 +31,7 @@ class XMLDataWrapper implements XMLCloudEventData { } @Override - public Document getElement() { + public Document getDocument() { return xmlDoc; } diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java index 9854a2d45..02777d396 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ package io.cloudevents.xml; import io.cloudevents.CloudEventData; diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java index 39984c914..e0780dfdd 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java @@ -1,3 +1,20 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package io.cloudevents.xml; import io.cloudevents.CloudEvent; @@ -141,10 +158,6 @@ private boolean isXMLContent(String contentType) { ; } - private void writeBinaryData(byte[] data) { - addElement("data", "xs:base64Binary", Base64.getEncoder().encodeToString(data)); - } - private void writeXmlData(Document dataDoc) { // Create the wrapper @@ -224,7 +237,7 @@ public CloudEventContextWriter withContextAttribute(String name, byte[] value) t public Document end(CloudEventData data) throws CloudEventRWException { if (data instanceof XMLCloudEventData) { - writeXmlData(((XMLCloudEventData) data).getElement()); + writeXmlData(((XMLCloudEventData) data).getDocument()); } else if (isXMLContent(dataContentType)) { writeXmlData(data.toBytes()); } else if (isTextContent(dataContentType)) { @@ -232,7 +245,8 @@ public Document end(CloudEventData data) throws CloudEventRWException { addElement("data", "xs:string", new String(data.toBytes())); } else { // Handle Binary Content - writeBinaryData(data.toBytes()); + final String encodedValue = Base64.getEncoder().encodeToString(data.toBytes()); + addElement ("data", "xs:base64Binary",encodedValue); } return end(); } diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java index ffd51b4fa..286d75488 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java @@ -1,3 +1,20 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package io.cloudevents.xml; import io.cloudevents.rw.CloudEventRWException; diff --git a/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java b/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java new file mode 100644 index 000000000..a2bb7a6b5 --- /dev/null +++ b/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java @@ -0,0 +1,31 @@ +package io.cloudevents.xml; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.net.URL; +import java.nio.file.Files; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestUtils { + + static File getFile(String filename) throws IOException { + URL file = Thread.currentThread().getContextClassLoader().getResource(filename); + assertThat(file).isNotNull(); + File dataFile = new File(file.getFile()); + assertThat(dataFile).isNotNull(); + return dataFile; + } + + static Reader getReader(String filename) throws IOException { + File dataFile = getFile(filename); + return new FileReader(dataFile); + } + + static byte[] getData(String filename) throws IOException { + File f = getFile(filename); + return Files.readAllBytes(f.toPath()); + } +} diff --git a/formats/xml/src/test/java/io/cloudevents/xml/XMLDataWrapperTest.java b/formats/xml/src/test/java/io/cloudevents/xml/XMLDataWrapperTest.java new file mode 100644 index 000000000..653e53d01 --- /dev/null +++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLDataWrapperTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.cloudevents.xml; + +import io.cloudevents.CloudEventData; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class XMLDataWrapperTest { + + @Test + /** + * Verify that the extension attributes are correctly + * handled. + */ + public void verifyWrapping() throws IOException { + + byte[] raw = TestUtils.getData("v1/min.xml"); + Document d = XMLUtils.parseIntoDocument(raw); + + CloudEventData cde = XMLCloudEventData.wrap(d); + assertThat(cde).isNotNull(); + assertThat(cde).isInstanceOf(CloudEventData.class); + + // We should be able to get the byte data + byte[] data = cde.toBytes(); + assertThat(data).isNotNull(); + assertThat(data).isNotEmpty(); + + // Now verify our variant + XMLCloudEventData xcde = (XMLCloudEventData) cde; + assertThat(xcde.getDocument()).isNotNull(); + + } +} diff --git a/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java b/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java index 67eb84220..6a35ccb5d 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java @@ -1,3 +1,20 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package io.cloudevents.xml; import io.cloudevents.CloudEvent; @@ -14,10 +31,8 @@ import javax.xml.transform.Source; import javax.xml.transform.stream.StreamSource; import java.io.*; -import java.net.URL; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.util.stream.Stream; import static io.cloudevents.core.test.Data.*; @@ -33,6 +48,25 @@ public void testRegistration() { assertThat(format.serializedContentType()).isEqualTo("application/cloudevents+xml"); } + @Test + public void verifyExtensions() throws IOException + { + byte[] raw = TestUtils.getData("v1/with_extensions.xml"); + + CloudEvent ce = format.deserialize(raw); + assertThat(ce).isNotNull(); + + assertExtension(ce, "myinteger", new Integer(42)); + assertExtension(ce, "mystring", "Greetings"); + assertExtension(ce, "myboolean", Boolean.FALSE); + } + + private void assertExtension(CloudEvent ce, String name, Object expected) { + assertThat(ce.getExtension(name)).isNotNull(); + assertThat(ce.getExtension(name)).isInstanceOf(expected.getClass()); + assertThat(ce.getExtension(name)).isEqualTo(expected); + } + @ParameterizedTest @MethodSource("serializeTestArgumentsDefault") /** @@ -106,7 +140,7 @@ public static Stream serializeTestArgumentsDefault() { public void deserialize(String xmlFile) throws IOException { // Get the test data - byte[] data = getData(xmlFile); + byte[] data = TestUtils.getData(xmlFile); assertThat(data).isNotNull(); assertThat(data).isNotEmpty(); @@ -128,7 +162,7 @@ public void deserialize(String xmlFile) throws IOException { */ public void roundTrip(String fileName) throws IOException { - byte[] inputData = getData(fileName); + byte[] inputData = TestUtils.getData(fileName); // (1) DeSerialize CloudEvent ce = format.deserialize(inputData); @@ -181,31 +215,13 @@ public static Stream deserializeArgs() { //------------------------------------------------------- - private static File getFile(String filename) throws IOException { - URL file = Thread.currentThread().getContextClassLoader().getResource(filename); - assertThat(file).isNotNull(); - File dataFile = new File(file.getFile()); - assertThat(dataFile).isNotNull(); - return dataFile; - } - - private static Reader getReader(String filename) throws IOException { - File dataFile = getFile(filename); - return new FileReader(dataFile); - } - - private static byte[] getData(String filename) throws IOException { - File f = getFile(filename); - return Files.readAllBytes(f.toPath()); - } - private StreamSource getStreamSource(byte[] data) { ByteArrayInputStream bais = new ByteArrayInputStream(data); return new StreamSource(bais); } private Source getTestSource(String filename) throws IOException { - return Input.fromFile(getFile(filename)).build(); + return Input.fromFile(TestUtils.getFile(filename)).build(); } private void dumpXml(byte[] data) { diff --git a/formats/xml/src/test/resources/v1/with_extensions.xml b/formats/xml/src/test/resources/v1/with_extensions.xml new file mode 100644 index 000000000..33fd9c6d6 --- /dev/null +++ b/formats/xml/src/test/resources/v1/with_extensions.xml @@ -0,0 +1,12 @@ + + + 1 + http://localhost/source + mock.test + 42 + Greetings + false + From 71c8e5b97d638a13b81e450427dc47b66523c848 Mon Sep 17 00:00:00 2001 From: "Day, Jeremy(jday)" Date: Fri, 18 Feb 2022 16:02:57 -0800 Subject: [PATCH 03/13] More negative tests Signed-off-by: Day, Jeremy(jday) Signed-off-by: Jem Day --- .../io/cloudevents/xml/XMLDeserializer.java | 11 ++++- .../io/cloudevents/xml/BadInputDataTest.java | 40 +++++++++++++++++++ .../io/cloudevents/xml/XMLFormatTest.java | 2 + .../test/resources/bad/bad_data_content.xml | 16 ++++++++ .../src/test/resources/bad/bad_data_ns.xml | 13 ++++++ .../src/test/resources/bad/bad_malformed.xml | 13 ++++++ .../resources/bad/bad_missing_data_ns.xml | 13 ++++++ .../xml/src/test/resources/bad/bad_no_ns.xml | 10 +++++ formats/xml/src/test/resources/bad/bad_ns.xml | 12 ++++++ 9 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java create mode 100644 formats/xml/src/test/resources/bad/bad_data_content.xml create mode 100644 formats/xml/src/test/resources/bad/bad_data_ns.xml create mode 100644 formats/xml/src/test/resources/bad/bad_malformed.xml create mode 100644 formats/xml/src/test/resources/bad/bad_missing_data_ns.xml create mode 100644 formats/xml/src/test/resources/bad/bad_no_ns.xml create mode 100644 formats/xml/src/test/resources/bad/bad_ns.xml diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java index 02777d396..105cba9d8 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java @@ -145,6 +145,8 @@ private Element findFirstElement(Element e) { private CloudEventData processData(Element data) throws CloudEventRWException { CloudEventData retVal = null; + + final String attrType = extractAttributeType(data); switch (attrType) { @@ -157,12 +159,19 @@ private CloudEventData processData(Element data) throws CloudEventRWException { break; case "xs:any": try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document newDoc = dbf.newDocumentBuilder().newDocument(); Element eventData = findFirstElement(data); + String eventDataNS = eventData.getNamespaceURI(); + + // Ensure the Data isn't in our namespace + if (CE_NAMESPACE.equals(eventDataNS)){ + throw CloudEventRWException.newInvalidDataType("data namespace: "+data.getNamespaceURI(), "Anything but " + CE_NAMESPACE); + } - Element newRoot = newDoc.createElementNS(eventData.getNamespaceURI(), eventData.getLocalName()); + Element newRoot = newDoc.createElementNS(eventDataNS, eventData.getLocalName()); newDoc.appendChild(newRoot); // Copy the children... diff --git a/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java b/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java new file mode 100644 index 000000000..6f5d55ade --- /dev/null +++ b/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java @@ -0,0 +1,40 @@ +package io.cloudevents.xml; + +import io.cloudevents.core.format.EventFormat; +import io.cloudevents.rw.CloudEventRWException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.io.IOException; +import java.util.stream.Stream; + +/** + * A seperate Test set to hold the test cases related + * to dealing with invalid representations + */ +public class BadInputDataTest { + + private final EventFormat format = new XMLFormat(); + + @ParameterizedTest + @MethodSource("badExampleFiles") + public void verifyRejection(String fileName) throws IOException { + + byte[] data = TestUtils.getData(fileName); + + assertThatExceptionOfType(CloudEventRWException.class).isThrownBy(() -> {format.deserialize(data);}); + } + + public static Stream badExampleFiles() { + return Stream.of( + Arguments.of("bad/bad_ns.xml"), + Arguments.of("bad/bad_no_ns.xml"), + Arguments.of("bad/bad_missing_data_ns.xml"), + Arguments.of("bad/bad_malformed.xml"), + Arguments.of("bad/bad_data_ns.xml"), + Arguments.of("bad/bad_data_content.xml") + ); + } +} diff --git a/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java b/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java index 6a35ccb5d..43cb1b65e 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java @@ -19,6 +19,7 @@ import io.cloudevents.CloudEvent; import io.cloudevents.core.format.EventFormat; +import io.cloudevents.rw.CloudEventRWException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -37,6 +38,7 @@ import static io.cloudevents.core.test.Data.*; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; public class XMLFormatTest { diff --git a/formats/xml/src/test/resources/bad/bad_data_content.xml b/formats/xml/src/test/resources/bad/bad_data_content.xml new file mode 100644 index 000000000..f2efaafa5 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_data_content.xml @@ -0,0 +1,16 @@ + + + + + + 1 + http://localhost/source + mock.test + + TEST + TEST + + diff --git a/formats/xml/src/test/resources/bad/bad_data_ns.xml b/formats/xml/src/test/resources/bad/bad_data_ns.xml new file mode 100644 index 000000000..4244ca2d7 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_data_ns.xml @@ -0,0 +1,13 @@ + + + + + + 1 + http://localhost/source + mock.test + Yes, Really! + diff --git a/formats/xml/src/test/resources/bad/bad_malformed.xml b/formats/xml/src/test/resources/bad/bad_malformed.xml new file mode 100644 index 000000000..1b4ab7582 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_malformed.xml @@ -0,0 +1,13 @@ + + + + + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/bad/bad_missing_data_ns.xml b/formats/xml/src/test/resources/bad/bad_missing_data_ns.xml new file mode 100644 index 000000000..4e3b8256d --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_missing_data_ns.xml @@ -0,0 +1,13 @@ + + + + + + 1 + http://localhost/source + mock.test + Yes, Really! + diff --git a/formats/xml/src/test/resources/bad/bad_no_ns.xml b/formats/xml/src/test/resources/bad/bad_no_ns.xml new file mode 100644 index 000000000..82d920298 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_no_ns.xml @@ -0,0 +1,10 @@ + + + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/bad/bad_ns.xml b/formats/xml/src/test/resources/bad/bad_ns.xml new file mode 100644 index 000000000..129a99302 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_ns.xml @@ -0,0 +1,12 @@ + + + + + + 1 + http://localhost/source + mock.test + From b78186dd7297fa1f540b3f5724b99359c7c52e3c Mon Sep 17 00:00:00 2001 From: "Day, Jeremy(jday)" Date: Tue, 22 Feb 2022 14:30:25 -0800 Subject: [PATCH 04/13] More Tests Signed-off-by: Day, Jeremy(jday) Signed-off-by: Jem Day --- .../io/cloudevents/xml/XMLDeserializer.java | 121 +++++++++++------- .../java/io/cloudevents/xml/XMLUtils.java | 29 +++++ .../io/cloudevents/xml/BadInputDataTest.java | 36 ++++-- .../java/io/cloudevents/xml/TestUtils.java | 12 +- .../java/io/cloudevents/xml/XMLUtilsTest.java | 39 ++++++ .../test/resources/bad/bad_attrib_name.xml | 9 ++ ...ata_content.xml => bad_data_content_1.xml} | 0 .../test/resources/bad/bad_data_content_2.xml | 13 ++ ...ng_data_ns.xml => bad_data_missing_ns.xml} | 0 .../src/test/resources/bad/bad_ext_name.xml | 10 ++ .../test/resources/bad/bad_spec_version.xml | 9 ++ 11 files changed, 217 insertions(+), 61 deletions(-) create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java create mode 100644 formats/xml/src/test/resources/bad/bad_attrib_name.xml rename formats/xml/src/test/resources/bad/{bad_data_content.xml => bad_data_content_1.xml} (100%) create mode 100644 formats/xml/src/test/resources/bad/bad_data_content_2.xml rename formats/xml/src/test/resources/bad/{bad_missing_data_ns.xml => bad_data_missing_ns.xml} (100%) create mode 100644 formats/xml/src/test/resources/bad/bad_ext_name.xml create mode 100644 formats/xml/src/test/resources/bad/bad_spec_version.xml diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java index 105cba9d8..fb4f8e6da 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java @@ -74,42 +74,41 @@ public , R> R read( Element e = (Element) node; // Sanity - if (isValidAttribute(e)) { - - // Grab all the useful markers. - final String attrName = e.getLocalName(); - final String attrType = extractAttributeType(e); - final String attrValue = e.getTextContent(); - - // Check if this is a Required or Optional attribute - if (specVersion.getAllAttributes().contains(attrName)) { - writer.withContextAttribute(attrName, attrValue); + ensureValidContextAttribute(e); + + // Grab all the useful markers. + final String attrName = e.getLocalName(); + final String attrType = extractAttributeType(e); + final String attrValue = e.getTextContent(); + + // Check if this is a Required or Optional attribute + if (specVersion.getAllAttributes().contains(attrName)) { + writer.withContextAttribute(attrName, attrValue); + } else { + if ("data".equals(attrName)) { + // Just remember the data node for now.. + dataElement = e; } else { - if ("data".equals(attrName)) { - // Just remember the data node for now.. - dataElement = e; - } else { - // Handle the extension attributes - switch (attrType) { - case "xs:string": - writer.withContextAttribute(attrName, attrValue); - break; - case "xs:int": - writer.withContextAttribute(attrName, Integer.valueOf(attrValue)); - break; - case "xs:dateTime": - writer.withContextAttribute(attrName, Time.parseTime(attrValue)); - break; - case "xs:boolean": - writer.withContextAttribute(attrName, Boolean.valueOf(attrValue)); - break; - case "xs:anyURI": - writer.withContextAttribute(attrName, URI.create(attrValue)); - break; - case "xs:base64Binary": - writer.withContextAttribute(attrName, Base64.getDecoder().decode(attrValue)); - break; - } + // Handle the extension attributes + switch (attrType) { + case "xs:string": + writer.withContextAttribute(attrName, attrValue); + break; + case "xs:int": + writer.withContextAttribute(attrName, Integer.valueOf(attrValue)); + break; + case "xs:dateTime": + writer.withContextAttribute(attrName, Time.parseTime(attrValue)); + break; + case "xs:boolean": + writer.withContextAttribute(attrName, Boolean.valueOf(attrValue)); + break; + case "xs:anyURI": + writer.withContextAttribute(attrName, URI.create(attrValue)); + break; + case "xs:base64Binary": + writer.withContextAttribute(attrName, Base64.getDecoder().decode(attrValue)); + break; } } } @@ -145,8 +144,6 @@ private Element findFirstElement(Element e) { private CloudEventData processData(Element data) throws CloudEventRWException { CloudEventData retVal = null; - - final String attrType = extractAttributeType(data); switch (attrType) { @@ -159,19 +156,15 @@ private CloudEventData processData(Element data) throws CloudEventRWException { break; case "xs:any": try { + // Ensure it's acceptable before we move forward. + ensureValidDataElement(data); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document newDoc = dbf.newDocumentBuilder().newDocument(); Element eventData = findFirstElement(data); - String eventDataNS = eventData.getNamespaceURI(); - - // Ensure the Data isn't in our namespace - if (CE_NAMESPACE.equals(eventDataNS)){ - throw CloudEventRWException.newInvalidDataType("data namespace: "+data.getNamespaceURI(), "Anything but " + CE_NAMESPACE); - } - Element newRoot = newDoc.createElementNS(eventDataNS, eventData.getLocalName()); + Element newRoot = newDoc.createElementNS(eventData.getNamespaceURI(), eventData.getLocalName()); newDoc.appendChild(newRoot); // Copy the children... @@ -194,6 +187,7 @@ private CloudEventData processData(Element data) throws CloudEventRWException { } break; default: + // I don't believe this is reachable break; } @@ -212,13 +206,44 @@ private void checkValidRootElement(Element e) throws CloudEventRWException { } } - private boolean isValidAttribute(Node n) { + private void ensureValidDataElement(Element dataEl) throws CloudEventRWException { - if (!CE_NAMESPACE.equals(n.getNamespaceURI())) { - return false; + // It must have a single child + final int childCount = XMLUtils.countOfChildElements(dataEl); + if (childCount != 1) { + throw CloudEventRWException.newInvalidDataType("data has " + childCount + " children", "1 expected"); } - return allLowerCase(n.getLocalName()); + // The child MUST NOT be in our namespace + String childNS = dataEl.getFirstChild().getNamespaceURI(); + if (CE_NAMESPACE.equals(childNS)) { + throw CloudEventRWException.newInvalidDataType(dataEl.getFirstChild().getLocalName(), "MUST not be in namespace: " + CE_NAMESPACE); + } + + } + + private void ensureValidContextAttribute(Element el) throws CloudEventRWException { + + final String localName = el.getLocalName(); + + // It must be in our namespace + if (!CE_NAMESPACE.equals(el.getNamespaceURI())) { + final String allowedTxt = el.getLocalName() + " Expected namespace: " + CE_NAMESPACE; + throw CloudEventRWException.newInvalidDataType(el.getNamespaceURI(), allowedTxt); + } + + // It must be all lowercase + if (!allLowerCase(localName)) { + throw CloudEventRWException.newInvalidDataType(localName, " context atttribute names MUST be lowercase"); + } + + // A bit kludgy, not relevent for 'data' - will refactor + if (!"data".equals(localName)) { + // It must not have any children + if (XMLUtils.countOfChildElements(el) != 0) { + throw CloudEventRWException.newInvalidDataType(el.getLocalName(), "Unexpected child element(s)"); + } + } } diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java index 286d75488..b7a2c8a11 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java @@ -19,6 +19,9 @@ import io.cloudevents.rw.CloudEventRWException; import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; @@ -71,4 +74,30 @@ static byte[] documentToBytes(Document doc) throws TransformerException { // And we're done return os.toByteArray(); } + + /** + * Get the number of child elements of an {@link Element} + * @param e The Element to introspect. + * @return The count of child elements + */ + static int countOfChildElements(Element e) { + + if (e == null) { + return 0; + } + + int retVal = 0; + + NodeList nodeLIst = e.getChildNodes(); + + for (int i=0;i< nodeLIst.getLength(); i++){ + final Node n = nodeLIst.item(i); + + if (n.getNodeType() == Node.ELEMENT_NODE){ + retVal++; + } + } + + return retVal; + } } diff --git a/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java b/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java index 6f5d55ade..c2865cdd6 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java @@ -7,7 +7,11 @@ import org.junit.jupiter.params.provider.MethodSource; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.stream.Stream; /** @@ -19,22 +23,30 @@ public class BadInputDataTest { private final EventFormat format = new XMLFormat(); @ParameterizedTest - @MethodSource("badExampleFiles") - public void verifyRejection(String fileName) throws IOException { + @MethodSource("badDataTestFiles") + public void verifyRejection(File testFile) throws IOException { - byte[] data = TestUtils.getData(fileName); + byte[] data = TestUtils.getData(testFile); assertThatExceptionOfType(CloudEventRWException.class).isThrownBy(() -> {format.deserialize(data);}); } - public static Stream badExampleFiles() { - return Stream.of( - Arguments.of("bad/bad_ns.xml"), - Arguments.of("bad/bad_no_ns.xml"), - Arguments.of("bad/bad_missing_data_ns.xml"), - Arguments.of("bad/bad_malformed.xml"), - Arguments.of("bad/bad_data_ns.xml"), - Arguments.of("bad/bad_data_content.xml") - ); + /** + * Obtain a list of all the "bad exmaple" resource files + * @return + * @throws IOException + */ + public static Stream badDataTestFiles() throws IOException { + + File fileDir = TestUtils.getFile("bad"); + + File[] fileList = fileDir.listFiles(); + List argList = new ArrayList<>(); + + for (File f : fileList) { + argList.add(Arguments.of(f)); + } + + return argList.stream(); } } diff --git a/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java b/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java index a2bb7a6b5..5e37c6eb3 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java @@ -11,6 +11,12 @@ public class TestUtils { + /** + * Get a File forn item in the resource path. + * @param filename + * @return + * @throws IOException + */ static File getFile(String filename) throws IOException { URL file = Thread.currentThread().getContextClassLoader().getResource(filename); assertThat(file).isNotNull(); @@ -24,8 +30,12 @@ static Reader getReader(String filename) throws IOException { return new FileReader(dataFile); } + static byte[] getData(File dataFile) throws IOException { + return Files.readAllBytes(dataFile.toPath()); + } + static byte[] getData(String filename) throws IOException { File f = getFile(filename); - return Files.readAllBytes(f.toPath()); + return getData(f); } } diff --git a/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java b/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java new file mode 100644 index 000000000..12eaead81 --- /dev/null +++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java @@ -0,0 +1,39 @@ +package io.cloudevents.xml; + +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class XMLUtilsTest { + + @Test + public void voidTestChildCount() throws ParserConfigurationException { + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + Document doc = dbf.newDocumentBuilder().newDocument(); + + Element root = doc.createElement("root"); + doc.appendChild(root); + + // NO Children on root thus far + assertThat(XMLUtils.countOfChildElements(root)).isEqualTo(0); + + // Add a child + Element c1 = doc.createElement("ChildOne"); + root.appendChild(c1); + + assertThat(XMLUtils.countOfChildElements(root)).isEqualTo(1); + + // Add a another child + Element c2 = doc.createElement("ChildTwo"); + root.appendChild(c2); + + assertThat(XMLUtils.countOfChildElements(root)).isEqualTo(2); + + } +} diff --git a/formats/xml/src/test/resources/bad/bad_attrib_name.xml b/formats/xml/src/test/resources/bad/bad_attrib_name.xml new file mode 100644 index 000000000..c2d238b6e --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_attrib_name.xml @@ -0,0 +1,9 @@ + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/bad/bad_data_content.xml b/formats/xml/src/test/resources/bad/bad_data_content_1.xml similarity index 100% rename from formats/xml/src/test/resources/bad/bad_data_content.xml rename to formats/xml/src/test/resources/bad/bad_data_content_1.xml diff --git a/formats/xml/src/test/resources/bad/bad_data_content_2.xml b/formats/xml/src/test/resources/bad/bad_data_content_2.xml new file mode 100644 index 000000000..aac81ae5f --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_data_content_2.xml @@ -0,0 +1,13 @@ + + + + + + 1 + http://localhost/source + mock.test + This is illegal + diff --git a/formats/xml/src/test/resources/bad/bad_missing_data_ns.xml b/formats/xml/src/test/resources/bad/bad_data_missing_ns.xml similarity index 100% rename from formats/xml/src/test/resources/bad/bad_missing_data_ns.xml rename to formats/xml/src/test/resources/bad/bad_data_missing_ns.xml diff --git a/formats/xml/src/test/resources/bad/bad_ext_name.xml b/formats/xml/src/test/resources/bad/bad_ext_name.xml new file mode 100644 index 000000000..32fedaa25 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_ext_name.xml @@ -0,0 +1,10 @@ + + + 1 + http://localhost/source + mock.test + hello + diff --git a/formats/xml/src/test/resources/bad/bad_spec_version.xml b/formats/xml/src/test/resources/bad/bad_spec_version.xml new file mode 100644 index 000000000..8c3cd418c --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_spec_version.xml @@ -0,0 +1,9 @@ + + + 1 + http://localhost/source + mock.test + From ba689aed49ec02014b8ad382c667707cad24247a Mon Sep 17 00:00:00 2001 From: "Day, Jeremy(jday)" Date: Fri, 25 Feb 2022 16:26:57 -0800 Subject: [PATCH 05/13] Adopted (unratified) chnages to types and namespaces Signed-off-by: Day, Jeremy(jday) Signed-off-by: Jem Day --- .../io/cloudevents/xml/XMLCloudEventData.java | 3 +- .../java/io/cloudevents/xml/XMLConstants.java | 71 +++++++++++++++++++ .../io/cloudevents/xml/XMLDataWrapper.java | 4 ++ .../io/cloudevents/xml/XMLDeserializer.java | 24 ++++--- .../io/cloudevents/xml/XMLSerializer.java | 25 ++++--- .../java/io/cloudevents/xml/XMLUtils.java | 5 +- .../io/cloudevents/xml/BadInputDataTest.java | 27 +++++-- .../java/io/cloudevents/xml/TestUtils.java | 17 +++++ .../io/cloudevents/xml/XMLConstantsTest.java | 50 +++++++++++++ .../io/cloudevents/xml/XMLFormatTest.java | 14 ++-- .../java/io/cloudevents/xml/XMLUtilsTest.java | 16 +++++ .../resources/bad/bad_data_missing_ns.xml | 13 ---- ...ad_data_ns.xml => bad_missing_specver.xml} | 6 +- .../xml/src/test/resources/v1/binary_attr.xml | 2 +- .../test/resources/v1/json_data_with_ext.xml | 6 +- .../src/test/resources/v1/with_extensions.xml | 6 +- 16 files changed, 225 insertions(+), 64 deletions(-) create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLConstants.java create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/XMLConstantsTest.java delete mode 100644 formats/xml/src/test/resources/bad/bad_data_missing_ns.xml rename formats/xml/src/test/resources/bad/{bad_data_ns.xml => bad_missing_specver.xml} (59%) diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java index b24727169..ba6dc71d4 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java @@ -21,7 +21,7 @@ import org.w3c.dom.Document; /** - * A variant of {@link CloudEventData} support direct access + * A variant of {@link CloudEventData} that supports direct access * to data as an XML Document. */ public interface XMLCloudEventData extends CloudEventData { @@ -36,6 +36,7 @@ public interface XMLCloudEventData extends CloudEventData { /** * Wraps an XML {@link Document} + * * @param xmlDoc {@link Document} * @return {@link XMLCloudEventData} */ diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLConstants.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLConstants.java new file mode 100644 index 000000000..df8f66fb3 --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLConstants.java @@ -0,0 +1,71 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.cloudevents.xml; + +import java.util.ArrayList; +import java.util.Collection; + +final class XMLConstants { + + // Our Namepace + static final String CE_NAMESPACE = "http://cloudevents.io/xmlformat/V1"; + + // CE Attribute Type Designators + static final String CE_ATTR_STRING = "ce:string"; + static final String CE_ATTR_BOOLEAN = "ce:boolean"; + static final String CE_ATTR_INTEGER = "ce:integer"; + static final String CE_ATTR_URI = "ce:uri"; + static final String CE_ATTR_URI_REF = "ce:uriRef"; + static final String CE_ATTR_BINARY = "ce:binary"; + static final String CE_ATTR_TIMESTAMP = "ce:timestamp"; + + // CE Data Type Designators + static final String CE_DATA_ATTR_BINARY = "xs:base64Binary"; + static final String CE_DATA_ATTR_TEXT = "xs:string"; + static final String CE_DATA_ATTR_XML = "xs:any"; + + // General XML Constants + static final String XSI_TYPE = "xsi:type"; + + // Bundle these into a collection (probably could be made more efficient) + static final Collection CE_ATTR_LIST = new ArrayList() {{ + add(CE_ATTR_STRING); + add(CE_ATTR_BOOLEAN); + add(CE_ATTR_INTEGER); + add(CE_ATTR_TIMESTAMP); + add(CE_ATTR_URI); + add(CE_ATTR_URI_REF); + add(CE_ATTR_BINARY); + }}; + + static final Collection CE_DATA_ATTRS = new ArrayList() {{ + add(CE_DATA_ATTR_TEXT); + add(CE_DATA_ATTR_BINARY); + add(CE_DATA_ATTR_XML); + }}; + + private XMLConstants() { + } + + static boolean isCloudEventAttributeType(final String type) { + return CE_ATTR_LIST.contains(type); + } + + static boolean isValidDataType(final String type) { + return CE_DATA_ATTRS.contains(type); + } +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java index 7dd19e132..7b09d4bc6 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java @@ -22,6 +22,10 @@ import javax.xml.transform.TransformerException; +/** + * Local Implementation of {@link XMLCloudEventData} that + * wraps an {@link Document} + */ class XMLDataWrapper implements XMLCloudEventData { private final Document xmlDoc; diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java index fb4f8e6da..e3061761c 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java @@ -55,7 +55,7 @@ public , R> R read( String specVer = root.getAttribute("specversion"); if (specVer == null) { - // Error + throw CloudEventRWException.newInvalidSpecVersion("null - Missing XML attribute"); } final SpecVersion specVersion = SpecVersion.parse(specVer); @@ -91,22 +91,25 @@ public , R> R read( } else { // Handle the extension attributes switch (attrType) { - case "xs:string": + case XMLConstants.CE_ATTR_STRING: writer.withContextAttribute(attrName, attrValue); break; - case "xs:int": + case XMLConstants.CE_ATTR_INTEGER: writer.withContextAttribute(attrName, Integer.valueOf(attrValue)); break; - case "xs:dateTime": + case XMLConstants.CE_ATTR_TIMESTAMP: writer.withContextAttribute(attrName, Time.parseTime(attrValue)); break; - case "xs:boolean": + case XMLConstants.CE_ATTR_BOOLEAN: writer.withContextAttribute(attrName, Boolean.valueOf(attrValue)); break; - case "xs:anyURI": + case XMLConstants.CE_ATTR_URI: writer.withContextAttribute(attrName, URI.create(attrValue)); break; - case "xs:base64Binary": + case XMLConstants.CE_ATTR_URI_REF: + writer.withContextAttribute(attrName, URI.create(attrValue)); + break; + case XMLConstants.CE_ATTR_BINARY: writer.withContextAttribute(attrName, Base64.getDecoder().decode(attrValue)); break; } @@ -147,14 +150,14 @@ private CloudEventData processData(Element data) throws CloudEventRWException { final String attrType = extractAttributeType(data); switch (attrType) { - case "xs:string": + case XMLConstants.CE_DATA_ATTR_TEXT: retVal = new TextCloudEventData(data.getTextContent()); break; - case "xs:base64Binary": + case XMLConstants.CE_DATA_ATTR_BINARY: String eData = data.getTextContent(); retVal = BytesCloudEventData.wrap(Base64.getDecoder().decode(eData)); break; - case "xs:any": + case XMLConstants.CE_DATA_ATTR_XML: try { // Ensure it's acceptable before we move forward. ensureValidDataElement(data); @@ -198,7 +201,6 @@ private void checkValidRootElement(Element e) throws CloudEventRWException { if (!"event".equals(e.getLocalName())) { throw CloudEventRWException.newInvalidDataType(e.getLocalName(), "event"); - } if (!CE_NAMESPACE.equalsIgnoreCase(e.getNamespaceURI())) { diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java index e0780dfdd..767fea27b 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java @@ -67,8 +67,6 @@ static Document toDocument(CloudEvent ce) { private static class XMLCloudEventWriter implements CloudEventWriter { - static final String XSI_TYPE = "xsi:type"; - static final String CLOUDEVENT_NAMESPACE = "http://cloudevents.io/xmlformat/V1"; static final String XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"; static final String XS_NAMESPACE = "http://www.w3.org/2001/XMLSchema"; static final String CE_ROOT_ELEMENT = "event"; @@ -93,7 +91,7 @@ private static class XMLCloudEventWriter implements CloudEventWriter { } // Start the Document - root = xmlDocument.createElementNS(CLOUDEVENT_NAMESPACE, CE_ROOT_ELEMENT); + root = xmlDocument.createElementNS(XMLConstants.CE_NAMESPACE, CE_ROOT_ELEMENT); root.setAttribute("xmlns:xs", XS_NAMESPACE); root.setAttribute("xmlns:xsi", XSI_NAMESPACE); root.setAttribute("specversion", specVersion.toString()); @@ -115,8 +113,9 @@ private void addElement(String name, String xsiType, String value) { // don't need to communicate the type information. if (!specVersion.getAllAttributes().contains(name)) { - e.setAttribute(XSI_TYPE, xsiType); + e.setAttribute(XMLConstants.XSI_TYPE, xsiType); } + e.setTextContent(value); root.appendChild(e); @@ -162,7 +161,7 @@ private void writeXmlData(Document dataDoc) { // Create the wrapper Element e = xmlDocument.createElement("data"); - e.setAttribute(XSI_TYPE, "xs:any"); + e.setAttribute(XMLConstants.XSI_TYPE, XMLConstants.CE_DATA_ATTR_XML); root.appendChild(e); // Get the Root Element @@ -184,21 +183,21 @@ private void writeXmlData(byte[] data) { @Override public CloudEventContextWriter withContextAttribute(String name, String value) throws CloudEventRWException { - addElement(name, "xs:string", value); + addElement(name, XMLConstants.CE_ATTR_STRING, value); return this; } @Override public CloudEventContextWriter withContextAttribute(String name, URI value) throws CloudEventRWException { - addElement(name, "xs:anyURI", value.toString()); + addElement(name, XMLConstants.CE_ATTR_URI, value.toString()); return this; } @Override public CloudEventContextWriter withContextAttribute(String name, OffsetDateTime value) throws CloudEventRWException { - addElement(name, "xs:dateTime", Time.writeTime(value)); + addElement(name, XMLConstants.CE_ATTR_TIMESTAMP, Time.writeTime(value)); return this; } @@ -215,21 +214,21 @@ public CloudEventContextWriter withContextAttribute(String name, Number value) t @Override public CloudEventContextWriter withContextAttribute(String name, Integer value) throws CloudEventRWException { - addElement(name, "xs:int", value.toString()); + addElement(name, XMLConstants.CE_ATTR_INTEGER, value.toString()); return this; } @Override public CloudEventContextWriter withContextAttribute(String name, Boolean value) throws CloudEventRWException { - addElement(name, "xs:boolean", value.toString()); + addElement(name, XMLConstants.CE_ATTR_BOOLEAN, value.toString()); return this; } @Override public CloudEventContextWriter withContextAttribute(String name, byte[] value) throws CloudEventRWException { - addElement(name, "xs:base64Binary", Base64.getEncoder().encodeToString(value)); + addElement(name, XMLConstants.CE_ATTR_BINARY, Base64.getEncoder().encodeToString(value)); return this; } @@ -242,11 +241,11 @@ public Document end(CloudEventData data) throws CloudEventRWException { writeXmlData(data.toBytes()); } else if (isTextContent(dataContentType)) { // Handle Textual Content - addElement("data", "xs:string", new String(data.toBytes())); + addElement("data", XMLConstants.CE_DATA_ATTR_TEXT, new String(data.toBytes())); } else { // Handle Binary Content final String encodedValue = Base64.getEncoder().encodeToString(data.toBytes()); - addElement ("data", "xs:base64Binary",encodedValue); + addElement("data", XMLConstants.CE_DATA_ATTR_BINARY, encodedValue); } return end(); } diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java index b7a2c8a11..6783075fe 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java @@ -77,6 +77,7 @@ static byte[] documentToBytes(Document doc) throws TransformerException { /** * Get the number of child elements of an {@link Element} + * * @param e The Element to introspect. * @return The count of child elements */ @@ -90,10 +91,10 @@ static int countOfChildElements(Element e) { NodeList nodeLIst = e.getChildNodes(); - for (int i=0;i< nodeLIst.getLength(); i++){ + for (int i = 0; i < nodeLIst.getLength(); i++) { final Node n = nodeLIst.item(i); - if (n.getNodeType() == Node.ELEMENT_NODE){ + if (n.getNodeType() == Node.ELEMENT_NODE) { retVal++; } } diff --git a/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java b/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java index c2865cdd6..96c682eaa 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ package io.cloudevents.xml; import io.cloudevents.core.format.EventFormat; @@ -5,15 +21,15 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.stream.Stream; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * A seperate Test set to hold the test cases related * to dealing with invalid representations @@ -28,15 +44,18 @@ public void verifyRejection(File testFile) throws IOException { byte[] data = TestUtils.getData(testFile); - assertThatExceptionOfType(CloudEventRWException.class).isThrownBy(() -> {format.deserialize(data);}); + assertThatExceptionOfType(CloudEventRWException.class).isThrownBy(() -> { + format.deserialize(data); + }); } /** * Obtain a list of all the "bad exmaple" resource files + * * @return * @throws IOException */ - public static Stream badDataTestFiles() throws IOException { + public static Stream badDataTestFiles() throws IOException { File fileDir = TestUtils.getFile("bad"); diff --git a/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java b/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java index 5e37c6eb3..5c40da380 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ package io.cloudevents.xml; import java.io.File; @@ -13,6 +29,7 @@ public class TestUtils { /** * Get a File forn item in the resource path. + * * @param filename * @return * @throws IOException diff --git a/formats/xml/src/test/java/io/cloudevents/xml/XMLConstantsTest.java b/formats/xml/src/test/java/io/cloudevents/xml/XMLConstantsTest.java new file mode 100644 index 000000000..dad5f6fe8 --- /dev/null +++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLConstantsTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.cloudevents.xml; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class XMLConstantsTest { + + @Test + public void verifyNS() { + assertThat(XMLConstants.CE_NAMESPACE).isEqualTo("http://cloudevents.io/xmlformat/V1"); + } + + public void verifyContextAttributeTypes() { + assertThat(XMLConstants.isCloudEventAttributeType("ce:boolean")).isTrue(); + assertThat(XMLConstants.isCloudEventAttributeType("ce:integer")).isTrue(); + assertThat(XMLConstants.isCloudEventAttributeType("ce:string")).isTrue(); + assertThat(XMLConstants.isCloudEventAttributeType("ce:binary")).isTrue(); + assertThat(XMLConstants.isCloudEventAttributeType("ce:uri")).isTrue(); + assertThat(XMLConstants.isCloudEventAttributeType("ce:uriRef")).isTrue(); + assertThat(XMLConstants.isCloudEventAttributeType("ce:timestamp")).isTrue(); + + assertThat(XMLConstants.CE_ATTR_LIST.size()).isEqualTo(7); + } + + public void verifyDataTypes() { + assertThat(XMLConstants.isValidDataType("xs:string")).isTrue(); + assertThat(XMLConstants.isValidDataType("xs:base64Binary")).isTrue(); + assertThat(XMLConstants.isValidDataType("xs:any")).isTrue(); + + assertThat(XMLConstants.CE_DATA_ATTRS.size()).isEqualTo(3); + + } +} diff --git a/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java b/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java index 43cb1b65e..e3da317e6 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java @@ -19,7 +19,6 @@ import io.cloudevents.CloudEvent; import io.cloudevents.core.format.EventFormat; -import io.cloudevents.rw.CloudEventRWException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -31,14 +30,14 @@ import javax.xml.transform.Source; import javax.xml.transform.stream.StreamSource; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.stream.Stream; import static io.cloudevents.core.test.Data.*; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; public class XMLFormatTest { @@ -51,8 +50,7 @@ public void testRegistration() { } @Test - public void verifyExtensions() throws IOException - { + public void verifyExtensions() throws IOException { byte[] raw = TestUtils.getData("v1/with_extensions.xml"); CloudEvent ce = format.deserialize(raw); @@ -105,7 +103,7 @@ public void serialize(io.cloudevents.CloudEvent input, String xmlFile) throws IO .checkForSimilar() .build(); - if (diff.hasDifferences()){ + if (diff.hasDifferences()) { // Dump what was actually generated. dumpXml(raw); @@ -187,10 +185,10 @@ public void roundTrip(String fileName) throws IOException { .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byName)) .build(); - if (diff.hasDifferences()){ + if (diff.hasDifferences()) { dumpXml(outputData); - if (diff.hasDifferences()){ + if (diff.hasDifferences()) { for (Difference d : diff.getDifferences()) { System.out.println(d); } diff --git a/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java b/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java index 12eaead81..914d17473 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ package io.cloudevents.xml; import org.junit.jupiter.api.Test; diff --git a/formats/xml/src/test/resources/bad/bad_data_missing_ns.xml b/formats/xml/src/test/resources/bad/bad_data_missing_ns.xml deleted file mode 100644 index 4e3b8256d..000000000 --- a/formats/xml/src/test/resources/bad/bad_data_missing_ns.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - 1 - http://localhost/source - mock.test - Yes, Really! - diff --git a/formats/xml/src/test/resources/bad/bad_data_ns.xml b/formats/xml/src/test/resources/bad/bad_missing_specver.xml similarity index 59% rename from formats/xml/src/test/resources/bad/bad_data_ns.xml rename to formats/xml/src/test/resources/bad/bad_missing_specver.xml index 4244ca2d7..641731b69 100644 --- a/formats/xml/src/test/resources/bad/bad_data_ns.xml +++ b/formats/xml/src/test/resources/bad/bad_missing_specver.xml @@ -1,13 +1,9 @@ - - - + > 1 http://localhost/source mock.test - Yes, Really! diff --git a/formats/xml/src/test/resources/v1/binary_attr.xml b/formats/xml/src/test/resources/v1/binary_attr.xml index 7651a227b..cd45e3655 100644 --- a/formats/xml/src/test/resources/v1/binary_attr.xml +++ b/formats/xml/src/test/resources/v1/binary_attr.xml @@ -6,5 +6,5 @@ 1 http://localhost/source mock.test - 4P8ARKo= + 4P8ARKo= diff --git a/formats/xml/src/test/resources/v1/json_data_with_ext.xml b/formats/xml/src/test/resources/v1/json_data_with_ext.xml index bbd74bfd7..5f0ebe70d 100644 --- a/formats/xml/src/test/resources/v1/json_data_with_ext.xml +++ b/formats/xml/src/test/resources/v1/json_data_with_ext.xml @@ -11,7 +11,7 @@ 1 http://localhost/source mock.test - aaa - true - 10 + aaa + true + 10 diff --git a/formats/xml/src/test/resources/v1/with_extensions.xml b/formats/xml/src/test/resources/v1/with_extensions.xml index 33fd9c6d6..c991ad6c4 100644 --- a/formats/xml/src/test/resources/v1/with_extensions.xml +++ b/formats/xml/src/test/resources/v1/with_extensions.xml @@ -6,7 +6,7 @@ 1 http://localhost/source mock.test - 42 - Greetings - false + 42 + Greetings + false From 5e833d8cc13fe92f2f0edeb43951e886144dcefe Mon Sep 17 00:00:00 2001 From: "Day, Jeremy(jday)" Date: Mon, 28 Feb 2022 14:44:45 -0800 Subject: [PATCH 06/13] More cleanups Signed-off-by: Day, Jeremy(jday) Signed-off-by: Jem Day --- docs/index.md | 4 + docs/xml.md | 75 ++++++++++++++++++ .../java/io/cloudevents/xml/XMLConstants.java | 8 +- .../io/cloudevents/xml/XMLDeserializer.java | 76 ++++++++++++++----- .../java/io/cloudevents/xml/XMLFormat.java | 3 +- .../io/cloudevents/xml/XMLSerializer.java | 10 +-- .../test/resources/bad/bad_root_element.xml | 12 +++ 7 files changed, 159 insertions(+), 29 deletions(-) create mode 100644 docs/xml.md create mode 100644 formats/xml/src/test/resources/bad/bad_root_element.xml diff --git a/docs/index.md b/docs/index.md index 07664c2ee..15eede441 100644 --- a/docs/index.md +++ b/docs/index.md @@ -42,6 +42,8 @@ Using the Java SDK you can: | - [Jackson](json-jackson.md) | :heavy_check_mark: | :heavy_check_mark: | | Protobuf Event Format | :heavy_check_mark: | :heavy_check_mark: | | - [Proto](protobuf.md) | :heavy_check_mark: | :heavy_check_mark: | +| XML Event Format | :heavy_check_mark: | :heavy_check_mark: | +| - [XML](xml.md) | :heavy_check_mark: | :heavy_check_mark: | | [Kafka Protocol Binding](kafka.md) | :heavy_check_mark: | :heavy_check_mark: | | MQTT Protocol Binding | :x: | :x: | | NATS Protocol Binding | :x: | :x: | @@ -96,6 +98,7 @@ a different feature from the different sub specs of [Jackson](https://github.com/FasterXML/jackson) - [`cloudevents-protobuf`] Implementation of [Protobuf Event format] using code generated from the standard [protoc](https://github.com/protocolbuffers/protobuf) compiler. +- [`cloudevents-xml`] Implementation of the XML Event Format. - [`cloudevents-http-vertx`] Implementation of [HTTP Protocol Binding] with [Vert.x Core](https://vertx.io/) - [`cloudevents-http-restful-ws`] Implementation of [HTTP Protocol Binding] @@ -123,6 +126,7 @@ You can look at the latest published artifacts on [`cloudevents-core`]: https://github.com/cloudevents/sdk-java/tree/master/core [`cloudevents-json-jackson`]: https://github.com/cloudevents/sdk-java/tree/master/formats/json-jackson [`cloudevents-protobuf`]: https://github.com/cloudevents/sdk-java/tree/master/formats/protobuf +[`cloudevents-xml`]: https://github.com/cloudevents/sdk-java/tree/master/formats/xml [`cloudevents-http-vertx`]: https://github.com/cloudevents/sdk-java/tree/master/http/vertx [`cloudevents-http-basic`]: https://github.com/cloudevents/sdk-java/tree/master/http/basic [`cloudevents-http-restful-ws`]: https://github.com/cloudevents/sdk-java/tree/master/http/restful-ws diff --git a/docs/xml.md b/docs/xml.md new file mode 100644 index 000000000..26844b069 --- /dev/null +++ b/docs/xml.md @@ -0,0 +1,75 @@ +--- +title: CloudEvents Protocol Buffers +nav_order: 4 +--- + +# CloudEvents XML Format + +[![Javadocs](http://www.javadoc.io/badge/io.cloudevents/cloudevents-xml.svg?color=green)](http://www.javadoc.io/doc/io.cloudevents/cloudevents-xml) + +This module provides and `EventFormat` implementation that adhere's +to the CloudEvent XML Format specification. This format also supports +specialized handling for XML CloudEvent `data`. + +For Maven based projects, use the following dependency: + +```xml + + io.cloudevents + cloudevents-xml + 2.4.0 + +``` + +## Using the XML Event Format + +You don't need to perform any operation to configure the module, more than +adding the dependency to your project: + +```java +import io.cloudevents.CloudEvent; +import io.cloudevents.core.format.EventFormatProvider; +import io.cloudevents.core.builder.CloudEventBuilder; +import io.cloudevents.xml.XMLFormat; + +CloudEvent event = CloudEventBuilder.v1() + .withId("hello") + .withType("example.xml") + .withSource(URI.create("http://localhost")) + .build(); + +byte[]serialized = EventFormatProvider + .getInstance() + .resolveFormat(XMLFormat.CONTENT_TYPE) + .serialize(event); +``` + +The `EventFormatProvider` will resolve automatically the `XMLFormat` using the +`ServiceLoader` APIs. + +XML Document data handling is supported via the `XMLCloudEventData` +facility. This convenience wrapper can be used with `any` other supported +format. + +```java +import org.w3c.dom.Document; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; +import io.cloudevents.xml.XMLCloudEventData; + +// Create the business event data. +Document xmlDoc = .... ; + +// Wrap it into CloudEventData +CloudEventData myData = XMLCloudEventData.wrap(xmlDoc); + +// Construct the event +CloudEvent event = CloudEventBuilder.v1() + .withId("hello") + .withType("example.xml") + .withSource(URI.create("http://localhost")) + .withData(myData) + .build(); + + + diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLConstants.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLConstants.java index df8f66fb3..40a63341a 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLConstants.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLConstants.java @@ -21,8 +21,10 @@ final class XMLConstants { - // Our Namepace + // Namespaces static final String CE_NAMESPACE = "http://cloudevents.io/xmlformat/V1"; + static final String XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"; + static final String XS_NAMESPACE = "http://www.w3.org/2001/XMLSchema"; // CE Attribute Type Designators static final String CE_ATTR_STRING = "ce:string"; @@ -41,6 +43,10 @@ final class XMLConstants { // General XML Constants static final String XSI_TYPE = "xsi:type"; + // Special Element names + static final String XML_DATA_ELEMENT = "data"; + static final String XML_ROOT_ELEMENT = "event"; + // Bundle these into a collection (probably could be made more efficient) static final Collection CE_ATTR_LIST = new ArrayList() {{ add(CE_ATTR_STRING); diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java index e3061761c..f11dc6d3e 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java @@ -28,11 +28,8 @@ import java.net.URI; import java.util.Base64; -public class XMLDeserializer implements CloudEventReader { +class XMLDeserializer implements CloudEventReader { - static final String CE_NAMESPACE = "http://cloudevents.io/xmlformat/V1"; - static final String XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"; - static final String XS_NAMESPACE = "http://www.w3.org/2001/XMLSchema"; private final Document xmlDocument; XMLDeserializer(Document doc) { @@ -83,9 +80,10 @@ public , R> R read( // Check if this is a Required or Optional attribute if (specVersion.getAllAttributes().contains(attrName)) { + // Yep .. Just write it out. writer.withContextAttribute(attrName, attrValue); } else { - if ("data".equals(attrName)) { + if (XMLConstants.XML_DATA_ELEMENT.equals(attrName)) { // Just remember the data node for now.. dataElement = e; } else { @@ -130,6 +128,12 @@ public , R> R read( // Private Methods -------------------------------------------------------- + /** + * Geyt the first child {@link Element} of an {@link Element} + * + * @param e + * @return The first child, or NULL if there isn't one. + */ private Element findFirstElement(Element e) { NodeList nodeList = e.getChildNodes(); @@ -144,11 +148,24 @@ private Element findFirstElement(Element e) { return null; } + /** + * Process the business event data of the XNML Formatted + * event. + *

+ * This may result in an XML specific data wrapper being returned + * depending on payload. + * + * @param data + * @return {@link CloudEventData} The data wrapper. + * @throws CloudEventRWException + */ private CloudEventData processData(Element data) throws CloudEventRWException { CloudEventData retVal = null; final String attrType = extractAttributeType(data); + // Process based on the defined `xsi:type` of the data element. + switch (attrType) { case XMLConstants.CE_DATA_ATTR_TEXT: retVal = new TextCloudEventData(data.getTextContent()); @@ -197,17 +214,32 @@ private CloudEventData processData(Element data) throws CloudEventRWException { return retVal; } + /** + * Ensure that the root elemement of the received XML document is valid + * in our context. + * + * @param e The root {@link Element} + * @throws CloudEventRWException + */ private void checkValidRootElement(Element e) throws CloudEventRWException { - if (!"event".equals(e.getLocalName())) { - throw CloudEventRWException.newInvalidDataType(e.getLocalName(), "event"); + // It must be the name we expect. + if (!XMLConstants.XML_ROOT_ELEMENT.equals(e.getLocalName())) { + throw CloudEventRWException.newInvalidDataType(e.getLocalName(), XMLConstants.XML_ROOT_ELEMENT); } - if (!CE_NAMESPACE.equalsIgnoreCase(e.getNamespaceURI())) { - throw CloudEventRWException.newInvalidDataType(e.getNamespaceURI(), "Namespace: " + CE_NAMESPACE); + // It must be in the CE namespace. + if (!XMLConstants.CE_NAMESPACE.equalsIgnoreCase(e.getNamespaceURI())) { + throw CloudEventRWException.newInvalidDataType(e.getNamespaceURI(), "Namespace: " + XMLConstants.CE_NAMESPACE); } } + /** + * Ensure the XML `data` element is well formed. + * + * @param dataEl + * @throws CloudEventRWException + */ private void ensureValidDataElement(Element dataEl) throws CloudEventRWException { // It must have a single child @@ -216,21 +248,27 @@ private void ensureValidDataElement(Element dataEl) throws CloudEventRWException throw CloudEventRWException.newInvalidDataType("data has " + childCount + " children", "1 expected"); } - // The child MUST NOT be in our namespace - String childNS = dataEl.getFirstChild().getNamespaceURI(); - if (CE_NAMESPACE.equals(childNS)) { - throw CloudEventRWException.newInvalidDataType(dataEl.getFirstChild().getLocalName(), "MUST not be in namespace: " + CE_NAMESPACE); - } + // And there must be a valid type descriminator + final String xsiType = dataEl.getAttribute(XMLConstants.XSI_TYPE); + if (xsiType == null) { + throw CloudEventRWException.newInvalidDataType("NULL", "xsi:type oneOf [xs:base64Binary, xs:string, xs:any]"); + } } + /** + * Ensure a CludEvent context attribute representation is as expected. + * + * @param el + * @throws CloudEventRWException + */ private void ensureValidContextAttribute(Element el) throws CloudEventRWException { final String localName = el.getLocalName(); // It must be in our namespace - if (!CE_NAMESPACE.equals(el.getNamespaceURI())) { - final String allowedTxt = el.getLocalName() + " Expected namespace: " + CE_NAMESPACE; + if (!XMLConstants.CE_NAMESPACE.equals(el.getNamespaceURI())) { + final String allowedTxt = el.getLocalName() + " Expected namespace: " + XMLConstants.CE_NAMESPACE; throw CloudEventRWException.newInvalidDataType(el.getNamespaceURI(), allowedTxt); } @@ -239,8 +277,8 @@ private void ensureValidContextAttribute(Element el) throws CloudEventRWExceptio throw CloudEventRWException.newInvalidDataType(localName, " context atttribute names MUST be lowercase"); } - // A bit kludgy, not relevent for 'data' - will refactor - if (!"data".equals(localName)) { + // A bit kludgy, not relevent for 'data' - should refactor + if (!XMLConstants.XML_DATA_ELEMENT.equals(localName)) { // It must not have any children if (XMLUtils.countOfChildElements(el) != 0) { throw CloudEventRWException.newInvalidDataType(el.getLocalName(), "Unexpected child element(s)"); @@ -251,7 +289,7 @@ private void ensureValidContextAttribute(Element el) throws CloudEventRWExceptio private String extractAttributeType(Element e) { - Attr a = e.getAttributeNodeNS(XSI_NAMESPACE, "type"); + final Attr a = e.getAttributeNodeNS(XMLConstants.XSI_NAMESPACE, "type"); if (a != null) { return a.getValue(); diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.java index 2ab57fee8..996f07cf3 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.java @@ -36,11 +36,10 @@ public class XMLFormat implements EventFormat { /** - * The content type for transports sending cloudevents in the protocol buffer format. + * The content type for transports sending cloudevents in XML format. */ public static final String XML_CONTENT_TYPE = "application/cloudevents+xml"; - @Override public byte[] serialize(CloudEvent event) throws EventSerializationException { diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java index 767fea27b..d95f44fa4 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java @@ -67,10 +67,6 @@ static Document toDocument(CloudEvent ce) { private static class XMLCloudEventWriter implements CloudEventWriter { - static final String XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"; - static final String XS_NAMESPACE = "http://www.w3.org/2001/XMLSchema"; - static final String CE_ROOT_ELEMENT = "event"; - private final Document xmlDocument; private final Element root; private final SpecVersion specVersion; @@ -91,9 +87,9 @@ private static class XMLCloudEventWriter implements CloudEventWriter { } // Start the Document - root = xmlDocument.createElementNS(XMLConstants.CE_NAMESPACE, CE_ROOT_ELEMENT); - root.setAttribute("xmlns:xs", XS_NAMESPACE); - root.setAttribute("xmlns:xsi", XSI_NAMESPACE); + root = xmlDocument.createElementNS(XMLConstants.CE_NAMESPACE, XMLConstants.XML_ROOT_ELEMENT); + root.setAttribute("xmlns:xs", XMLConstants.XS_NAMESPACE); + root.setAttribute("xmlns:xsi", XMLConstants.XSI_NAMESPACE); root.setAttribute("specversion", specVersion.toString()); xmlDocument.appendChild(root); } diff --git a/formats/xml/src/test/resources/bad/bad_root_element.xml b/formats/xml/src/test/resources/bad/bad_root_element.xml new file mode 100644 index 000000000..f58bc87f3 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_root_element.xml @@ -0,0 +1,12 @@ + + + + + + 1 + http://localhost/source + mock.test + From 4acbd5d38853d513ead190ca44b646ddf424ac7c Mon Sep 17 00:00:00 2001 From: "Day, Jeremy(jday)" Date: Fri, 11 Mar 2022 10:15:17 -0800 Subject: [PATCH 07/13] reformatting and comment cleanups Signed-off-by: Day, Jeremy(jday) Signed-off-by: Jem Day --- README.md | 1 + .../java/io/cloudevents/xml/XMLCloudEventData.java | 2 +- .../java/io/cloudevents/xml/XMLDataWrapper.java | 2 +- .../java/io/cloudevents/xml/XMLDeserializer.java | 6 +++--- .../src/main/java/io/cloudevents/xml/XMLUtils.java | 14 ++++++++++++++ .../test/java/io/cloudevents/xml/TestUtils.java | 2 +- .../java/io/cloudevents/xml/XMLFormatTest.java | 2 +- 7 files changed, 22 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e5e86ba13..d02ded796 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Javadocs are available on [javadoc.io](https://www.javadoc.io): - [cloudevents-core](https://www.javadoc.io/doc/io.cloudevents/cloudevents-core) - [cloudevents-json-jackson](https://www.javadoc.io/doc/io.cloudevents/cloudevents-json-jackson) - [cloudevents-protobuf](https://www.javadoc.io/doc/io.cloudevents/cloudevents-protobuf) +- [cloudevents-xml](https://www.javadoc.io/doc/io.cloudevents/cloudevents-xml) - [cloudevents-http-basic](https://www.javadoc.io/doc/io.cloudevents/cloudevents-http-basic) - [cloudevents-http-restful-ws](https://www.javadoc.io/doc/io.cloudevents/cloudevents-http-restful-ws) - [cloudevents-http-vertx](https://www.javadoc.io/doc/io.cloudevents/cloudevents-http-vertx) diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java index ba6dc71d4..a33e055ef 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java @@ -22,7 +22,7 @@ /** * A variant of {@link CloudEventData} that supports direct access - * to data as an XML Document. + * to data as an XML {@link Document}. */ public interface XMLCloudEventData extends CloudEventData { diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java index 7b09d4bc6..1cd7af70b 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java @@ -24,7 +24,7 @@ /** * Local Implementation of {@link XMLCloudEventData} that - * wraps an {@link Document} + * wraps an XML {@link Document} */ class XMLDataWrapper implements XMLCloudEventData { diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java index f11dc6d3e..322da67f7 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java @@ -129,7 +129,7 @@ public , R> R read( // Private Methods -------------------------------------------------------- /** - * Geyt the first child {@link Element} of an {@link Element} + * Get the first child {@link Element} of an {@link Element} * * @param e * @return The first child, or NULL if there isn't one. @@ -149,7 +149,7 @@ private Element findFirstElement(Element e) { } /** - * Process the business event data of the XNML Formatted + * Process the business event data of the XML Formatted * event. *

* This may result in an XML specific data wrapper being returned @@ -257,7 +257,7 @@ private void ensureValidDataElement(Element dataEl) throws CloudEventRWException } /** - * Ensure a CludEvent context attribute representation is as expected. + * Ensure a CloudEvent context attribute representation is as expected. * * @param el * @throws CloudEventRWException diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java index 6783075fe..dbfe77a8c 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java @@ -43,6 +43,13 @@ class XMLUtils { private XMLUtils() { } + /** + * Parse a byte stream into an XML {@link Document} + * + * @param data + * @return {@link Document} + * @throws CloudEventRWException + */ static Document parseIntoDocument(byte[] data) throws CloudEventRWException { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); @@ -57,6 +64,13 @@ static Document parseIntoDocument(byte[] data) throws CloudEventRWException { } + /** + * Obtain a byte array representation of a {@link Document} + * + * @param doc {@link Document} + * @return byte[] + * @throws TransformerException + */ static byte[] documentToBytes(Document doc) throws TransformerException { // Build our transformer diff --git a/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java b/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java index 5c40da380..37c9d2c5b 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java @@ -25,7 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; -public class TestUtils { +class TestUtils { /** * Get a File forn item in the resource path. diff --git a/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java b/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java index e3da317e6..0c3303573 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java @@ -158,7 +158,7 @@ public void deserialize(String xmlFile) throws IOException { * Round-trip test starting with an XML Formated event * 1. Deserialize an XML Formated Event into a CE * 2. Serialize the CE back into XML - * 3. Compare the orginal (expected) and new XML document + * 3. Compare the original (expected) and new XML document */ public void roundTrip(String fileName) throws IOException { From 91b2b382be711609c2d39e35f59df4fc4b107a0b Mon Sep 17 00:00:00 2001 From: "Day, Jeremy(jday)" Date: Thu, 17 Mar 2022 15:27:11 -0700 Subject: [PATCH 08/13] Minor tweak to trigger build Signed-off-by: Day, Jeremy(jday) Signed-off-by: Jem Day --- .../xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java index a33e055ef..d93b099e1 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java @@ -38,7 +38,7 @@ public interface XMLCloudEventData extends CloudEventData { * Wraps an XML {@link Document} * * @param xmlDoc {@link Document} - * @return {@link XMLCloudEventData} + * @return The wrapping {@link XMLCloudEventData} */ static CloudEventData wrap(Document xmlDoc) { return new XMLDataWrapper(xmlDoc); From 87b4992ee6f33eb35277805b6b219cf55cae9b31 Mon Sep 17 00:00:00 2001 From: "Day, Jeremy(jday)" Date: Thu, 24 Mar 2022 10:36:04 -0700 Subject: [PATCH 09/13] Add checks for duplicated CE atrtributes in XML document Signed-off-by: Day, Jeremy(jday) Signed-off-by: Jem Day --- .../io/cloudevents/xml/OccuranceTracker.java | 46 +++++++++++++++++++ .../io/cloudevents/xml/XMLDeserializer.java | 9 +++- .../cloudevents/xml/OccuranceTrackerTest.java | 44 ++++++++++++++++++ .../test/resources/bad/bad_duplicate_attr.xml | 10 ++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/OccuranceTracker.java create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/OccuranceTrackerTest.java create mode 100644 formats/xml/src/test/resources/bad/bad_duplicate_attr.xml diff --git a/formats/xml/src/main/java/io/cloudevents/xml/OccuranceTracker.java b/formats/xml/src/main/java/io/cloudevents/xml/OccuranceTracker.java new file mode 100644 index 000000000..e3d6996b7 --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/OccuranceTracker.java @@ -0,0 +1,46 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.cloudevents.xml; + +import java.util.HashSet; +import java.util.Set; + +/** + * Tracks the occurances of a key to ensure only a single + * instance is allowed. + * + * Used to ensure that each CloudEvent context attribute + * only occurs once in each CloudEvent element instance. + * + */ +class OccuranceTracker { + + private Set keySet; + + OccuranceTracker() { + keySet = new HashSet<>(); + } + + void trackOccurance(String name) throws IllegalStateException { + + if (! keySet.add(name)){ + throw new IllegalStateException(name + ": Occurs more than once"); + } + } + +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java index 322da67f7..bcae6d114 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java @@ -31,6 +31,7 @@ class XMLDeserializer implements CloudEventReader { private final Document xmlDocument; + private final OccuranceTracker ceAtrributeTracker = new OccuranceTracker(); XMLDeserializer(Document doc) { this.xmlDocument = doc; @@ -248,7 +249,7 @@ private void ensureValidDataElement(Element dataEl) throws CloudEventRWException throw CloudEventRWException.newInvalidDataType("data has " + childCount + " children", "1 expected"); } - // And there must be a valid type descriminator + // And there must be a valid type discriminator final String xsiType = dataEl.getAttribute(XMLConstants.XSI_TYPE); if (xsiType == null) { @@ -285,6 +286,12 @@ private void ensureValidContextAttribute(Element el) throws CloudEventRWExceptio } } + // Finally, ensure we only see each CE Attribute once... + try { + ceAtrributeTracker.trackOccurance(localName); + } catch (IllegalStateException e){ + throw CloudEventRWException.newOther(e); + } } private String extractAttributeType(Element e) { diff --git a/formats/xml/src/test/java/io/cloudevents/xml/OccuranceTrackerTest.java b/formats/xml/src/test/java/io/cloudevents/xml/OccuranceTrackerTest.java new file mode 100644 index 000000000..c8729d525 --- /dev/null +++ b/formats/xml/src/test/java/io/cloudevents/xml/OccuranceTrackerTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.cloudevents.xml; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class OccuranceTrackerTest { + + private final OccuranceTracker tracker = new OccuranceTracker(); + + @Test + public void verifyTracking() { + + // These should all work... + Assertions.assertDoesNotThrow(() -> { + tracker.trackOccurance("CE1"); + tracker.trackOccurance("CE2"); + tracker.trackOccurance("ce1"); + }); + + // This should fail + Assertions.assertThrows(IllegalStateException.class, () -> { + tracker.trackOccurance("CE2"); + }); + + } + +} diff --git a/formats/xml/src/test/resources/bad/bad_duplicate_attr.xml b/formats/xml/src/test/resources/bad/bad_duplicate_attr.xml new file mode 100644 index 000000000..196e0fb9f --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_duplicate_attr.xml @@ -0,0 +1,10 @@ + + + 1 + http://localhost/source + mock.test + http://localhost/another + From 9c857c0b7714ca1be84a30977014b1c3d4feefd9 Mon Sep 17 00:00:00 2001 From: "Day, Jeremy(jday)" Date: Fri, 22 Apr 2022 10:30:50 -0700 Subject: [PATCH 10/13] Address review comments Signed-off-by: Day, Jeremy(jday) Signed-off-by: Jem Day --- docs/xml.md | 3 ++- formats/xml/pom.xml | 10 +++++++--- .../main/java/io/cloudevents/xml/XMLDeserializer.java | 4 ++-- .../services/io.cloudevents.core.format.EventFormat | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/xml.md b/docs/xml.md index 26844b069..d0b673dd0 100644 --- a/docs/xml.md +++ b/docs/xml.md @@ -38,7 +38,7 @@ CloudEvent event = CloudEventBuilder.v1() .withSource(URI.create("http://localhost")) .build(); -byte[]serialized = EventFormatProvider +byte[] serialized = EventFormatProvider .getInstance() .resolveFormat(XMLFormat.CONTENT_TYPE) .serialize(event); @@ -70,6 +70,7 @@ CloudEvent event = CloudEventBuilder.v1() .withSource(URI.create("http://localhost")) .withData(myData) .build(); +``` diff --git a/formats/xml/pom.xml b/formats/xml/pom.xml index f2c4628fc..23eb06242 100644 --- a/formats/xml/pom.xml +++ b/formats/xml/pom.xml @@ -16,8 +16,8 @@ ~ --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 @@ -31,6 +31,10 @@ CloudEvents - XML Format jar + + 2.9.0 + + @@ -66,7 +70,7 @@ org.xmlunit xmlunit-core - 2.9.0 + ${xmlunit.version} test diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java index bcae6d114..5f5a28162 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java @@ -85,7 +85,7 @@ public , R> R read( writer.withContextAttribute(attrName, attrValue); } else { if (XMLConstants.XML_DATA_ELEMENT.equals(attrName)) { - // Just remember the data node for now.. + // Just remember the data node for now. dataElement = e; } else { // Handle the extension attributes @@ -135,7 +135,7 @@ public , R> R read( * @param e * @return The first child, or NULL if there isn't one. */ - private Element findFirstElement(Element e) { + private static Element findFirstElement(Element e) { NodeList nodeList = e.getChildNodes(); for (int i = 0; i < nodeList.getLength(); i++) { diff --git a/formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat b/formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat index ae492de58..17fc74eaf 100644 --- a/formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat +++ b/formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat @@ -1 +1 @@ -io.cloudevents.protobuf.XMLFormat +io.cloudevents.xml.XMLFormat From dec4019652dd52bcd42b4b973dfd13ea2daa9a15 Mon Sep 17 00:00:00 2001 From: Jem Day Date: Thu, 29 Dec 2022 11:52:42 -0800 Subject: [PATCH 11/13] JavaDoc now builds, fixed typos Signed-off-by: Jem Day --- formats/xml/pom.xml | 10 +++++++++- ...ccuranceTracker.java => OccurrenceTracker.java} | 10 +++++----- .../java/io/cloudevents/xml/XMLCloudEventData.java | 4 ++-- .../java/io/cloudevents/xml/XMLDeserializer.java | 14 +++++++------- .../java/io/cloudevents/xml/XMLSerializer.java | 4 ++-- .../src/main/java/io/cloudevents/xml/XMLUtils.java | 6 +++--- ...TrackerTest.java => OccurrenceTrackerTest.java} | 12 ++++++------ 7 files changed, 34 insertions(+), 26 deletions(-) rename formats/xml/src/main/java/io/cloudevents/xml/{OccuranceTracker.java => OccurrenceTracker.java} (82%) rename formats/xml/src/test/java/io/cloudevents/xml/{OccuranceTrackerTest.java => OccurrenceTrackerTest.java} (77%) diff --git a/formats/xml/pom.xml b/formats/xml/pom.xml index 23eb06242..9f57fbf23 100644 --- a/formats/xml/pom.xml +++ b/formats/xml/pom.xml @@ -23,7 +23,7 @@ io.cloudevents cloudevents-parent - 2.4.0-SNAPSHOT + 2.5.0-SNAPSHOT ../../pom.xml @@ -32,7 +32,9 @@ jar + io.cloudevents.formats.xml 2.9.0 + 2.3.1 @@ -43,6 +45,12 @@ ${project.version} + + javax.xml.bind + jaxb-api + ${javax.xml.version} + + org.junit.jupiter diff --git a/formats/xml/src/main/java/io/cloudevents/xml/OccuranceTracker.java b/formats/xml/src/main/java/io/cloudevents/xml/OccurrenceTracker.java similarity index 82% rename from formats/xml/src/main/java/io/cloudevents/xml/OccuranceTracker.java rename to formats/xml/src/main/java/io/cloudevents/xml/OccurrenceTracker.java index e3d6996b7..b69dab165 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/OccuranceTracker.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/OccurrenceTracker.java @@ -21,22 +21,22 @@ import java.util.Set; /** - * Tracks the occurances of a key to ensure only a single + * Tracks the occurrences of a key to ensure only a single * instance is allowed. * * Used to ensure that each CloudEvent context attribute * only occurs once in each CloudEvent element instance. * */ -class OccuranceTracker { +class OccurrenceTracker { - private Set keySet; + private final Set keySet; - OccuranceTracker() { + OccurrenceTracker() { keySet = new HashSet<>(); } - void trackOccurance(String name) throws IllegalStateException { + void trackOccurrence(String name) throws IllegalStateException { if (! keySet.add(name)){ throw new IllegalStateException(name + ": Occurs more than once"); diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java index d93b099e1..ed14a29d4 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java @@ -22,12 +22,12 @@ /** * A variant of {@link CloudEventData} that supports direct access - * to data as an XML {@link Document}. + * to data as an XML {@link Document} */ public interface XMLCloudEventData extends CloudEventData { /** - * Get the XML {@link Document} representation of the + * Get an XML Document representation of the * CloudEvent data. * * @return The {@link Document} representation. diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java index 5f5a28162..279983443 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java @@ -31,7 +31,7 @@ class XMLDeserializer implements CloudEventReader { private final Document xmlDocument; - private final OccuranceTracker ceAtrributeTracker = new OccuranceTracker(); + private final OccurrenceTracker ceAttributeTracker = new OccurrenceTracker(); XMLDeserializer(Document doc) { this.xmlDocument = doc; @@ -130,7 +130,7 @@ public , R> R read( // Private Methods -------------------------------------------------------- /** - * Get the first child {@link Element} of an {@link Element} + * Get the first child Element of an Element * * @param e * @return The first child, or NULL if there isn't one. @@ -216,10 +216,10 @@ private CloudEventData processData(Element data) throws CloudEventRWException { } /** - * Ensure that the root elemement of the received XML document is valid + * Ensure that the root element of the received XML document is valid * in our context. * - * @param e The root {@link Element} + * @param e The root Element * @throws CloudEventRWException */ private void checkValidRootElement(Element e) throws CloudEventRWException { @@ -236,7 +236,7 @@ private void checkValidRootElement(Element e) throws CloudEventRWException { } /** - * Ensure the XML `data` element is well formed. + * Ensure the XML `data` element is well-formed. * * @param dataEl * @throws CloudEventRWException @@ -278,7 +278,7 @@ private void ensureValidContextAttribute(Element el) throws CloudEventRWExceptio throw CloudEventRWException.newInvalidDataType(localName, " context atttribute names MUST be lowercase"); } - // A bit kludgy, not relevent for 'data' - should refactor + // A bit of a kludge, not relevant for 'data' - should refactor if (!XMLConstants.XML_DATA_ELEMENT.equals(localName)) { // It must not have any children if (XMLUtils.countOfChildElements(el) != 0) { @@ -288,7 +288,7 @@ private void ensureValidContextAttribute(Element el) throws CloudEventRWExceptio // Finally, ensure we only see each CE Attribute once... try { - ceAtrributeTracker.trackOccurance(localName); + ceAttributeTracker.trackOccurrence(localName); } catch (IllegalStateException e){ throw CloudEventRWException.newOther(e); } diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java index d95f44fa4..d024ce9b8 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java @@ -41,14 +41,14 @@ class XMLSerializer { /** - * Convert the CloudEvent to an XML DOM representation. + * Convert a CloudEvent to an XML {@link Document}. * * @param ce * @return */ static Document toDocument(CloudEvent ce) { - // Setup the writer + // Set up the writer XMLCloudEventWriter eventWriter = new XMLCloudEventWriter(ce.getSpecVersion()); // Process the Context Attributes diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java index dbfe77a8c..f9b789e06 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java @@ -47,7 +47,7 @@ private XMLUtils() { * Parse a byte stream into an XML {@link Document} * * @param data - * @return {@link Document} + * @return Document * @throws CloudEventRWException */ static Document parseIntoDocument(byte[] data) throws CloudEventRWException { @@ -65,7 +65,7 @@ static Document parseIntoDocument(byte[] data) throws CloudEventRWException { } /** - * Obtain a byte array representation of a {@link Document} + * Obtain a byte array representation of an {@link Document} * * @param doc {@link Document} * @return byte[] @@ -92,7 +92,7 @@ static byte[] documentToBytes(Document doc) throws TransformerException { /** * Get the number of child elements of an {@link Element} * - * @param e The Element to introspect. + * @param e The {@link Element} to introspect. * @return The count of child elements */ static int countOfChildElements(Element e) { diff --git a/formats/xml/src/test/java/io/cloudevents/xml/OccuranceTrackerTest.java b/formats/xml/src/test/java/io/cloudevents/xml/OccurrenceTrackerTest.java similarity index 77% rename from formats/xml/src/test/java/io/cloudevents/xml/OccuranceTrackerTest.java rename to formats/xml/src/test/java/io/cloudevents/xml/OccurrenceTrackerTest.java index c8729d525..440cc48d4 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/OccuranceTrackerTest.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/OccurrenceTrackerTest.java @@ -20,23 +20,23 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -public class OccuranceTrackerTest { +public class OccurrenceTrackerTest { - private final OccuranceTracker tracker = new OccuranceTracker(); + private final OccurrenceTracker tracker = new OccurrenceTracker(); @Test public void verifyTracking() { // These should all work... Assertions.assertDoesNotThrow(() -> { - tracker.trackOccurance("CE1"); - tracker.trackOccurance("CE2"); - tracker.trackOccurance("ce1"); + tracker.trackOccurrence("CE1"); + tracker.trackOccurrence("CE2"); + tracker.trackOccurrence("ce1"); }); // This should fail Assertions.assertThrows(IllegalStateException.class, () -> { - tracker.trackOccurance("CE2"); + tracker.trackOccurrence("CE2"); }); } From 492de4182f75af6c258f74346f277fd5777c4317 Mon Sep 17 00:00:00 2001 From: Jem Day Date: Tue, 3 Jan 2023 09:11:25 -0800 Subject: [PATCH 12/13] Minor docs tweak Signed-off-by: Jem Day --- docs/xml.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/xml.md b/docs/xml.md index d0b673dd0..6d0640157 100644 --- a/docs/xml.md +++ b/docs/xml.md @@ -1,5 +1,5 @@ --- -title: CloudEvents Protocol Buffers +title: CloudEvents XML Format nav_order: 4 --- @@ -7,9 +7,10 @@ nav_order: 4 [![Javadocs](http://www.javadoc.io/badge/io.cloudevents/cloudevents-xml.svg?color=green)](http://www.javadoc.io/doc/io.cloudevents/cloudevents-xml) -This module provides and `EventFormat` implementation that adhere's -to the CloudEvent XML Format specification. This format also supports -specialized handling for XML CloudEvent `data`. +This module provides and `EventFormat` implementation that adheres +to the CloudEvent XML Format specification. + +This format also supports specialized handling for XML CloudEvent `data`. For Maven based projects, use the following dependency: From de0442f02818ad7d723a6cd43d5143afaf91d37c Mon Sep 17 00:00:00 2001 From: Jem Day Date: Thu, 5 Jan 2023 12:13:13 -0800 Subject: [PATCH 13/13] Address Review Comments Signed-off-by: Jem Day --- .../cloudevents/rw/CloudEventRWException.java | 11 +++ .../io/cloudevents/xml/OccurrenceTracker.java | 16 ++-- .../io/cloudevents/xml/XMLDeserializer.java | 6 +- .../io/cloudevents/xml/XMLSerializer.java | 35 +-------- .../java/io/cloudevents/xml/XMLUtils.java | 45 ++++++++++++ .../xml/OccurrenceTrackerTest.java | 12 +-- .../java/io/cloudevents/xml/XMLUtilsTest.java | 73 ++++++++++++++++++- 7 files changed, 146 insertions(+), 52 deletions(-) diff --git a/api/src/main/java/io/cloudevents/rw/CloudEventRWException.java b/api/src/main/java/io/cloudevents/rw/CloudEventRWException.java index 3648ffc59..3ee53de7d 100644 --- a/api/src/main/java/io/cloudevents/rw/CloudEventRWException.java +++ b/api/src/main/java/io/cloudevents/rw/CloudEventRWException.java @@ -216,4 +216,15 @@ public static CloudEventRWException newOther(Throwable cause) { cause ); } + + /** + * An exception for use where none of the other variants are + * appropriate. + * + * @param msg A description error message. + * @return a new {@link CloudEventRWException} + */ + public static CloudEventRWException newOther(String msg){ + return new CloudEventRWException(CloudEventRWExceptionKind.OTHER, msg); + } } diff --git a/formats/xml/src/main/java/io/cloudevents/xml/OccurrenceTracker.java b/formats/xml/src/main/java/io/cloudevents/xml/OccurrenceTracker.java index b69dab165..bd94de4be 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/OccurrenceTracker.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/OccurrenceTracker.java @@ -24,7 +24,7 @@ * Tracks the occurrences of a key to ensure only a single * instance is allowed. * - * Used to ensure that each CloudEvent context attribute + * Used to help ensure that each CloudEvent context attribute * only occurs once in each CloudEvent element instance. * */ @@ -33,14 +33,18 @@ class OccurrenceTracker { private final Set keySet; OccurrenceTracker() { - keySet = new HashSet<>(); + keySet = new HashSet<>(10); } - void trackOccurrence(String name) throws IllegalStateException { + /** + * Record an occurrence of attribute name. + * @param name The name to track. + * @return boolean true => accepted, false => duplicate name. + */ + boolean trackOccurrence(String name) { + + return keySet.add(name); - if (! keySet.add(name)){ - throw new IllegalStateException(name + ": Occurs more than once"); - } } } diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java index 279983443..11e578976 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java @@ -287,10 +287,8 @@ private void ensureValidContextAttribute(Element el) throws CloudEventRWExceptio } // Finally, ensure we only see each CE Attribute once... - try { - ceAttributeTracker.trackOccurrence(localName); - } catch (IllegalStateException e){ - throw CloudEventRWException.newOther(e); + if ( ! ceAttributeTracker.trackOccurrence(localName)) { + throw CloudEventRWException.newOther(localName + ": Attribute appeared more than once"); } } diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java index d024ce9b8..ff5882282 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java @@ -122,37 +122,6 @@ private void addElement(String name, String xsiType, String value) { } } - /** - * Need refactoring.. - * - * @param contentType - * @return - */ - private boolean isTextContent(String contentType) { - - if (contentType == null) { - return false; - } - - return contentType.startsWith("text/") - || "application/json".equals(contentType) - || "application/xml".equals(contentType) - || contentType.endsWith("+json") - || contentType.endsWith("+xml") - ; - } - - private boolean isXMLContent(String contentType) { - if (contentType == null) { - return false; - } - - return "application/xml".equals(contentType) - || "text/xml".equals(contentType) - || contentType.endsWith("+xml") - ; - } - private void writeXmlData(Document dataDoc) { // Create the wrapper @@ -233,9 +202,9 @@ public Document end(CloudEventData data) throws CloudEventRWException { if (data instanceof XMLCloudEventData) { writeXmlData(((XMLCloudEventData) data).getDocument()); - } else if (isXMLContent(dataContentType)) { + } else if (XMLUtils.isXmlContent(dataContentType)) { writeXmlData(data.toBytes()); - } else if (isTextContent(dataContentType)) { + } else if (XMLUtils.isTextContent(dataContentType)) { // Handle Textual Content addElement("data", XMLConstants.CE_DATA_ATTR_TEXT, new String(data.toBytes())); } else { diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java index f9b789e06..591efd052 100644 --- a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java @@ -36,9 +36,15 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; class XMLUtils { + private static final Pattern XML_PATTERN = Pattern.compile("^(application|text)\\/([a-zA-Z]+\\+)?xml(;.*)*$"); + private static final Pattern TEXT_PATTERN = Pattern.compile("^application\\/([a-zA-Z]+\\+)?(xml|json)(;.*)*$"); + + // Prevent Construction private XMLUtils() { } @@ -101,6 +107,7 @@ static int countOfChildElements(Element e) { return 0; } + int retVal = 0; NodeList nodeLIst = e.getChildNodes(); @@ -115,4 +122,42 @@ static int countOfChildElements(Element e) { return retVal; } + + /** + * Determine if the given content-type string indicates XML content. + * @param contentType + * @return + */ + static boolean isXmlContent(String contentType){ + + if (contentType == null){ + return false; + } + + final Matcher m = XML_PATTERN.matcher(contentType); + + return m.matches(); + + } + + /** + * Detemrine if the given content-type indicates textual content. + * @param contentType + * @return + */ + static boolean isTextContent(String contentType) { + + if (contentType == null) { + return false; + } + + if (contentType.startsWith("text/")) { + return true; + } + + final Matcher m = TEXT_PATTERN.matcher(contentType); + + return m.matches(); + + } } diff --git a/formats/xml/src/test/java/io/cloudevents/xml/OccurrenceTrackerTest.java b/formats/xml/src/test/java/io/cloudevents/xml/OccurrenceTrackerTest.java index 440cc48d4..d22223e07 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/OccurrenceTrackerTest.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/OccurrenceTrackerTest.java @@ -28,16 +28,12 @@ public class OccurrenceTrackerTest { public void verifyTracking() { // These should all work... - Assertions.assertDoesNotThrow(() -> { - tracker.trackOccurrence("CE1"); - tracker.trackOccurrence("CE2"); - tracker.trackOccurrence("ce1"); - }); + Assertions.assertTrue(tracker.trackOccurrence("CE1")); + Assertions.assertTrue(tracker.trackOccurrence("CE2")); + Assertions.assertTrue(tracker.trackOccurrence("ce1")); // This should fail - Assertions.assertThrows(IllegalStateException.class, () -> { - tracker.trackOccurrence("CE2"); - }); + Assertions.assertFalse(tracker.trackOccurrence("CE2")); } diff --git a/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java b/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java index 914d17473..3052820e1 100644 --- a/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java +++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java @@ -16,19 +16,24 @@ */ package io.cloudevents.xml; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.w3c.dom.Document; import org.w3c.dom.Element; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; public class XMLUtilsTest { @Test - public void voidTestChildCount() throws ParserConfigurationException { + public void testChildCount() throws ParserConfigurationException { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document doc = dbf.newDocumentBuilder().newDocument(); @@ -52,4 +57,70 @@ public void voidTestChildCount() throws ParserConfigurationException { assertThat(XMLUtils.countOfChildElements(root)).isEqualTo(2); } + + @ParameterizedTest + @MethodSource("xmlTestContentTypes") + public void testXmlContentType(String contentType, boolean expected) { + + Assertions.assertEquals(expected, XMLUtils.isXmlContent(contentType), contentType); + } + + @ParameterizedTest + @MethodSource("textTestContentTypes") + public void testTextContentType(String contentType, boolean expected) { + + Assertions.assertEquals(expected, XMLUtils.isTextContent(contentType), contentType); + + } + + static Stream xmlTestContentTypes() { + + return Stream.of( + + // Good Examples + Arguments.of("application/xml", true), + Arguments.of("application/xml;charset=utf-8", true), + Arguments.of("application/xml;\tcharset = \"utf-8\"", true), + Arguments.of("application/cloudevents+xml;charset=UTF-8", true), + Arguments.of("application/cloudevents+xml", true), + Arguments.of("text/xml", true), + Arguments.of("text/xml;charset=utf-8", true), + Arguments.of("text/cloudevents+xml;charset=UTF-8", true), + Arguments.of("text/xml;\twhatever", true), + Arguments.of("text/xml; boundary=something", true), + Arguments.of("text/xml;foo=\"bar\"", true), + Arguments.of("text/xml; charset = \"us-ascii\"", true), + Arguments.of("text/xml; \t", true), + Arguments.of("text/xml;", true), + + // Bad Examples + + Arguments.of("applications/xml", false), + Arguments.of("application/xmll", false), + Arguments.of("application/fobar", false), + Arguments.of("text/json ", false), + Arguments.of("text/json ;", false), + Arguments.of("test/xml", false), + Arguments.of("application/json", false) + + ); + } + + static Stream textTestContentTypes() { + + return Stream.of( + + // Text Content + Arguments.of("text/foo", true), + Arguments.of("text/plain", true), + Arguments.of("application/xml", true), + Arguments.of("application/json", true), + Arguments.of("application/foo+json", true), + + // Not Text Content + Arguments.of("image/png", false) + + ); + } + }