From 05d8658b08acc9656f661f8c4a9cfc2f2cbabe23 Mon Sep 17 00:00:00 2001 From: Matthias Wessendorf Date: Wed, 12 Dec 2018 11:08:05 +0100 Subject: [PATCH] :snowman: Refactoring, and adding better JSON API access Signed-off-by: Matthias Wessendorf --- .../main/java/io/cloudevents/CloudEvent.java | 4 + .../impl/DefaultCloudEventImpl.java | 1 + .../main/java/io/cloudevents/json/Json.java | 113 ++++++++++++++++ .../ZonedDateTimeDeserializer.java | 2 +- .../ZonedDateTimeSerializer.java | 2 +- .../io/cloudevents/CloudEventJacksonTest.java | 10 +- .../json/CustomEventTypesTest.java | 56 ++++++++ .../json/types/GlusterVolumeClaim.java | 121 ++++++++++++++++++ .../io/cloudevents/util/JacksonMapper.java | 79 ------------ api/src/test/resources/pvc.json | 17 +++ .../java/io/cloudevents/AbstractTestBase.java | 2 - .../http/vertx/impl/VertxCloudEventsImpl.java | 19 +-- 12 files changed, 321 insertions(+), 105 deletions(-) create mode 100644 api/src/main/java/io/cloudevents/json/Json.java rename api/src/main/java/io/cloudevents/{impl => json}/ZonedDateTimeDeserializer.java (98%) rename api/src/main/java/io/cloudevents/{impl => json}/ZonedDateTimeSerializer.java (97%) create mode 100644 api/src/test/java/io/cloudevents/json/CustomEventTypesTest.java create mode 100644 api/src/test/java/io/cloudevents/json/types/GlusterVolumeClaim.java delete mode 100644 api/src/test/java/io/cloudevents/util/JacksonMapper.java create mode 100644 api/src/test/resources/pvc.json diff --git a/api/src/main/java/io/cloudevents/CloudEvent.java b/api/src/main/java/io/cloudevents/CloudEvent.java index e133b016..faf0de53 100644 --- a/api/src/main/java/io/cloudevents/CloudEvent.java +++ b/api/src/main/java/io/cloudevents/CloudEvent.java @@ -15,6 +15,9 @@ */ package io.cloudevents; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.cloudevents.impl.DefaultCloudEventImpl; + import java.net.URI; import java.time.ZonedDateTime; import java.util.List; @@ -24,6 +27,7 @@ import java.util.Optional; * An abstract event envelope, representing the 0.2 version of the CNCF CloudEvent spec. * */ +@JsonDeserialize(as = DefaultCloudEventImpl.class) public interface CloudEvent { /** diff --git a/api/src/main/java/io/cloudevents/impl/DefaultCloudEventImpl.java b/api/src/main/java/io/cloudevents/impl/DefaultCloudEventImpl.java index ba08811b..dcec374d 100644 --- a/api/src/main/java/io/cloudevents/impl/DefaultCloudEventImpl.java +++ b/api/src/main/java/io/cloudevents/impl/DefaultCloudEventImpl.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.cloudevents.CloudEvent; import io.cloudevents.Extension; +import io.cloudevents.json.ZonedDateTimeDeserializer; import java.io.Serializable; import java.net.URI; diff --git a/api/src/main/java/io/cloudevents/json/Json.java b/api/src/main/java/io/cloudevents/json/Json.java new file mode 100644 index 00000000..ec559276 --- /dev/null +++ b/api/src/main/java/io/cloudevents/json/Json.java @@ -0,0 +1,113 @@ +/** + * Copyright 2018 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.json; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import io.cloudevents.CloudEvent; +import io.cloudevents.impl.DefaultCloudEventImpl; + +import java.io.InputStream; +import java.time.ZonedDateTime; + +public final class Json { + + public static final ObjectMapper MAPPER = new ObjectMapper(); + + static { + // add Jackson datatype for ZonedDateTime + MAPPER.registerModule(new Jdk8Module()); + + final SimpleModule module = new SimpleModule(); + module.addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer()); + module.addDeserializer(ZonedDateTime.class, new ZonedDateTimeDeserializer()); + MAPPER.registerModule(module); + } + + /** + * Encode a POJO to JSON using the underlying Jackson mapper. + * + * @param obj a POJO + * @return a String containing the JSON representation of the given POJO. + * @throws IllegalStateException if a property cannot be encoded. + */ + public static String encode(final Object obj) throws IllegalStateException { + try { + return MAPPER.writeValueAsString(obj); + } catch (Exception e) { + throw new IllegalStateException("Failed to encode as JSON: " + e.getMessage()); + } + } + + public static CloudEvent fromInputStream(final InputStream inputStream) { + try { + return MAPPER.readValue(inputStream, DefaultCloudEventImpl.class); + } catch (Exception e) { + throw new IllegalStateException("Failed to encode as JSON: " + e.getMessage()); + } + } + + /** + * Decode a given JSON string to a CloudEvent . + * + * @param str the JSON string. + * @return an instance of CloudEvent + * @throws IllegalStateException when there is a parsing or invalid mapping. + */ + public static DefaultCloudEventImpl decodeCloudEvent(final String str) throws IllegalStateException { + return decodeValue(str, DefaultCloudEventImpl.class); + } + + /** + * Decode a given JSON string to a POJO of the given class type. + * + * @param str the JSON string. + * @param clazz the class to map to. + * @param the generic type. + * @return an instance of T + * @throws IllegalStateException when there is a parsing or invalid mapping. + */ + protected static T decodeValue(final String str, final Class clazz) throws IllegalStateException { + try { + return MAPPER.readValue(str, clazz); + } catch (Exception e) { + throw new IllegalStateException("Failed to decode: " + e.getMessage()); + } + } + + /** + * Decode a given JSON string to a POJO of the given type. + * + * @param str the JSON string. + * @param type the type to map to. + * @param the generic type. + * @return an instance of T + * @throws IllegalStateException when there is a parsing or invalid mapping. + */ + public static T decodeValue(final String str, final TypeReference type) throws IllegalStateException { + try { + return MAPPER.readValue(str, type); + } catch (Exception e) { + throw new IllegalStateException("Failed to decode: " + e.getMessage(), e); + } + } + + private Json() { + // no-op + } +} diff --git a/api/src/main/java/io/cloudevents/impl/ZonedDateTimeDeserializer.java b/api/src/main/java/io/cloudevents/json/ZonedDateTimeDeserializer.java similarity index 98% rename from api/src/main/java/io/cloudevents/impl/ZonedDateTimeDeserializer.java rename to api/src/main/java/io/cloudevents/json/ZonedDateTimeDeserializer.java index 793fbf8d..3017646e 100644 --- a/api/src/main/java/io/cloudevents/impl/ZonedDateTimeDeserializer.java +++ b/api/src/main/java/io/cloudevents/json/ZonedDateTimeDeserializer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.cloudevents.impl; +package io.cloudevents.json; import java.io.IOException; import java.time.DateTimeException; diff --git a/api/src/main/java/io/cloudevents/impl/ZonedDateTimeSerializer.java b/api/src/main/java/io/cloudevents/json/ZonedDateTimeSerializer.java similarity index 97% rename from api/src/main/java/io/cloudevents/impl/ZonedDateTimeSerializer.java rename to api/src/main/java/io/cloudevents/json/ZonedDateTimeSerializer.java index 63213cf6..484e0473 100644 --- a/api/src/main/java/io/cloudevents/impl/ZonedDateTimeSerializer.java +++ b/api/src/main/java/io/cloudevents/json/ZonedDateTimeSerializer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.cloudevents.impl; +package io.cloudevents.json; import java.io.IOException; import java.time.ZonedDateTime; diff --git a/api/src/test/java/io/cloudevents/CloudEventJacksonTest.java b/api/src/test/java/io/cloudevents/CloudEventJacksonTest.java index 50784158..6dffe97f 100644 --- a/api/src/test/java/io/cloudevents/CloudEventJacksonTest.java +++ b/api/src/test/java/io/cloudevents/CloudEventJacksonTest.java @@ -15,7 +15,7 @@ */ package io.cloudevents; -import io.cloudevents.util.JacksonMapper; +import io.cloudevents.json.Json; import org.junit.Test; import java.net.URI; @@ -29,14 +29,14 @@ public class CloudEventJacksonTest { @Test public void testParseAzure01JSON() { - CloudEvent> ce = JacksonMapper.fromInputStream(Thread.currentThread().getContextClassLoader().getResourceAsStream("01_azure.json")); + CloudEvent> ce = Json.fromInputStream(Thread.currentThread().getContextClassLoader().getResourceAsStream("01_azure.json")); assertThat(ce.getSpecVersion()).isEqualTo(SpecVersion.V_01.toString()); assertAzureCloudEvent(ce); } @Test public void testParseAzure02JSON() { - CloudEvent> ce = JacksonMapper.fromInputStream(Thread.currentThread().getContextClassLoader().getResourceAsStream("02_azure.json")); + CloudEvent> ce = Json.fromInputStream(Thread.currentThread().getContextClassLoader().getResourceAsStream("02_azure.json")); assertThat(ce.getSpecVersion()).isEqualTo(SpecVersion.V_02.toString()); assertAzureCloudEvent(ce); } @@ -56,13 +56,13 @@ public class CloudEventJacksonTest { @Test public void testParseAmazon01JSON() { - CloudEvent ce = JacksonMapper.fromInputStream(Thread.currentThread().getContextClassLoader().getResourceAsStream("01_aws.json")); + CloudEvent ce = Json.fromInputStream(Thread.currentThread().getContextClassLoader().getResourceAsStream("01_aws.json")); assertAmazonCloudEvent(ce); } @Test public void testParseAmazon02JSON() { - CloudEvent ce = JacksonMapper.fromInputStream(Thread.currentThread().getContextClassLoader().getResourceAsStream("02_aws.json")); + CloudEvent ce = Json.fromInputStream(Thread.currentThread().getContextClassLoader().getResourceAsStream("02_aws.json")); assertAmazonCloudEvent(ce); } diff --git a/api/src/test/java/io/cloudevents/json/CustomEventTypesTest.java b/api/src/test/java/io/cloudevents/json/CustomEventTypesTest.java new file mode 100644 index 00000000..9d51d8f8 --- /dev/null +++ b/api/src/test/java/io/cloudevents/json/CustomEventTypesTest.java @@ -0,0 +1,56 @@ +/** + * Copyright 2018 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.json; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.cloudevents.CloudEvent; +import io.cloudevents.CloudEventBuilder; +import io.cloudevents.json.types.GlusterVolumeClaim; +import org.junit.Test; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; +import java.util.UUID; + +import static io.cloudevents.json.Json.MAPPER; +import static org.assertj.core.api.Assertions.assertThat; +public class CustomEventTypesTest { + + @Test + public void testBinding() throws IOException { + + // given + final Map storagePayload = (MAPPER.readValue(Thread.currentThread().getContextClassLoader().getResourceAsStream("pvc.json"), Map.class)); + final CloudEvent> storageCloudEventWrapper = new CloudEventBuilder>() + .type("ProvisioningSucceeded") + .source(URI.create("/scheduler")) + .id(UUID.randomUUID().toString()) + .data(storagePayload) + .build(); + + // when + final String httpSerializedPayload = MAPPER.writeValueAsString(storageCloudEventWrapper); + assertThat(httpSerializedPayload).contains("PersistentVolumeClaim"); + //PARSE into real object, on the other side + final CloudEvent event = Json.decodeValue(httpSerializedPayload, new TypeReference>() {}); + + // then + assertThat(event.getData().get()).isNotNull(); + assertThat(event.getData().get().getSpec().getCapacity().get("storage")).isEqualTo("2Gi"); + assertThat(event.getData().get().getSpec().getAccessModes()).containsExactly("ReadWriteMany"); + } +} diff --git a/api/src/test/java/io/cloudevents/json/types/GlusterVolumeClaim.java b/api/src/test/java/io/cloudevents/json/types/GlusterVolumeClaim.java new file mode 100644 index 00000000..3e59147d --- /dev/null +++ b/api/src/test/java/io/cloudevents/json/types/GlusterVolumeClaim.java @@ -0,0 +1,121 @@ +/** + * Copyright 2018 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.json.types; + +import java.util.List; +import java.util.Map; + +public class GlusterVolumeClaim { + + private String apiVersion = null; + + private String kind = null; + + private Map metadata = null; + + private PVCSpec spec = null; + + public String getApiVersion() { + return apiVersion; + } + + public void setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + } + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public PVCSpec getSpec() { + return spec; + } + + public void setSpec(PVCSpec spec) { + this.spec = spec; + } + + public static class PVCSpec { + public PVCSpec() { + + } + private Map capacity; + private List accessModes; + private GlusterFS glusterfs; + + public Map getCapacity() { + return capacity; + } + + public void setCapacity(Map capacity) { + this.capacity = capacity; + } + + public List getAccessModes() { + return accessModes; + } + + public void setAccessModes(List accessModes) { + this.accessModes = accessModes; + } + + public GlusterFS getGlusterfs() { + return glusterfs; + } + + public void setGlusterfs(GlusterFS glusterfs) { + this.glusterfs = glusterfs; + } + }; + + public static class GlusterFS { + + public GlusterFS() { + + } + private String endpoints; + private String path; + + public String getEndpoints() { + return endpoints; + } + + public void setEndpoints(String endpoint) { + this.endpoints = endpoint; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + } + +} diff --git a/api/src/test/java/io/cloudevents/util/JacksonMapper.java b/api/src/test/java/io/cloudevents/util/JacksonMapper.java deleted file mode 100644 index d7dc8e6d..00000000 --- a/api/src/test/java/io/cloudevents/util/JacksonMapper.java +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright 2018 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.util; - - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import io.cloudevents.CloudEvent; -import io.cloudevents.impl.DefaultCloudEventImpl; - -import java.io.IOException; -import java.io.InputStream; -import java.time.DateTimeException; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.logging.Logger; - - -public final class JacksonMapper { - - private static final ObjectMapper MAPPER = new ObjectMapper(); - private static final Logger LOGGER = Logger.getLogger(JacksonMapper.class.getName()); - - static { - MAPPER.registerModule(new Jdk8Module()); - - SimpleModule module = new SimpleModule(); - // custom types - module.addDeserializer(ZonedDateTime.class, new TestZonedDateTimeDeserializer()); - MAPPER.registerModule(module); - } - - public static CloudEvent fromInputStream(final InputStream inputStream) { - try { - return MAPPER.readValue(inputStream, DefaultCloudEventImpl.class); - } catch (IOException e) { - LOGGER.severe(e.getMessage()); - throw new IllegalStateException("input was not parseable", e); - } - } - - private static class TestZonedDateTimeDeserializer extends StdDeserializer { - - public TestZonedDateTimeDeserializer() { - this(null); - } - - public TestZonedDateTimeDeserializer(Class vc) { - super(vc); - } - - @Override - public ZonedDateTime deserialize(JsonParser jsonparser, DeserializationContext ctxt) throws IOException { - // not serializing timezone data yet - try { - return ZonedDateTime.parse(jsonparser.getText(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); - } catch (DateTimeException e) { - throw new IllegalArgumentException("could not parse"); - } - } - } -} diff --git a/api/src/test/resources/pvc.json b/api/src/test/resources/pvc.json new file mode 100644 index 00000000..9cdee055 --- /dev/null +++ b/api/src/test/resources/pvc.json @@ -0,0 +1,17 @@ +{ + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "name": "gluster-default-volume" + }, + "spec": { + "capacity": { + "storage": "2Gi" + }, + "accessModes": [ "ReadWriteMany" ], + "glusterfs": { + "endpoints": "glusterfs-cluster", + "path": "myVol1" + } + } +} diff --git a/cdi/src/test/java/io/cloudevents/AbstractTestBase.java b/cdi/src/test/java/io/cloudevents/AbstractTestBase.java index e4b12de4..d612ec7d 100644 --- a/cdi/src/test/java/io/cloudevents/AbstractTestBase.java +++ b/cdi/src/test/java/io/cloudevents/AbstractTestBase.java @@ -15,7 +15,6 @@ */ package io.cloudevents; -import io.cloudevents.impl.DefaultCloudEventImpl; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; @@ -25,7 +24,6 @@ public abstract class AbstractTestBase { public static JavaArchive createFrameworkDeployment() { return ShrinkWrap.create(JavaArchive.class) - .addPackage(DefaultCloudEventImpl.class.getPackage()) .addPackage(MockProvider.class.getPackage()) .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); } diff --git a/http/vertx/src/main/java/io/cloudevents/http/vertx/impl/VertxCloudEventsImpl.java b/http/vertx/src/main/java/io/cloudevents/http/vertx/impl/VertxCloudEventsImpl.java index d9bbf341..2303b731 100644 --- a/http/vertx/src/main/java/io/cloudevents/http/vertx/impl/VertxCloudEventsImpl.java +++ b/http/vertx/src/main/java/io/cloudevents/http/vertx/impl/VertxCloudEventsImpl.java @@ -15,8 +15,6 @@ */ package io.cloudevents.http.vertx.impl; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import io.cloudevents.CloudEvent; import io.cloudevents.CloudEventBuilder; import io.cloudevents.Extension; @@ -24,9 +22,7 @@ import io.cloudevents.SpecVersion; import io.cloudevents.http.HttpTransportAttributes; import io.cloudevents.http.V02HttpTransportMappers; import io.cloudevents.http.vertx.VertxCloudEvents; -import io.cloudevents.impl.DefaultCloudEventImpl; -import io.cloudevents.impl.ZonedDateTimeDeserializer; -import io.cloudevents.impl.ZonedDateTimeSerializer; +import io.cloudevents.json.Json; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; @@ -35,7 +31,6 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.json.Json; import io.vertx.core.json.JsonObject; import java.lang.reflect.Field; @@ -49,16 +44,6 @@ public final class VertxCloudEventsImpl implements VertxCloudEvents { private final static CharSequence BINARY_TYPE = HttpHeaders.createOptimized("application/json"); private final static CharSequence STRUCTURED_TYPE = HttpHeaders.createOptimized("application/cloudevents+json"); - static { - // add Jackson datatype for ZonedDateTime - Json.mapper.registerModule(new Jdk8Module()); - - final SimpleModule module = new SimpleModule(); - module.addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer()); - module.addDeserializer(ZonedDateTime.class, new ZonedDateTimeDeserializer()); - Json.mapper.registerModule(module); - } - private static String readRequiredHeaderValue(final MultiMap headers, final String headerName) { return requireNonNull(headers.get(headerName)); } @@ -155,7 +140,7 @@ public final class VertxCloudEventsImpl implements VertxCloudEvents { request.bodyHandler((Buffer buff) -> { if (buff.length()>0) { - resultHandler.handle(Future.succeededFuture(Json.decodeValue(buff.toString(), DefaultCloudEventImpl.class))); + resultHandler.handle(Future.succeededFuture(Json.decodeCloudEvent(buff.toString()))); } else { throw new IllegalArgumentException("no cloudevent body"); }