Initial Implementation of XML Format (#448)

Signed-off-by: Day, Jeremy(jday) <jday@paypal.com>
Signed-off-by: Jem Day <Jem.Day@cliffhanger.com>
This commit is contained in:
Jem Day 2023-01-10 00:48:36 -08:00 committed by GitHub
parent 40fe91a5e0
commit 433ec5b274
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 2137 additions and 0 deletions

View File

@ -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)

View File

@ -216,4 +216,15 @@ public class CloudEventRWException extends RuntimeException {
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);
}
}

View File

@ -124,6 +124,9 @@ public abstract class BaseCloudEventBuilder<SELF extends BaseCloudEventBuilder<S
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 abstract class BaseCloudEventBuilder<SELF extends BaseCloudEventBuilder<S
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);

View File

@ -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

77
docs/xml.md Normal file
View File

@ -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
<dependency>
<groupId>io.cloudevents</groupId>
<artifactId>cloudevents-xml</artifactId>
<version>2.4.0</version>
</dependency>
```
## 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();
```

87
formats/xml/pom.xml Normal file
View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2021-Present The CloudEvents Authors
~ <p>
~ 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
~ <p>
~ http://www.apache.org/licenses/LICENSE-2.0
~ <p>
~ 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.
~
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.cloudevents</groupId>
<artifactId>cloudevents-parent</artifactId>
<version>2.5.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>cloudevents-xml</artifactId>
<name>CloudEvents - XML Format</name>
<packaging>jar</packaging>
<properties>
<module-name>io.cloudevents.formats.xml</module-name>
<xmlunit.version>2.9.0</xmlunit.version>
<javax.xml.version>2.3.1</javax.xml.version>
</properties>
<dependencies>
<dependency>
<groupId>io.cloudevents</groupId>
<artifactId>cloudevents-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${javax.xml.version}</version>
</dependency>
<!-- Test deps -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj-core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cloudevents</groupId>
<artifactId>cloudevents-core</artifactId>
<classifier>tests</classifier>
<type>test-jar</type>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<version>${xmlunit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,50 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<String> keySet;
OccurrenceTracker() {
keySet = new HashSet<>(10);
}
/**
* 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);
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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);
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<String> CE_ATTR_LIST = new ArrayList<String>() {{
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<String> CE_DATA_ATTRS = new ArrayList<String>() {{
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);
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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);
}
}
}

View File

@ -0,0 +1,336 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 <W extends CloudEventWriter<R>, R> R read(
CloudEventWriterFactory<W, R> writerFactory,
CloudEventDataMapper<? extends CloudEventData> 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) {
throw CloudEventRWException.newInvalidSpecVersion("null - Missing XML attribute");
}
final SpecVersion specVersion = SpecVersion.parse(specVer);
final CloudEventWriter<R> 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
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)) {
// Yep .. Just write it out.
writer.withContextAttribute(attrName, attrValue);
} else {
if (XMLConstants.XML_DATA_ELEMENT.equals(attrName)) {
// Just remember the data node for now.
dataElement = e;
} else {
// Handle the extension attributes
switch (attrType) {
case XMLConstants.CE_ATTR_STRING:
writer.withContextAttribute(attrName, attrValue);
break;
case XMLConstants.CE_ATTR_INTEGER:
writer.withContextAttribute(attrName, Integer.valueOf(attrValue));
break;
case XMLConstants.CE_ATTR_TIMESTAMP:
writer.withContextAttribute(attrName, Time.parseTime(attrValue));
break;
case XMLConstants.CE_ATTR_BOOLEAN:
writer.withContextAttribute(attrName, Boolean.valueOf(attrValue));
break;
case XMLConstants.CE_ATTR_URI:
writer.withContextAttribute(attrName, URI.create(attrValue));
break;
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;
}
}
}
}
}
// And handle any data
if (dataElement != null) {
return writer.end(processData(dataElement));
} else {
return writer.end();
}
}
// Private Methods --------------------------------------------------------
/**
* Get the first child Element of an Element
*
* @param e
* @return The first child, or NULL if there isn't one.
*/
private static 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;
}
/**
* Process the business event data of the XML Formatted
* event.
* <p>
* 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();
}
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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}.
* <p>
* 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;
}
}

View File

@ -0,0 +1,223 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<Document> {
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(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);
}
/**
* 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(XMLConstants.XSI_TYPE, xsiType);
}
e.setTextContent(value);
root.appendChild(e);
// Look for, and remember, the data content type
if ("datacontenttype".equals(name)) {
dataContentType = value;
}
}
private void writeXmlData(Document dataDoc) {
// Create the wrapper
Element e = xmlDocument.createElement("data");
e.setAttribute(XMLConstants.XSI_TYPE, XMLConstants.CE_DATA_ATTR_XML);
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, XMLConstants.CE_ATTR_STRING, value);
return this;
}
@Override
public CloudEventContextWriter withContextAttribute(String name, URI value) throws CloudEventRWException {
addElement(name, XMLConstants.CE_ATTR_URI, value.toString());
return this;
}
@Override
public CloudEventContextWriter withContextAttribute(String name, OffsetDateTime value) throws CloudEventRWException {
addElement(name, XMLConstants.CE_ATTR_TIMESTAMP, 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, XMLConstants.CE_ATTR_INTEGER, value.toString());
return this;
}
@Override
public CloudEventContextWriter withContextAttribute(String name, Boolean value) throws CloudEventRWException {
addElement(name, XMLConstants.CE_ATTR_BOOLEAN, value.toString());
return this;
}
@Override
public CloudEventContextWriter withContextAttribute(String name, byte[] value) throws CloudEventRWException {
addElement(name, XMLConstants.CE_ATTR_BINARY, Base64.getEncoder().encodeToString(value));
return this;
}
@Override
public Document end(CloudEventData data) throws CloudEventRWException {
if (data instanceof XMLCloudEventData) {
writeXmlData(((XMLCloudEventData) data).getDocument());
} else if (XMLUtils.isXmlContent(dataContentType)) {
writeXmlData(data.toBytes());
} else if (XMLUtils.isTextContent(dataContentType)) {
// Handle Textual Content
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", XMLConstants.CE_DATA_ATTR_BINARY, encodedValue);
}
return end();
}
@Override
public Document end() throws CloudEventRWException {
return xmlDocument;
}
}
}

View File

@ -0,0 +1,163 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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();
}
}

View File

@ -0,0 +1 @@
io.cloudevents.xml.XMLFormat

View File

@ -0,0 +1,71 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<Arguments> badDataTestFiles() throws IOException {
File fileDir = TestUtils.getFile("bad");
File[] fileList = fileDir.listFiles();
List<Arguments> argList = new ArrayList<>();
for (File f : fileList) {
argList.add(Arguments.of(f));
}
return argList.stream();
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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"));
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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);
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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);
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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();
}
}

View File

@ -0,0 +1,244 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<Arguments> 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 = TestUtils.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 original (expected) and new XML document
*/
public void roundTrip(String fileName) throws IOException {
byte[] inputData = TestUtils.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<Arguments> 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 StreamSource getStreamSource(byte[] data) {
ByteArrayInputStream bais = new ByteArrayInputStream(data);
return new StreamSource(bais);
}
private Source getTestSource(String filename) throws IOException {
return Input.fromFile(TestUtils.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);
}
}
}

View File

@ -0,0 +1,126 @@
/*
* Copyright 2018-Present The CloudEvents Authors
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<Arguments> 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<Arguments> 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)
);
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<Source>http://localhost/source</Source>
<type>mock.test</type>
</event>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- data is present, but contains multiple elements -->
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
<data xsi:type="xs:any">
<MyData>TEST</MyData>
<MyAdditionalData>TEST</MyAdditionalData>
</data>
</event>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- data is present, but only contains text -->
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
<data xsi:type="xs:any">This is illegal</data>
</event>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
<source>http://localhost/another</source>
</event>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
<MyExtension>hello</MyExtension>
</event>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Just badly formed XML document -->
<!-- type and tripe -->
<event xmlns="http://cloudevents.io/notarealnamespace/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</tripe>
</event>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
>
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
</event>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Missing namspace definition -->
<event xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
</event>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Incorrect target namespace -->
<event xmlns="http://cloudevents.io/notarealnamespace/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
</event>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- The root element is simply not as-per specification -->
<trash xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
</trash>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="2.77">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
</event>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="0.3">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
</event>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
<binary xsi:type="ce:binary">4P8ARKo=</binary>
</event>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<dataschema>http://localhost/schema</dataschema>
<datacontenttype>application/json</datacontenttype>
<data xsi:type="xs:string">{}</data>
<subject>sub</subject>
<time>2018-04-26T14:48:09+02:00</time>
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
</event>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<dataschema>http://localhost/schema</dataschema>
<datacontenttype>application/json</datacontenttype>
<data xsi:type="xs:string">{}</data>
<subject>sub</subject>
<time>2018-04-26T14:48:09+02:00</time>
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
<astring xsi:type="ce:string">aaa</astring>
<aboolean xsi:type="ce:boolean">true</aboolean>
<anumber xsi:type="ce:integer">10</anumber>
</event>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
</event>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
<datacontenttype>text/plain</datacontenttype>
<data xsi:type="xs:string">Hello World Lorena!</data>
<subject>sub</subject>
<time>2018-04-26T14:48:09+02:00</time>
</event>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
<myinteger xsi:type="ce:integer">42</myinteger>
<mystring xsi:type="ce:string">Greetings</mystring>
<myboolean xsi:type="ce:boolean">false</myboolean>
</event>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
<datacontenttype>application/xml</datacontenttype>
<data xsi:type="xs:any">
<!-- NULL namspace -->
<stuff xmlns=""></stuff>
</data>
<subject>sub</subject>
<time>2018-04-26T14:48:09+02:00</time>
</event>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
<datacontenttype>application/xml</datacontenttype>
<data xsi:type="xs:any">
<!-- NS using prefixes -->
<geo:Location xmlns:geo="http://someauthority.example/">
<geo:Latitude>51.509865</geo:Latitude>
<geo:Longitude>-0.118092</geo:Longitude>
</geo:Location>
</data>
<subject>sub</subject>
<time>2018-04-26T14:48:09+02:00</time>
</event>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
<datacontenttype>application/xml</datacontenttype>
<data xsi:type="xs:any">
<!-- NS applied in the scope of the element -->
<Location xmlns="http://someauthority.example/">
<Latitude>51.509865</Latitude>
<Longitude>-0.118092</Longitude>
</Location>
</data>
<subject>sub</subject>
<time>2018-04-26T14:48:09+02:00</time>
</event>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<event xmlns="http://cloudevents.io/xmlformat/V1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:geo="http://someauthority.example/"
specversion="1.0">
<id>1</id>
<source>http://localhost/source</source>
<type>mock.test</type>
<datacontenttype>application/xml</datacontenttype>
<data xsi:type="xs:any">
<!--
NS defined in document root
For completeness only, SDKs are unlikely to emit
content in this format, but it is semantically correct.
-->
<geo:Location>
<geo:Latitude>51.509865</geo:Latitude>
<geo:Longitude>-0.118092</geo:Longitude>
</geo:Location>
</data>
<subject>sub</subject>
<time>2018-04-26T14:48:09+02:00</time>
</event>

View File

@ -70,6 +70,7 @@
<module>core</module>
<module>formats/json-jackson</module>
<module>formats/protobuf</module>
<module>formats/xml</module>
<module>amqp</module>
<module>http/basic</module>
<module>http/vertx</module>