From 1617f77b7472af1ea325dd85c8135992fa2d4968 Mon Sep 17 00:00:00 2001 From: Edgar Asatryan Date: Sun, 27 Mar 2022 21:49:54 +0400 Subject: [PATCH] feat: Add complex type support for JSON mappings. --- .../nstdio/http/ext/spi/GsonJsonMapping.java | 15 +++ .../http/ext/spi/JacksonJsonMapping.java | 19 +++ .../nstdio/http/ext/spi/JsonMapping.java | 28 +++++ .../http/ext/spi/GsonJsonMappingTest.java | 54 +------- .../http/ext/spi/JacksonJsonMappingTest.java | 29 +++++ .../ext/spi/JacksonMappingProviderTest.java | 59 --------- .../http/ext/spi/JsonMappingContract.java | 119 ++++++++++++++++++ 7 files changed, 215 insertions(+), 108 deletions(-) create mode 100644 src/spiTest/java/io/github/nstdio/http/ext/spi/JacksonJsonMappingTest.java delete mode 100644 src/spiTest/java/io/github/nstdio/http/ext/spi/JacksonMappingProviderTest.java create mode 100644 src/spiTest/java/io/github/nstdio/http/ext/spi/JsonMappingContract.java diff --git a/src/main/java/io/github/nstdio/http/ext/spi/GsonJsonMapping.java b/src/main/java/io/github/nstdio/http/ext/spi/GsonJsonMapping.java index b109290..1c20dc0 100644 --- a/src/main/java/io/github/nstdio/http/ext/spi/GsonJsonMapping.java +++ b/src/main/java/io/github/nstdio/http/ext/spi/GsonJsonMapping.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.lang.reflect.Type; import java.util.Objects; import static java.nio.charset.StandardCharsets.UTF_8; @@ -48,8 +49,22 @@ public T read(InputStream in, Class targetType) throws IOException { } } + @Override + public T read(InputStream in, Type targetType) throws IOException { + try (var reader = new InputStreamReader(in, UTF_8)) { + return gson.fromJson(reader, targetType); + } catch (JsonParseException e) { + throw new IOException(e); + } + } + @Override public T read(byte[] bytes, Class targetType) throws IOException { return read(new ByteArrayInputStream(bytes), targetType); } + + @Override + public T read(byte[] bytes, Type targetType) throws IOException { + return read(new ByteArrayInputStream(bytes), targetType); + } } diff --git a/src/main/java/io/github/nstdio/http/ext/spi/JacksonJsonMapping.java b/src/main/java/io/github/nstdio/http/ext/spi/JacksonJsonMapping.java index b8f5285..27bda6a 100644 --- a/src/main/java/io/github/nstdio/http/ext/spi/JacksonJsonMapping.java +++ b/src/main/java/io/github/nstdio/http/ext/spi/JacksonJsonMapping.java @@ -16,10 +16,12 @@ package io.github.nstdio.http.ext.spi; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Type; public class JacksonJsonMapping implements JsonMapping { private final ObjectMapper mapper; @@ -40,8 +42,25 @@ public T read(InputStream in, Class targetType) throws IOException { } } + @Override + public T read(InputStream in, Type targetType) throws IOException { + try (var stream = in) { + return mapper.readValue(stream, constructType(targetType)); + } + } + @Override public T read(byte[] bytes, Class targetType) throws IOException { return mapper.readValue(bytes, targetType); } + + @Override + public T read(byte[] bytes, Type targetType) throws IOException { + return mapper.readValue(bytes, constructType(targetType)); + } + + + private JavaType constructType(Type targetType) { + return mapper.constructType(targetType); + } } diff --git a/src/main/java/io/github/nstdio/http/ext/spi/JsonMapping.java b/src/main/java/io/github/nstdio/http/ext/spi/JsonMapping.java index 4af01da..d8a3320 100644 --- a/src/main/java/io/github/nstdio/http/ext/spi/JsonMapping.java +++ b/src/main/java/io/github/nstdio/http/ext/spi/JsonMapping.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Type; /** * The simple strategy for binding JSON to Java objects. @@ -37,6 +38,20 @@ public interface JsonMapping { */ T read(InputStream in, Class targetType) throws IOException; + /** + * Reads JSON data from the {@code in} and creates mapped object of type {@code targetType}. Note that {@code in} + * might not be closed by the underlying implementation and caller should try to close {@code in}. + * + * @param in The input source. + * @param targetType The required type. + * @param The type of object to create. + * + * @return The object created from JSON. + * + * @throws IOException When there is a JSON parsing or binding error or I/O error occurred. + */ + T read(InputStream in, Type targetType) throws IOException; + /** * Reads JSON data from the {@code bytes} and creates mapped object of type {@code targetType}. * @@ -49,4 +64,17 @@ public interface JsonMapping { * @throws IOException When there is a JSON parsing or binding error or I/O error occurred. */ T read(byte[] bytes, Class targetType) throws IOException; + + /** + * Reads JSON data from the {@code bytes} and creates mapped object of type {@code targetType}. + * + * @param bytes The input source. + * @param targetType The required type. + * @param The type of object to create. + * + * @return The object created from JSON. + * + * @throws IOException When there is a JSON parsing or binding error or I/O error occurred. + */ + T read(byte[] bytes, Type targetType) throws IOException; } diff --git a/src/spiTest/java/io/github/nstdio/http/ext/spi/GsonJsonMappingTest.java b/src/spiTest/java/io/github/nstdio/http/ext/spi/GsonJsonMappingTest.java index 4525d00..e058da5 100644 --- a/src/spiTest/java/io/github/nstdio/http/ext/spi/GsonJsonMappingTest.java +++ b/src/spiTest/java/io/github/nstdio/http/ext/spi/GsonJsonMappingTest.java @@ -17,57 +17,13 @@ package io.github.nstdio.http.ext.spi; import io.github.nstdio.http.ext.jupiter.EnabledIfOnClasspath; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import static io.github.nstdio.http.ext.OptionalDependencies.GSON; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIOException; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; @EnabledIfOnClasspath(GSON) -class GsonJsonMappingTest { - - @Test - void shouldThrowIOExceptionWhenParsingException() { - //given - var bytes = "{".getBytes(StandardCharsets.UTF_8); - var mapping = new GsonJsonMapping(); - - //when - assertThatIOException() - .isThrownBy(() -> mapping.read(bytes, Object.class)) - .havingCause(); - } - - @Test - void shouldCloseInputStream() throws IOException { - //given - var inSpy = spy(new ByteArrayInputStream("{}".getBytes(StandardCharsets.UTF_8))); - var mapping = new GsonJsonMapping(); - - //when - Object read = mapping.read(inSpy, Object.class); - - //then - assertThat(read).isNotNull(); - verify(inSpy).close(); - } - - @Test - void shouldReadFromByteArray() throws IOException { - //given - var bytes = "{}".getBytes(StandardCharsets.UTF_8); - var mapping = new GsonJsonMapping(); - - //when - Object read = mapping.read(bytes, Object.class); - - //then - assertThat(read).isNotNull(); +public class GsonJsonMappingTest implements JsonMappingContract { + @Override + public JsonMapping get() { + return new GsonJsonMapping(); } -} \ No newline at end of file +} diff --git a/src/spiTest/java/io/github/nstdio/http/ext/spi/JacksonJsonMappingTest.java b/src/spiTest/java/io/github/nstdio/http/ext/spi/JacksonJsonMappingTest.java new file mode 100644 index 0000000..23e7e04 --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/spi/JacksonJsonMappingTest.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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.github.nstdio.http.ext.spi; + +import io.github.nstdio.http.ext.jupiter.EnabledIfOnClasspath; + +import static io.github.nstdio.http.ext.OptionalDependencies.JACKSON; + +@EnabledIfOnClasspath(JACKSON) +public class JacksonJsonMappingTest implements JsonMappingContract { + @Override + public JsonMapping get() { + return new JacksonJsonMapping(); + } +} diff --git a/src/spiTest/java/io/github/nstdio/http/ext/spi/JacksonMappingProviderTest.java b/src/spiTest/java/io/github/nstdio/http/ext/spi/JacksonMappingProviderTest.java deleted file mode 100644 index 6f87e95..0000000 --- a/src/spiTest/java/io/github/nstdio/http/ext/spi/JacksonMappingProviderTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2022 Edgar Asatryan - * - * 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.github.nstdio.http.ext.spi; - -import io.github.nstdio.http.ext.jupiter.EnabledIfOnClasspath; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import static io.github.nstdio.http.ext.OptionalDependencies.JACKSON; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - -@EnabledIfOnClasspath(JACKSON) -class JacksonMappingProviderTest { - - @Test - void shouldCloseInputStream() throws IOException { - //given - var inSpy = spy(new ByteArrayInputStream("{}".getBytes(StandardCharsets.UTF_8))); - var mapping = new JacksonJsonMapping(); - - //when - Object read = mapping.read(inSpy, Object.class); - - //then - assertThat(read).isNotNull(); - verify(inSpy, atLeastOnce()).close(); - } - - @Test - void shouldReadFromByteArray() throws IOException { - //given - var bytes = "{}".getBytes(StandardCharsets.UTF_8); - var mapping = new JacksonJsonMapping(); - - //when - Object read = mapping.read(bytes, Object.class); - - //then - assertThat(read).isNotNull(); - } -} diff --git a/src/spiTest/java/io/github/nstdio/http/ext/spi/JsonMappingContract.java b/src/spiTest/java/io/github/nstdio/http/ext/spi/JsonMappingContract.java new file mode 100644 index 0000000..4718301 --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/spi/JsonMappingContract.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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.github.nstdio.http.ext.spi; + +import com.google.common.reflect.TypeToken; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +interface JsonMappingContract { + JsonMapping get(); + + @Test + default void shouldThrowIOExceptionWhenParsingException() { + //given + var bytes = "{".getBytes(StandardCharsets.UTF_8); + var mapping = get(); + + //when + assertThrows(IOException.class, () -> mapping.read(bytes, Object.class)); + } + + @Test + default void shouldThrowIOExceptionWhenParsingExceptionWithComplexType() { + //given + var bytes = new ByteArrayInputStream("{".getBytes(StandardCharsets.UTF_8)); + var mapping = get(); + var targetType = new TypeToken>() {}.getType(); + + //when + assertThrows(IOException.class, () -> mapping.read(bytes, targetType)); + } + + @Test + default void shouldCloseInputStream() throws IOException { + //given + byte[] jsonBytes = "{}".getBytes(StandardCharsets.UTF_8); + var inSpy = spy(new ByteArrayInputStream(jsonBytes)); + var mapping = get(); + + //when + Object read = mapping.read(inSpy, Object.class); + + //then + assertThat(read).isNotNull(); + verify(inSpy, atLeastOnce()).close(); + } + + @Test + default void shouldReadFromByteArray() throws IOException { + //given + var jsonBytes = "{}".getBytes(StandardCharsets.UTF_8); + var mapping = get(); + + //when + Object read = mapping.read(jsonBytes, Object.class); + + //then + assertThat(read).isNotNull(); + } + + @Test + default void shouldReadFromByteArrayUsingComplexType() throws IOException { + //given + var jsonBytes = "{\"a\": 1, \"b\": 2}".getBytes(StandardCharsets.UTF_8); + var mapping = get(); + var targetType = new TypeToken>() { + }.getType(); + + //when + Map read = mapping.read(jsonBytes, targetType); + + //then + assertThat(read) + .hasSize(2) + .containsEntry("a", 1) + .containsEntry("b", 2); + } + + @Test + default void shouldReadFromInputStreamUsingComplexType() throws IOException { + //given + var jsonBytes = new ByteArrayInputStream("{\"a\": 1, \"b\": 2}".getBytes(StandardCharsets.UTF_8)); + var mapping = get(); + var targetType = new TypeToken>() { + }.getType(); + + //when + Map read = mapping.read(jsonBytes, targetType); + + //then + assertThat(read) + .hasSize(2) + .containsEntry("a", 1) + .containsEntry("b", 2); + } +}