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:
parent
40fe91a5e0
commit
433ec5b274
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
title: CloudEvents XML Format
|
||||
nav_order: 4
|
||||
---
|
||||
|
||||
# CloudEvents XML Format
|
||||
|
||||
[](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();
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -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>
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
io.cloudevents.xml.XMLFormat
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue