From 433ec5b2744b478ccb7e1566272c0e26b29871a4 Mon Sep 17 00:00:00 2001 From: Jem Day Date: Tue, 10 Jan 2023 00:48:36 -0800 Subject: [PATCH] Initial Implementation of XML Format (#448) Signed-off-by: Day, Jeremy(jday) Signed-off-by: Jem Day --- README.md | 1 + .../cloudevents/rw/CloudEventRWException.java | 11 + .../core/impl/BaseCloudEventBuilder.java | 11 + docs/index.md | 4 + docs/xml.md | 77 ++++ formats/xml/pom.xml | 87 +++++ .../io/cloudevents/xml/OccurrenceTracker.java | 50 +++ .../io/cloudevents/xml/XMLCloudEventData.java | 46 +++ .../java/io/cloudevents/xml/XMLConstants.java | 77 ++++ .../io/cloudevents/xml/XMLDataWrapper.java | 50 +++ .../io/cloudevents/xml/XMLDeserializer.java | 336 ++++++++++++++++++ .../java/io/cloudevents/xml/XMLFormat.java | 71 ++++ .../io/cloudevents/xml/XMLSerializer.java | 223 ++++++++++++ .../java/io/cloudevents/xml/XMLUtils.java | 163 +++++++++ .../io.cloudevents.core.format.EventFormat | 1 + .../io/cloudevents/xml/BadInputDataTest.java | 71 ++++ .../xml/OccurrenceTrackerTest.java | 40 +++ .../java/io/cloudevents/xml/TestUtils.java | 58 +++ .../io/cloudevents/xml/XMLConstantsTest.java | 50 +++ .../cloudevents/xml/XMLDataWrapperTest.java | 54 +++ .../io/cloudevents/xml/XMLFormatTest.java | 244 +++++++++++++ .../java/io/cloudevents/xml/XMLUtilsTest.java | 126 +++++++ .../test/resources/bad/bad_attrib_name.xml | 9 + .../test/resources/bad/bad_data_content_1.xml | 16 + .../test/resources/bad/bad_data_content_2.xml | 13 + .../test/resources/bad/bad_duplicate_attr.xml | 10 + .../src/test/resources/bad/bad_ext_name.xml | 10 + .../src/test/resources/bad/bad_malformed.xml | 13 + .../resources/bad/bad_missing_specver.xml | 9 + .../xml/src/test/resources/bad/bad_no_ns.xml | 10 + formats/xml/src/test/resources/bad/bad_ns.xml | 12 + .../test/resources/bad/bad_root_element.xml | 12 + .../test/resources/bad/bad_spec_version.xml | 9 + formats/xml/src/test/resources/v03/min.xml | 9 + .../xml/src/test/resources/v1/binary_attr.xml | 10 + .../xml/src/test/resources/v1/json_data.xml | 14 + .../test/resources/v1/json_data_with_ext.xml | 17 + formats/xml/src/test/resources/v1/min.xml | 9 + .../xml/src/test/resources/v1/text_data.xml | 13 + .../src/test/resources/v1/with_extensions.xml | 12 + .../xml/src/test/resources/v1/xml_data.xml | 16 + .../test/resources/v1/xml_data_with_ns1.xml | 19 + .../test/resources/v1/xml_data_with_ns2.xml | 19 + .../test/resources/v1/xml_data_with_ns3.xml | 24 ++ pom.xml | 1 + 45 files changed, 2137 insertions(+) create mode 100644 docs/xml.md create mode 100644 formats/xml/pom.xml create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/OccurrenceTracker.java create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLConstants.java create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.java create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java create mode 100644 formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java create mode 100644 formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/OccurrenceTrackerTest.java create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/XMLConstantsTest.java create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/XMLDataWrapperTest.java create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/XMLFormatTest.java create mode 100644 formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java create mode 100644 formats/xml/src/test/resources/bad/bad_attrib_name.xml create mode 100644 formats/xml/src/test/resources/bad/bad_data_content_1.xml create mode 100644 formats/xml/src/test/resources/bad/bad_data_content_2.xml create mode 100644 formats/xml/src/test/resources/bad/bad_duplicate_attr.xml create mode 100644 formats/xml/src/test/resources/bad/bad_ext_name.xml create mode 100644 formats/xml/src/test/resources/bad/bad_malformed.xml create mode 100644 formats/xml/src/test/resources/bad/bad_missing_specver.xml create mode 100644 formats/xml/src/test/resources/bad/bad_no_ns.xml create mode 100644 formats/xml/src/test/resources/bad/bad_ns.xml create mode 100644 formats/xml/src/test/resources/bad/bad_root_element.xml create mode 100644 formats/xml/src/test/resources/bad/bad_spec_version.xml create mode 100644 formats/xml/src/test/resources/v03/min.xml create mode 100644 formats/xml/src/test/resources/v1/binary_attr.xml create mode 100644 formats/xml/src/test/resources/v1/json_data.xml create mode 100644 formats/xml/src/test/resources/v1/json_data_with_ext.xml create mode 100644 formats/xml/src/test/resources/v1/min.xml create mode 100644 formats/xml/src/test/resources/v1/text_data.xml create mode 100644 formats/xml/src/test/resources/v1/with_extensions.xml create mode 100644 formats/xml/src/test/resources/v1/xml_data.xml create mode 100644 formats/xml/src/test/resources/v1/xml_data_with_ns1.xml create mode 100644 formats/xml/src/test/resources/v1/xml_data_with_ns2.xml create mode 100644 formats/xml/src/test/resources/v1/xml_data_with_ns3.xml diff --git a/README.md b/README.md index e5e86ba13..d02ded796 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Javadocs are available on [javadoc.io](https://www.javadoc.io): - [cloudevents-core](https://www.javadoc.io/doc/io.cloudevents/cloudevents-core) - [cloudevents-json-jackson](https://www.javadoc.io/doc/io.cloudevents/cloudevents-json-jackson) - [cloudevents-protobuf](https://www.javadoc.io/doc/io.cloudevents/cloudevents-protobuf) +- [cloudevents-xml](https://www.javadoc.io/doc/io.cloudevents/cloudevents-xml) - [cloudevents-http-basic](https://www.javadoc.io/doc/io.cloudevents/cloudevents-http-basic) - [cloudevents-http-restful-ws](https://www.javadoc.io/doc/io.cloudevents/cloudevents-http-restful-ws) - [cloudevents-http-vertx](https://www.javadoc.io/doc/io.cloudevents/cloudevents-http-vertx) diff --git a/api/src/main/java/io/cloudevents/rw/CloudEventRWException.java b/api/src/main/java/io/cloudevents/rw/CloudEventRWException.java index 3648ffc59..3ee53de7d 100644 --- a/api/src/main/java/io/cloudevents/rw/CloudEventRWException.java +++ b/api/src/main/java/io/cloudevents/rw/CloudEventRWException.java @@ -216,4 +216,15 @@ public static CloudEventRWException newOther(Throwable cause) { cause ); } + + /** + * An exception for use where none of the other variants are + * appropriate. + * + * @param msg A description error message. + * @return a new {@link CloudEventRWException} + */ + public static CloudEventRWException newOther(String msg){ + return new CloudEventRWException(CloudEventRWExceptionKind.OTHER, msg); + } } diff --git a/core/src/main/java/io/cloudevents/core/impl/BaseCloudEventBuilder.java b/core/src/main/java/io/cloudevents/core/impl/BaseCloudEventBuilder.java index 69846c8eb..dbc6a5875 100644 --- a/core/src/main/java/io/cloudevents/core/impl/BaseCloudEventBuilder.java +++ b/core/src/main/java/io/cloudevents/core/impl/BaseCloudEventBuilder.java @@ -124,6 +124,9 @@ public SELF withExtension(@Nonnull String key, @Nonnull String value) { return self; } + // @TODO - I think this method should be removed/deprecated + // **Number** Is NOT a valid CE Context atrribute type. + public SELF withExtension(@Nonnull String key, @Nonnull Number value) { if (!isValidExtensionName(key)) { throw CloudEventRWException.newInvalidExtensionName(key); @@ -132,6 +135,14 @@ public SELF withExtension(@Nonnull String key, @Nonnull Number value) { return self; } + public SELF withExtension(@Nonnull String key, @Nonnull Integer value) { + if (!isValidExtensionName(key)) { + throw CloudEventRWException.newInvalidExtensionName(key); + } + this.extensions.put(key, value); + return self; + } + public SELF withExtension(@Nonnull String key, @Nonnull Boolean value) { if (!isValidExtensionName(key)) { throw CloudEventRWException.newInvalidExtensionName(key); diff --git a/docs/index.md b/docs/index.md index 07664c2ee..15eede441 100644 --- a/docs/index.md +++ b/docs/index.md @@ -42,6 +42,8 @@ Using the Java SDK you can: | - [Jackson](json-jackson.md) | :heavy_check_mark: | :heavy_check_mark: | | Protobuf Event Format | :heavy_check_mark: | :heavy_check_mark: | | - [Proto](protobuf.md) | :heavy_check_mark: | :heavy_check_mark: | +| XML Event Format | :heavy_check_mark: | :heavy_check_mark: | +| - [XML](xml.md) | :heavy_check_mark: | :heavy_check_mark: | | [Kafka Protocol Binding](kafka.md) | :heavy_check_mark: | :heavy_check_mark: | | MQTT Protocol Binding | :x: | :x: | | NATS Protocol Binding | :x: | :x: | @@ -96,6 +98,7 @@ a different feature from the different sub specs of [Jackson](https://github.com/FasterXML/jackson) - [`cloudevents-protobuf`] Implementation of [Protobuf Event format] using code generated from the standard [protoc](https://github.com/protocolbuffers/protobuf) compiler. +- [`cloudevents-xml`] Implementation of the XML Event Format. - [`cloudevents-http-vertx`] Implementation of [HTTP Protocol Binding] with [Vert.x Core](https://vertx.io/) - [`cloudevents-http-restful-ws`] Implementation of [HTTP Protocol Binding] @@ -123,6 +126,7 @@ You can look at the latest published artifacts on [`cloudevents-core`]: https://github.com/cloudevents/sdk-java/tree/master/core [`cloudevents-json-jackson`]: https://github.com/cloudevents/sdk-java/tree/master/formats/json-jackson [`cloudevents-protobuf`]: https://github.com/cloudevents/sdk-java/tree/master/formats/protobuf +[`cloudevents-xml`]: https://github.com/cloudevents/sdk-java/tree/master/formats/xml [`cloudevents-http-vertx`]: https://github.com/cloudevents/sdk-java/tree/master/http/vertx [`cloudevents-http-basic`]: https://github.com/cloudevents/sdk-java/tree/master/http/basic [`cloudevents-http-restful-ws`]: https://github.com/cloudevents/sdk-java/tree/master/http/restful-ws diff --git a/docs/xml.md b/docs/xml.md new file mode 100644 index 000000000..6d0640157 --- /dev/null +++ b/docs/xml.md @@ -0,0 +1,77 @@ +--- +title: CloudEvents XML Format +nav_order: 4 +--- + +# CloudEvents XML Format + +[![Javadocs](http://www.javadoc.io/badge/io.cloudevents/cloudevents-xml.svg?color=green)](http://www.javadoc.io/doc/io.cloudevents/cloudevents-xml) + +This module provides and `EventFormat` implementation that adheres +to the CloudEvent XML Format specification. + +This format also supports specialized handling for XML CloudEvent `data`. + +For Maven based projects, use the following dependency: + +```xml + + io.cloudevents + cloudevents-xml + 2.4.0 + +``` + +## Using the XML Event Format + +You don't need to perform any operation to configure the module, more than +adding the dependency to your project: + +```java +import io.cloudevents.CloudEvent; +import io.cloudevents.core.format.EventFormatProvider; +import io.cloudevents.core.builder.CloudEventBuilder; +import io.cloudevents.xml.XMLFormat; + +CloudEvent event = CloudEventBuilder.v1() + .withId("hello") + .withType("example.xml") + .withSource(URI.create("http://localhost")) + .build(); + +byte[] serialized = EventFormatProvider + .getInstance() + .resolveFormat(XMLFormat.CONTENT_TYPE) + .serialize(event); +``` + +The `EventFormatProvider` will resolve automatically the `XMLFormat` using the +`ServiceLoader` APIs. + +XML Document data handling is supported via the `XMLCloudEventData` +facility. This convenience wrapper can be used with `any` other supported +format. + +```java +import org.w3c.dom.Document; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; +import io.cloudevents.xml.XMLCloudEventData; + +// Create the business event data. +Document xmlDoc = .... ; + +// Wrap it into CloudEventData +CloudEventData myData = XMLCloudEventData.wrap(xmlDoc); + +// Construct the event +CloudEvent event = CloudEventBuilder.v1() + .withId("hello") + .withType("example.xml") + .withSource(URI.create("http://localhost")) + .withData(myData) + .build(); +``` + + + diff --git a/formats/xml/pom.xml b/formats/xml/pom.xml new file mode 100644 index 000000000..9f57fbf23 --- /dev/null +++ b/formats/xml/pom.xml @@ -0,0 +1,87 @@ + + + + 4.0.0 + + + io.cloudevents + cloudevents-parent + 2.5.0-SNAPSHOT + ../../pom.xml + + + cloudevents-xml + CloudEvents - XML Format + jar + + + io.cloudevents.formats.xml + 2.9.0 + 2.3.1 + + + + + + io.cloudevents + cloudevents-core + ${project.version} + + + + javax.xml.bind + jaxb-api + ${javax.xml.version} + + + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + + io.cloudevents + cloudevents-core + tests + test-jar + ${project.version} + test + + + + org.xmlunit + xmlunit-core + ${xmlunit.version} + test + + + + + diff --git a/formats/xml/src/main/java/io/cloudevents/xml/OccurrenceTracker.java b/formats/xml/src/main/java/io/cloudevents/xml/OccurrenceTracker.java new file mode 100644 index 000000000..bd94de4be --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/OccurrenceTracker.java @@ -0,0 +1,50 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

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

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

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.cloudevents.xml; + +import 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 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); + + } + +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java new file mode 100644 index 000000000..ed14a29d4 --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLCloudEventData.java @@ -0,0 +1,46 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

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

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

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

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

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

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.cloudevents.xml; + +import java.util.ArrayList; +import java.util.Collection; + +final class XMLConstants { + + // Namespaces + static final String CE_NAMESPACE = "http://cloudevents.io/xmlformat/V1"; + static final String XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"; + static final String XS_NAMESPACE = "http://www.w3.org/2001/XMLSchema"; + + // CE Attribute Type Designators + static final String CE_ATTR_STRING = "ce:string"; + static final String CE_ATTR_BOOLEAN = "ce:boolean"; + static final String CE_ATTR_INTEGER = "ce:integer"; + static final String CE_ATTR_URI = "ce:uri"; + static final String CE_ATTR_URI_REF = "ce:uriRef"; + static final String CE_ATTR_BINARY = "ce:binary"; + static final String CE_ATTR_TIMESTAMP = "ce:timestamp"; + + // CE Data Type Designators + static final String CE_DATA_ATTR_BINARY = "xs:base64Binary"; + static final String CE_DATA_ATTR_TEXT = "xs:string"; + static final String CE_DATA_ATTR_XML = "xs:any"; + + // General XML Constants + static final String XSI_TYPE = "xsi:type"; + + // Special Element names + static final String XML_DATA_ELEMENT = "data"; + static final String XML_ROOT_ELEMENT = "event"; + + // Bundle these into a collection (probably could be made more efficient) + static final Collection CE_ATTR_LIST = new ArrayList() {{ + add(CE_ATTR_STRING); + add(CE_ATTR_BOOLEAN); + add(CE_ATTR_INTEGER); + add(CE_ATTR_TIMESTAMP); + add(CE_ATTR_URI); + add(CE_ATTR_URI_REF); + add(CE_ATTR_BINARY); + }}; + + static final Collection CE_DATA_ATTRS = new ArrayList() {{ + add(CE_DATA_ATTR_TEXT); + add(CE_DATA_ATTR_BINARY); + add(CE_DATA_ATTR_XML); + }}; + + private XMLConstants() { + } + + static boolean isCloudEventAttributeType(final String type) { + return CE_ATTR_LIST.contains(type); + } + + static boolean isValidDataType(final String type) { + return CE_DATA_ATTRS.contains(type); + } +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java new file mode 100644 index 000000000..1cd7af70b --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDataWrapper.java @@ -0,0 +1,50 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

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

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

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.cloudevents.xml; + +import io.cloudevents.rw.CloudEventRWException; +import org.w3c.dom.Document; + +import javax.xml.transform.TransformerException; + +/** + * Local Implementation of {@link XMLCloudEventData} that + * wraps an XML {@link Document} + */ +class XMLDataWrapper implements XMLCloudEventData { + + private final Document xmlDoc; + + XMLDataWrapper(Document d) { + this.xmlDoc = d; + } + + @Override + public Document getDocument() { + return xmlDoc; + } + + @Override + public byte[] toBytes() { + try { + return XMLUtils.documentToBytes(xmlDoc); + } catch (TransformerException e) { + throw CloudEventRWException.newOther(e); + } + } +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java new file mode 100644 index 000000000..11e578976 --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLDeserializer.java @@ -0,0 +1,336 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

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

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

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.cloudevents.xml; + +import io.cloudevents.CloudEventData; +import io.cloudevents.SpecVersion; +import io.cloudevents.core.data.BytesCloudEventData; +import io.cloudevents.rw.*; +import io.cloudevents.types.Time; +import org.w3c.dom.*; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.net.URI; +import java.util.Base64; + +class XMLDeserializer implements CloudEventReader { + + private final Document xmlDocument; + private final OccurrenceTracker ceAttributeTracker = new OccurrenceTracker(); + + XMLDeserializer(Document doc) { + this.xmlDocument = doc; + } + + // CloudEventReader ------------------------------------------------------- + + @Override + public , R> R read( + CloudEventWriterFactory writerFactory, + CloudEventDataMapper mapper) throws CloudEventRWException { + + // Grab the Root and ensure it's what we expect. + final Element root = xmlDocument.getDocumentElement(); + + checkValidRootElement(root); + + // Get the specversion and build the CE Writer + String specVer = root.getAttribute("specversion"); + + if (specVer == null) { + throw CloudEventRWException.newInvalidSpecVersion("null - Missing XML attribute"); + } + + final SpecVersion specVersion = SpecVersion.parse(specVer); + final CloudEventWriter writer = writerFactory.create(specVersion); + + // Now iterate through the elements + + NodeList nodes = root.getChildNodes(); + Element dataElement = null; + + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + + if (node.getNodeType() == Node.ELEMENT_NODE) { + + Element e = (Element) node; + + // Sanity + 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. + *

+ * This may result in an XML specific data wrapper being returned + * depending on payload. + * + * @param data + * @return {@link CloudEventData} The data wrapper. + * @throws CloudEventRWException + */ + private CloudEventData processData(Element data) throws CloudEventRWException { + CloudEventData retVal = null; + + final String attrType = extractAttributeType(data); + + // Process based on the defined `xsi:type` of the data element. + + switch (attrType) { + case XMLConstants.CE_DATA_ATTR_TEXT: + retVal = new TextCloudEventData(data.getTextContent()); + break; + case XMLConstants.CE_DATA_ATTR_BINARY: + String eData = data.getTextContent(); + retVal = BytesCloudEventData.wrap(Base64.getDecoder().decode(eData)); + break; + case XMLConstants.CE_DATA_ATTR_XML: + try { + // Ensure it's acceptable before we move forward. + ensureValidDataElement(data); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + Document newDoc = dbf.newDocumentBuilder().newDocument(); + + Element eventData = findFirstElement(data); + + Element newRoot = newDoc.createElementNS(eventData.getNamespaceURI(), eventData.getLocalName()); + newDoc.appendChild(newRoot); + + // Copy the children... + NodeList nodesToCopy = eventData.getChildNodes(); + + for (int i = 0; i < nodesToCopy.getLength(); i++) { + Node n = nodesToCopy.item(i); + + if (n.getNodeType() == Node.ELEMENT_NODE) { + Node newNode = newDoc.importNode(n, true); + newRoot.appendChild(newNode); + } + } + + newDoc.normalizeDocument(); + retVal = XMLCloudEventData.wrap(newDoc); + + } catch (ParserConfigurationException e) { + throw CloudEventRWException.newDataConversion(e, null, null); + } + break; + default: + // I don't believe this is reachable + break; + } + + return retVal; + } + + /** + * Ensure that the root element of the received XML document is valid + * in our context. + * + * @param e The root Element + * @throws CloudEventRWException + */ + private void checkValidRootElement(Element e) throws CloudEventRWException { + + // It must be the name we expect. + if (!XMLConstants.XML_ROOT_ELEMENT.equals(e.getLocalName())) { + throw CloudEventRWException.newInvalidDataType(e.getLocalName(), XMLConstants.XML_ROOT_ELEMENT); + } + + // It must be in the CE namespace. + if (!XMLConstants.CE_NAMESPACE.equalsIgnoreCase(e.getNamespaceURI())) { + throw CloudEventRWException.newInvalidDataType(e.getNamespaceURI(), "Namespace: " + XMLConstants.CE_NAMESPACE); + } + } + + /** + * Ensure the XML `data` element is well-formed. + * + * @param dataEl + * @throws CloudEventRWException + */ + private void ensureValidDataElement(Element dataEl) throws CloudEventRWException { + + // It must have a single child + final int childCount = XMLUtils.countOfChildElements(dataEl); + if (childCount != 1) { + throw CloudEventRWException.newInvalidDataType("data has " + childCount + " children", "1 expected"); + } + + // And there must be a valid type discriminator + final String xsiType = dataEl.getAttribute(XMLConstants.XSI_TYPE); + + if (xsiType == null) { + throw CloudEventRWException.newInvalidDataType("NULL", "xsi:type oneOf [xs:base64Binary, xs:string, xs:any]"); + } + } + + /** + * Ensure a CloudEvent context attribute representation is as expected. + * + * @param el + * @throws CloudEventRWException + */ + private void ensureValidContextAttribute(Element el) throws CloudEventRWException { + + final String localName = el.getLocalName(); + + // It must be in our namespace + if (!XMLConstants.CE_NAMESPACE.equals(el.getNamespaceURI())) { + final String allowedTxt = el.getLocalName() + " Expected namespace: " + XMLConstants.CE_NAMESPACE; + throw CloudEventRWException.newInvalidDataType(el.getNamespaceURI(), allowedTxt); + } + + // It must be all lowercase + if (!allLowerCase(localName)) { + throw CloudEventRWException.newInvalidDataType(localName, " context atttribute names MUST be lowercase"); + } + + // A bit of a kludge, not relevant for 'data' - should refactor + if (!XMLConstants.XML_DATA_ELEMENT.equals(localName)) { + // It must not have any children + if (XMLUtils.countOfChildElements(el) != 0) { + throw CloudEventRWException.newInvalidDataType(el.getLocalName(), "Unexpected child element(s)"); + } + } + + // Finally, ensure we only see each CE Attribute once... + if ( ! ceAttributeTracker.trackOccurrence(localName)) { + throw CloudEventRWException.newOther(localName + ": Attribute appeared more than once"); + } + } + + private String extractAttributeType(Element e) { + + final Attr a = e.getAttributeNodeNS(XMLConstants.XSI_NAMESPACE, "type"); + + if (a != null) { + return a.getValue(); + } else { + return null; + } + } + + private boolean allLowerCase(String s) { + if (s == null) { + return false; + } + + for (int i = 0; i < s.length(); i++) { + if (Character.isUpperCase(s.charAt(i))) { + return false; + } + } + + return true; + } + + // DataWrapper Inner Classes + + public class TextCloudEventData implements CloudEventData { + + private final String text; + + TextCloudEventData(String text) { + this.text = text; + } + + @Override + public byte[] toBytes() { + return text.getBytes(); + } + } + +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.java new file mode 100644 index 000000000..996f07cf3 --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLFormat.java @@ -0,0 +1,71 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

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

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

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

+ * This {@link EventFormat} only works for {@link io.cloudevents.SpecVersion#V1}, as that was the first version the XML format was defined for. + */ +public class XMLFormat implements EventFormat { + + /** + * The content type for transports sending cloudevents in XML format. + */ + public static final String XML_CONTENT_TYPE = "application/cloudevents+xml"; + + @Override + public byte[] serialize(CloudEvent event) throws EventSerializationException { + + // Convert the CE into an XML Document + Document d = XMLSerializer.toDocument(event); + + try { + // Write out the XML Document + return XMLUtils.documentToBytes(d); + } catch (TransformerException e) { + throw new EventSerializationException(e); + } + } + + @Override + public CloudEvent deserialize(byte[] bytes, CloudEventDataMapper mapper) + throws EventDeserializationException { + + final Document doc = XMLUtils.parseIntoDocument(bytes); + return new XMLDeserializer(doc).read(CloudEventBuilder::fromSpecVersion); + + } + + @Override + public String serializedContentType() { + return XML_CONTENT_TYPE; + } + +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java new file mode 100644 index 000000000..ff5882282 --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLSerializer.java @@ -0,0 +1,223 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

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

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

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.cloudevents.xml; + +import io.cloudevents.CloudEvent; +import io.cloudevents.CloudEventData; +import io.cloudevents.SpecVersion; +import io.cloudevents.core.CloudEventUtils; +import io.cloudevents.core.format.EventSerializationException; +import io.cloudevents.rw.CloudEventContextReader; +import io.cloudevents.rw.CloudEventContextWriter; +import io.cloudevents.rw.CloudEventRWException; +import io.cloudevents.rw.CloudEventWriter; +import io.cloudevents.types.Time; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.net.URI; +import java.time.OffsetDateTime; +import java.util.Base64; + +class XMLSerializer { + + /** + * Convert a CloudEvent to an XML {@link Document}. + * + * @param ce + * @return + */ + static Document toDocument(CloudEvent ce) { + + // Set up the writer + XMLCloudEventWriter eventWriter = new XMLCloudEventWriter(ce.getSpecVersion()); + + // Process the Context Attributes + final CloudEventContextReader cloudEventContextReader = CloudEventUtils.toContextReader(ce); + cloudEventContextReader.readContext(eventWriter); + + // Now handle the Data + + final CloudEventData data = ce.getData(); + if (data != null) { + return eventWriter.end(data); + } else { + return eventWriter.end(); + } + } + + private static class XMLCloudEventWriter implements CloudEventWriter { + + 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; + } + } +} diff --git a/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java new file mode 100644 index 000000000..591efd052 --- /dev/null +++ b/formats/xml/src/main/java/io/cloudevents/xml/XMLUtils.java @@ -0,0 +1,163 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

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

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

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.cloudevents.xml; + +import io.cloudevents.rw.CloudEventRWException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class XMLUtils { + + private static final Pattern XML_PATTERN = Pattern.compile("^(application|text)\\/([a-zA-Z]+\\+)?xml(;.*)*$"); + private static final Pattern TEXT_PATTERN = Pattern.compile("^application\\/([a-zA-Z]+\\+)?(xml|json)(;.*)*$"); + + + // Prevent Construction + private XMLUtils() { + } + + /** + * Parse a byte stream into an XML {@link Document} + * + * @param data + * @return Document + * @throws CloudEventRWException + */ + static Document parseIntoDocument(byte[] data) throws CloudEventRWException { + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + + try { + DocumentBuilder builder = dbf.newDocumentBuilder(); + return builder.parse(new ByteArrayInputStream(data)); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw CloudEventRWException.newOther(e); + } + + } + + /** + * Obtain a byte array representation of an {@link Document} + * + * @param doc {@link Document} + * @return byte[] + * @throws TransformerException + */ + static byte[] documentToBytes(Document doc) throws TransformerException { + + // Build our transformer + TransformerFactory tFactory = TransformerFactory.newInstance(); + Transformer t = tFactory.newTransformer(); + + // Assign the source and result + Source src = new DOMSource(doc); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + StreamResult result = new StreamResult(os); + + // Write out the document + t.transform(src, result); + + // And we're done + return os.toByteArray(); + } + + /** + * Get the number of child elements of an {@link Element} + * + * @param e The {@link Element} to introspect. + * @return The count of child elements + */ + static int countOfChildElements(Element e) { + + if (e == null) { + return 0; + } + + + int retVal = 0; + + NodeList nodeLIst = e.getChildNodes(); + + for (int i = 0; i < nodeLIst.getLength(); i++) { + final Node n = nodeLIst.item(i); + + if (n.getNodeType() == Node.ELEMENT_NODE) { + retVal++; + } + } + + return retVal; + } + + /** + * Determine if the given content-type string indicates XML content. + * @param contentType + * @return + */ + static boolean isXmlContent(String contentType){ + + if (contentType == null){ + return false; + } + + final Matcher m = XML_PATTERN.matcher(contentType); + + return m.matches(); + + } + + /** + * Detemrine if the given content-type indicates textual content. + * @param contentType + * @return + */ + static boolean isTextContent(String contentType) { + + if (contentType == null) { + return false; + } + + if (contentType.startsWith("text/")) { + return true; + } + + final Matcher m = TEXT_PATTERN.matcher(contentType); + + return m.matches(); + + } +} diff --git a/formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat b/formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat new file mode 100644 index 000000000..17fc74eaf --- /dev/null +++ b/formats/xml/src/main/resources/META-INF/services/io.cloudevents.core.format.EventFormat @@ -0,0 +1 @@ +io.cloudevents.xml.XMLFormat diff --git a/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java b/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java new file mode 100644 index 000000000..96c682eaa --- /dev/null +++ b/formats/xml/src/test/java/io/cloudevents/xml/BadInputDataTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

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

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

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

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

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

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.cloudevents.xml; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class OccurrenceTrackerTest { + + private final OccurrenceTracker tracker = new OccurrenceTracker(); + + @Test + public void verifyTracking() { + + // These should all work... + Assertions.assertTrue(tracker.trackOccurrence("CE1")); + Assertions.assertTrue(tracker.trackOccurrence("CE2")); + Assertions.assertTrue(tracker.trackOccurrence("ce1")); + + // This should fail + Assertions.assertFalse(tracker.trackOccurrence("CE2")); + + } + +} diff --git a/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java b/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java new file mode 100644 index 000000000..37c9d2c5b --- /dev/null +++ b/formats/xml/src/test/java/io/cloudevents/xml/TestUtils.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

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

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

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

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

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

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.cloudevents.xml; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class XMLConstantsTest { + + @Test + public void verifyNS() { + assertThat(XMLConstants.CE_NAMESPACE).isEqualTo("http://cloudevents.io/xmlformat/V1"); + } + + public void verifyContextAttributeTypes() { + assertThat(XMLConstants.isCloudEventAttributeType("ce:boolean")).isTrue(); + assertThat(XMLConstants.isCloudEventAttributeType("ce:integer")).isTrue(); + assertThat(XMLConstants.isCloudEventAttributeType("ce:string")).isTrue(); + assertThat(XMLConstants.isCloudEventAttributeType("ce:binary")).isTrue(); + assertThat(XMLConstants.isCloudEventAttributeType("ce:uri")).isTrue(); + assertThat(XMLConstants.isCloudEventAttributeType("ce:uriRef")).isTrue(); + assertThat(XMLConstants.isCloudEventAttributeType("ce:timestamp")).isTrue(); + + assertThat(XMLConstants.CE_ATTR_LIST.size()).isEqualTo(7); + } + + public void verifyDataTypes() { + assertThat(XMLConstants.isValidDataType("xs:string")).isTrue(); + assertThat(XMLConstants.isValidDataType("xs:base64Binary")).isTrue(); + assertThat(XMLConstants.isValidDataType("xs:any")).isTrue(); + + assertThat(XMLConstants.CE_DATA_ATTRS.size()).isEqualTo(3); + + } +} diff --git a/formats/xml/src/test/java/io/cloudevents/xml/XMLDataWrapperTest.java b/formats/xml/src/test/java/io/cloudevents/xml/XMLDataWrapperTest.java new file mode 100644 index 000000000..653e53d01 --- /dev/null +++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLDataWrapperTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

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

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

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

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

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

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.cloudevents.xml; + +import io.cloudevents.CloudEvent; +import io.cloudevents.core.format.EventFormat; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.xmlunit.builder.DiffBuilder; +import org.xmlunit.builder.Input; +import org.xmlunit.diff.*; + +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import static io.cloudevents.core.test.Data.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class XMLFormatTest { + + private final EventFormat format = new XMLFormat(); + + @Test + public void testRegistration() { + assertThat(format.serializedContentType()).isNotNull(); + assertThat(format.serializedContentType()).isEqualTo("application/cloudevents+xml"); + } + + @Test + public void verifyExtensions() throws IOException { + byte[] raw = TestUtils.getData("v1/with_extensions.xml"); + + CloudEvent ce = format.deserialize(raw); + assertThat(ce).isNotNull(); + + assertExtension(ce, "myinteger", new Integer(42)); + assertExtension(ce, "mystring", "Greetings"); + assertExtension(ce, "myboolean", Boolean.FALSE); + } + + private void assertExtension(CloudEvent ce, String name, Object expected) { + assertThat(ce.getExtension(name)).isNotNull(); + assertThat(ce.getExtension(name)).isInstanceOf(expected.getClass()); + assertThat(ce.getExtension(name)).isEqualTo(expected); + } + + @ParameterizedTest + @MethodSource("serializeTestArgumentsDefault") + /** + * 1. Serialized a CloudEvent object into XML. + * 2. Compare the serialized output with the expected (control) content. + */ + public void serialize(io.cloudevents.CloudEvent input, String xmlFile) throws IOException { + + System.out.println("Serialize(" + xmlFile + ")"); + + // Serialize the event. + byte[] raw = format.serialize(input); + + Assertions.assertNotNull(raw); + Assertions.assertTrue(raw.length > 0); + + System.out.println("Serialized Size : " + raw.length + " bytes"); + + if (xmlFile != null) { + + Source expectedSource = getTestSource(xmlFile); + Source actualSource = Input.fromByteArray(raw).build(); + + assertThat(expectedSource).isNotNull(); + assertThat(actualSource).isNotNull(); + + // Now compare the documents + + Diff diff = DiffBuilder.compare(expectedSource) + .withTest(actualSource) + .ignoreComments() + .ignoreElementContentWhitespace() + .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byName)) + .checkForSimilar() + .build(); + + if (diff.hasDifferences()) { + + // Dump what was actually generated. + dumpXml(raw); + + for (Difference d : diff.getDifferences()) { + System.out.println(d); + } + } + Assertions.assertFalse(diff.hasDifferences(), diff.toString()); + } + + } + + public static Stream 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 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); + } + } +} diff --git a/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java b/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java new file mode 100644 index 000000000..3052820e1 --- /dev/null +++ b/formats/xml/src/test/java/io/cloudevents/xml/XMLUtilsTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

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

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

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.cloudevents.xml; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +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 xmlTestContentTypes() { + + return Stream.of( + + // Good Examples + Arguments.of("application/xml", true), + Arguments.of("application/xml;charset=utf-8", true), + Arguments.of("application/xml;\tcharset = \"utf-8\"", true), + Arguments.of("application/cloudevents+xml;charset=UTF-8", true), + Arguments.of("application/cloudevents+xml", true), + Arguments.of("text/xml", true), + Arguments.of("text/xml;charset=utf-8", true), + Arguments.of("text/cloudevents+xml;charset=UTF-8", true), + Arguments.of("text/xml;\twhatever", true), + Arguments.of("text/xml; boundary=something", true), + Arguments.of("text/xml;foo=\"bar\"", true), + Arguments.of("text/xml; charset = \"us-ascii\"", true), + Arguments.of("text/xml; \t", true), + Arguments.of("text/xml;", true), + + // Bad Examples + + Arguments.of("applications/xml", false), + Arguments.of("application/xmll", false), + Arguments.of("application/fobar", false), + Arguments.of("text/json ", false), + Arguments.of("text/json ;", false), + Arguments.of("test/xml", false), + Arguments.of("application/json", false) + + ); + } + + static Stream textTestContentTypes() { + + return Stream.of( + + // Text Content + Arguments.of("text/foo", true), + Arguments.of("text/plain", true), + Arguments.of("application/xml", true), + Arguments.of("application/json", true), + Arguments.of("application/foo+json", true), + + // Not Text Content + Arguments.of("image/png", false) + + ); + } + +} diff --git a/formats/xml/src/test/resources/bad/bad_attrib_name.xml b/formats/xml/src/test/resources/bad/bad_attrib_name.xml new file mode 100644 index 000000000..c2d238b6e --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_attrib_name.xml @@ -0,0 +1,9 @@ + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/bad/bad_data_content_1.xml b/formats/xml/src/test/resources/bad/bad_data_content_1.xml new file mode 100644 index 000000000..f2efaafa5 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_data_content_1.xml @@ -0,0 +1,16 @@ + + + + + + 1 + http://localhost/source + mock.test + + TEST + TEST + + diff --git a/formats/xml/src/test/resources/bad/bad_data_content_2.xml b/formats/xml/src/test/resources/bad/bad_data_content_2.xml new file mode 100644 index 000000000..aac81ae5f --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_data_content_2.xml @@ -0,0 +1,13 @@ + + + + + + 1 + http://localhost/source + mock.test + This is illegal + diff --git a/formats/xml/src/test/resources/bad/bad_duplicate_attr.xml b/formats/xml/src/test/resources/bad/bad_duplicate_attr.xml new file mode 100644 index 000000000..196e0fb9f --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_duplicate_attr.xml @@ -0,0 +1,10 @@ + + + 1 + http://localhost/source + mock.test + http://localhost/another + diff --git a/formats/xml/src/test/resources/bad/bad_ext_name.xml b/formats/xml/src/test/resources/bad/bad_ext_name.xml new file mode 100644 index 000000000..32fedaa25 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_ext_name.xml @@ -0,0 +1,10 @@ + + + 1 + http://localhost/source + mock.test + hello + diff --git a/formats/xml/src/test/resources/bad/bad_malformed.xml b/formats/xml/src/test/resources/bad/bad_malformed.xml new file mode 100644 index 000000000..1b4ab7582 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_malformed.xml @@ -0,0 +1,13 @@ + + + + + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/bad/bad_missing_specver.xml b/formats/xml/src/test/resources/bad/bad_missing_specver.xml new file mode 100644 index 000000000..641731b69 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_missing_specver.xml @@ -0,0 +1,9 @@ + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/bad/bad_no_ns.xml b/formats/xml/src/test/resources/bad/bad_no_ns.xml new file mode 100644 index 000000000..82d920298 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_no_ns.xml @@ -0,0 +1,10 @@ + + + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/bad/bad_ns.xml b/formats/xml/src/test/resources/bad/bad_ns.xml new file mode 100644 index 000000000..129a99302 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_ns.xml @@ -0,0 +1,12 @@ + + + + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/bad/bad_root_element.xml b/formats/xml/src/test/resources/bad/bad_root_element.xml new file mode 100644 index 000000000..f58bc87f3 --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_root_element.xml @@ -0,0 +1,12 @@ + + + + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/bad/bad_spec_version.xml b/formats/xml/src/test/resources/bad/bad_spec_version.xml new file mode 100644 index 000000000..8c3cd418c --- /dev/null +++ b/formats/xml/src/test/resources/bad/bad_spec_version.xml @@ -0,0 +1,9 @@ + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/v03/min.xml b/formats/xml/src/test/resources/v03/min.xml new file mode 100644 index 000000000..7aba7107c --- /dev/null +++ b/formats/xml/src/test/resources/v03/min.xml @@ -0,0 +1,9 @@ + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/v1/binary_attr.xml b/formats/xml/src/test/resources/v1/binary_attr.xml new file mode 100644 index 000000000..cd45e3655 --- /dev/null +++ b/formats/xml/src/test/resources/v1/binary_attr.xml @@ -0,0 +1,10 @@ + + + 1 + http://localhost/source + mock.test + 4P8ARKo= + diff --git a/formats/xml/src/test/resources/v1/json_data.xml b/formats/xml/src/test/resources/v1/json_data.xml new file mode 100644 index 000000000..bc7199220 --- /dev/null +++ b/formats/xml/src/test/resources/v1/json_data.xml @@ -0,0 +1,14 @@ + + + http://localhost/schema + application/json + {} + sub + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/v1/json_data_with_ext.xml b/formats/xml/src/test/resources/v1/json_data_with_ext.xml new file mode 100644 index 000000000..5f0ebe70d --- /dev/null +++ b/formats/xml/src/test/resources/v1/json_data_with_ext.xml @@ -0,0 +1,17 @@ + + + http://localhost/schema + application/json + {} + sub + + 1 + http://localhost/source + mock.test + aaa + true + 10 + diff --git a/formats/xml/src/test/resources/v1/min.xml b/formats/xml/src/test/resources/v1/min.xml new file mode 100644 index 000000000..8e5ef5131 --- /dev/null +++ b/formats/xml/src/test/resources/v1/min.xml @@ -0,0 +1,9 @@ + + + 1 + http://localhost/source + mock.test + diff --git a/formats/xml/src/test/resources/v1/text_data.xml b/formats/xml/src/test/resources/v1/text_data.xml new file mode 100644 index 000000000..43f84e5f7 --- /dev/null +++ b/formats/xml/src/test/resources/v1/text_data.xml @@ -0,0 +1,13 @@ + + + 1 + http://localhost/source + mock.test + text/plain + Hello World Lorena! + sub + + diff --git a/formats/xml/src/test/resources/v1/with_extensions.xml b/formats/xml/src/test/resources/v1/with_extensions.xml new file mode 100644 index 000000000..c991ad6c4 --- /dev/null +++ b/formats/xml/src/test/resources/v1/with_extensions.xml @@ -0,0 +1,12 @@ + + + 1 + http://localhost/source + mock.test + 42 + Greetings + false + diff --git a/formats/xml/src/test/resources/v1/xml_data.xml b/formats/xml/src/test/resources/v1/xml_data.xml new file mode 100644 index 000000000..476c2febf --- /dev/null +++ b/formats/xml/src/test/resources/v1/xml_data.xml @@ -0,0 +1,16 @@ + + + 1 + http://localhost/source + mock.test + application/xml + + + + + sub + + diff --git a/formats/xml/src/test/resources/v1/xml_data_with_ns1.xml b/formats/xml/src/test/resources/v1/xml_data_with_ns1.xml new file mode 100644 index 000000000..9e17f8e94 --- /dev/null +++ b/formats/xml/src/test/resources/v1/xml_data_with_ns1.xml @@ -0,0 +1,19 @@ + + + 1 + http://localhost/source + mock.test + application/xml + + + + 51.509865 + -0.118092 + + + sub + + diff --git a/formats/xml/src/test/resources/v1/xml_data_with_ns2.xml b/formats/xml/src/test/resources/v1/xml_data_with_ns2.xml new file mode 100644 index 000000000..afe389e7b --- /dev/null +++ b/formats/xml/src/test/resources/v1/xml_data_with_ns2.xml @@ -0,0 +1,19 @@ + + + 1 + http://localhost/source + mock.test + application/xml + + + + 51.509865 + -0.118092 + + + sub + + diff --git a/formats/xml/src/test/resources/v1/xml_data_with_ns3.xml b/formats/xml/src/test/resources/v1/xml_data_with_ns3.xml new file mode 100644 index 000000000..0983eb1c8 --- /dev/null +++ b/formats/xml/src/test/resources/v1/xml_data_with_ns3.xml @@ -0,0 +1,24 @@ + + + 1 + http://localhost/source + mock.test + application/xml + + + + 51.509865 + -0.118092 + + + sub + + diff --git a/pom.xml b/pom.xml index 6be267b78..db0575fc1 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,7 @@ core formats/json-jackson formats/protobuf + formats/xml amqp http/basic http/vertx