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/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/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/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..6d0640157
--- /dev/null
+++ b/docs/xml.md
@@ -0,0 +1,77 @@
+---
+title: CloudEvents XML Format
+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 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:
+
+```xml
+
+ * 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 occurrences of a key to ensure only a single
+ * instance is allowed.
+ *
+ * Used to help ensure that each CloudEvent context attribute
+ * only occurs once in each CloudEvent element instance.
+ *
+ */
+class OccurrenceTracker {
+
+ private final Set
+ * 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;
+
+/**
+ * A variant of {@link CloudEventData} that supports direct access
+ * to data as an XML {@link Document}
+ */
+public interface XMLCloudEventData extends CloudEventData {
+
+ /**
+ * Get an XML Document representation of the
+ * CloudEvent data.
+ *
+ * @return The {@link Document} representation.
+ */
+ Document getDocument();
+
+ /**
+ * Wraps an XML {@link Document}
+ *
+ * @param xmlDoc {@link Document}
+ * @return The wrapping {@link XMLCloudEventData}
+ */
+ static CloudEventData wrap(Document xmlDoc) {
+ return new XMLDataWrapper(xmlDoc);
+ }
+}
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..40a63341a
--- /dev/null
+++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLConstants.java
@@ -0,0 +1,77 @@
+/*
+ * 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 {
+
+ // 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";
+ 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";
+
+ // 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
+ * 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;
+import org.w3c.dom.Document;
+
+import javax.xml.transform.TransformerException;
+
+/**
+ * Local Implementation of {@link XMLCloudEventData} that
+ * wraps an XML {@link Document}
+ */
+class XMLDataWrapper implements XMLCloudEventData {
+
+ private final Document xmlDoc;
+
+ XMLDataWrapper(Document d) {
+ this.xmlDoc = d;
+ }
+
+ @Override
+ public Document getDocument() {
+ 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..11e578976
--- /dev/null
+++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java
@@ -0,0 +1,336 @@
+/*
+ * 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 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;
+
+class XMLDeserializer implements CloudEventReader {
+
+ private final Document xmlDocument;
+ private final OccurrenceTracker ceAttributeTracker = new OccurrenceTracker();
+
+ XMLDeserializer(Document doc) {
+ this.xmlDocument = doc;
+ }
+
+ // CloudEventReader -------------------------------------------------------
+
+ @Override
+ public
+ * 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());
+ break;
+ case XMLConstants.CE_DATA_ATTR_BINARY:
+ String eData = data.getTextContent();
+ retVal = BytesCloudEventData.wrap(Base64.getDecoder().decode(eData));
+ break;
+ case XMLConstants.CE_DATA_ATTR_XML:
+ try {
+ // Ensure it's acceptable before we move forward.
+ ensureValidDataElement(data);
+
+ 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:
+ // I don't believe this is reachable
+ break;
+ }
+
+ return retVal;
+ }
+
+ /**
+ * Ensure that the root element of the received XML document is valid
+ * in our context.
+ *
+ * @param e The root Element
+ * @throws CloudEventRWException
+ */
+ private void checkValidRootElement(Element e) throws CloudEventRWException {
+
+ // It must be the name we expect.
+ if (!XMLConstants.XML_ROOT_ELEMENT.equals(e.getLocalName())) {
+ throw CloudEventRWException.newInvalidDataType(e.getLocalName(), XMLConstants.XML_ROOT_ELEMENT);
+ }
+
+ // 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
+ final int childCount = XMLUtils.countOfChildElements(dataEl);
+ if (childCount != 1) {
+ throw CloudEventRWException.newInvalidDataType("data has " + childCount + " children", "1 expected");
+ }
+
+ // And there must be a valid type discriminator
+ 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 CloudEvent 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 (!XMLConstants.CE_NAMESPACE.equals(el.getNamespaceURI())) {
+ final String allowedTxt = el.getLocalName() + " Expected namespace: " + XMLConstants.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 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) {
+ throw CloudEventRWException.newInvalidDataType(el.getLocalName(), "Unexpected child element(s)");
+ }
+ }
+
+ // Finally, ensure we only see each CE Attribute once...
+ if ( ! ceAttributeTracker.trackOccurrence(localName)) {
+ throw CloudEventRWException.newOther(localName + ": Attribute appeared more than once");
+ }
+ }
+
+ private String extractAttributeType(Element e) {
+
+ final Attr a = e.getAttributeNodeNS(XMLConstants.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..996f07cf3
--- /dev/null
+++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.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 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 XML 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 extends CloudEventData> 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..ff5882282
--- /dev/null
+++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java
@@ -0,0 +1,223 @@
+/*
+ * 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.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 a CloudEvent to an XML {@link Document}.
+ *
+ * @param ce
+ * @return
+ */
+ static Document toDocument(CloudEvent ce) {
+
+ // Set up 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
+ * 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;
+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;
+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;
+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() {
+ }
+
+ /**
+ * Parse a byte stream into an XML {@link Document}
+ *
+ * @param data
+ * @return Document
+ * @throws CloudEventRWException
+ */
+ 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);
+ }
+
+ }
+
+ /**
+ * Obtain a byte array representation of an {@link Document}
+ *
+ * @param doc {@link Document}
+ * @return byte[]
+ * @throws TransformerException
+ */
+ 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();
+ }
+
+ /**
+ * Get the number of child elements of an {@link Element}
+ *
+ * @param e The {@link 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;
+ }
+
+ /**
+ * 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/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..17fc74eaf
--- /dev/null
+++ b/formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat
@@ -0,0 +1 @@
+io.cloudevents.xml.XMLFormat
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..96c682eaa
--- /dev/null
+++ b/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.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 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 java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+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
+ */
+public class BadInputDataTest {
+
+ private final EventFormat format = new XMLFormat();
+
+ @ParameterizedTest
+ @MethodSource("badDataTestFiles")
+ public void verifyRejection(File testFile) throws IOException {
+
+ byte[] data = TestUtils.getData(testFile);
+
+ assertThatExceptionOfType(CloudEventRWException.class).isThrownBy(() -> {
+ format.deserialize(data);
+ });
+ }
+
+ /**
+ * Obtain a list of all the "bad exmaple" resource files
+ *
+ * @return
+ * @throws IOException
+ */
+ public static Stream
+ * 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 OccurrenceTrackerTest {
+
+ private final OccurrenceTracker tracker = new OccurrenceTracker();
+
+ @Test
+ public void verifyTracking() {
+
+ // These should all work...
+ Assertions.assertTrue(tracker.trackOccurrence("CE1"));
+ Assertions.assertTrue(tracker.trackOccurrence("CE2"));
+ Assertions.assertTrue(tracker.trackOccurrence("ce1"));
+
+ // This should fail
+ Assertions.assertFalse(tracker.trackOccurrence("CE2"));
+
+ }
+
+}
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..37c9d2c5b
--- /dev/null
+++ b/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+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;
+
+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();
+ 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(File dataFile) throws IOException {
+ return Files.readAllBytes(dataFile.toPath());
+ }
+
+ static byte[] getData(String filename) throws IOException {
+ File f = getFile(filename);
+ return getData(f);
+ }
+}
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/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
new file mode 100644
index 000000000..0c3303573
--- /dev/null
+++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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.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.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;
+
+public class XMLFormatTest {
+
+ private final EventFormat format = new XMLFormat();
+
+ @Test
+ public void testRegistration() {
+ assertThat(format.serializedContentType()).isNotNull();
+ 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")
+ /**
+ * 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
+ * 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;
+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 testChildCount() 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);
+
+ }
+
+ @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