From 5fa364af72fe64c6f9f52f5a1d8c85c82122bf29 Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Fri, 15 Apr 2022 21:33:53 +0200 Subject: [PATCH 1/4] [#719] JSON-B extension. --- extensions/jsonb/bnd.bnd | 1 + extensions/jsonb/pom.xml | 47 ++++++++ .../jsonb/io/JsonbDeserializer.java | 66 +++++++++++ .../jsonb/io/JsonbSerializer.java | 75 ++++++++++++ .../services/io.jsonwebtoken.io.Deserializer | 1 + .../services/io.jsonwebtoken.io.Serializer | 1 + .../jsonb/io/JsonbDeserializerTest.groovy | 76 ++++++++++++ .../jsonb/io/JsonbSerializerTest.groovy | 111 ++++++++++++++++++ extensions/pom.xml | 3 +- pom.xml | 21 ++++ 10 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 extensions/jsonb/bnd.bnd create mode 100644 extensions/jsonb/pom.xml create mode 100644 extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbDeserializer.java create mode 100644 extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbSerializer.java create mode 100644 extensions/jsonb/src/main/resources/META-INF/services/io.jsonwebtoken.io.Deserializer create mode 100644 extensions/jsonb/src/main/resources/META-INF/services/io.jsonwebtoken.io.Serializer create mode 100644 extensions/jsonb/src/test/groovy/io/jsonwebtoken/jsonb/io/JsonbDeserializerTest.groovy create mode 100644 extensions/jsonb/src/test/groovy/io/jsonwebtoken/jsonb/io/JsonbSerializerTest.groovy diff --git a/extensions/jsonb/bnd.bnd b/extensions/jsonb/bnd.bnd new file mode 100644 index 000000000..fd7afa7ab --- /dev/null +++ b/extensions/jsonb/bnd.bnd @@ -0,0 +1 @@ +Fragment-Host: io.jsonwebtoken.jjwt-api diff --git a/extensions/jsonb/pom.xml b/extensions/jsonb/pom.xml new file mode 100644 index 000000000..ee2f1e176 --- /dev/null +++ b/extensions/jsonb/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + + io.jsonwebtoken + jjwt-root + 0.11.3-SNAPSHOT + ../../pom.xml + + + jjwt-jsonb + JJWT :: Extensions :: JSON-B + jar + + + ${basedir}/../.. + + 8 + + + + + io.jsonwebtoken + jjwt-api + + + jakarta.json + jakarta.json-api + test + + + jakarta.json.bind + jakarta.json.bind-api + provided + + + + + org.apache.johnzon + johnzon-jsonb + test + + + diff --git a/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbDeserializer.java b/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbDeserializer.java new file mode 100644 index 000000000..53bfabb70 --- /dev/null +++ b/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbDeserializer.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2014 jsonwebtoken.io + * + * 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.jsonwebtoken.jsonb.io; + +import io.jsonwebtoken.io.DeserializationException; +import io.jsonwebtoken.io.Deserializer; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbException; +import java.nio.charset.StandardCharsets; + +import static java.util.Objects.requireNonNull; + +/** + * @since 0.10.0 + */ +public class JsonbDeserializer implements Deserializer { + + private final Class returnType; + private final Jsonb jsonb; + + @SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator + public JsonbDeserializer() { + this(JsonbSerializer.DEFAULT_JSONB); + } + + @SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom ObjectMapper + public JsonbDeserializer(Jsonb jsonb) { + this(jsonb, (Class) Object.class); + } + + private JsonbDeserializer(Jsonb jsonb, Class returnType) { + requireNonNull(jsonb, "ObjectMapper cannot be null."); + requireNonNull(returnType, "Return type cannot be null."); + this.jsonb = jsonb; + this.returnType = returnType; + } + + @Override + public T deserialize(byte[] bytes) throws DeserializationException { + try { + return readValue(bytes); + } catch (JsonbException jsonbException) { + String msg = "Unable to deserialize bytes into a " + returnType.getName() + " instance: " + jsonbException.getMessage(); + throw new DeserializationException(msg, jsonbException); + } + } + + protected T readValue(byte[] bytes) { + return jsonb.fromJson(new String(bytes, StandardCharsets.UTF_8), returnType); + } + +} diff --git a/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbSerializer.java b/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbSerializer.java new file mode 100644 index 000000000..25b949ada --- /dev/null +++ b/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbSerializer.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2014 jsonwebtoken.io + * + * 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.jsonwebtoken.jsonb.io; + +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.io.SerializationException; +import io.jsonwebtoken.io.Serializer; +import io.jsonwebtoken.lang.Assert; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.json.bind.JsonbException; +import java.nio.charset.StandardCharsets; + +import static java.util.Objects.requireNonNull; + +/** + * @since 0.10.0 + */ +public class JsonbSerializer implements Serializer { + + static final Jsonb DEFAULT_JSONB = JsonbBuilder.create(); + + private final Jsonb jsonb; + + @SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator + public JsonbSerializer() { + this(DEFAULT_JSONB); + } + + @SuppressWarnings("WeakerAccess") //intended for end-users to use when providing a custom ObjectMapper + public JsonbSerializer(Jsonb jsonb) { + requireNonNull(jsonb, "Jsonb cannot be null."); + this.jsonb = jsonb; + } + + @Override + public byte[] serialize(T t) throws SerializationException { + Assert.notNull(t, "Object to serialize cannot be null."); + try { + return writeValueAsBytes(t); + } catch (JsonbException jsonbException) { + String msg = "Unable to serialize object: " + jsonbException.getMessage(); + throw new SerializationException(msg, jsonbException); + } + } + + @SuppressWarnings("WeakerAccess") //for testing + protected byte[] writeValueAsBytes(T t) { + final Object obj; + + if (t instanceof byte[]) { + obj = Encoders.BASE64.encode((byte[]) t); + } else if (t instanceof char[]) { + obj = new String((char[]) t); + } else { + obj = t; + } + + return this.jsonb.toJson(obj).getBytes(StandardCharsets.UTF_8); + } +} diff --git a/extensions/jsonb/src/main/resources/META-INF/services/io.jsonwebtoken.io.Deserializer b/extensions/jsonb/src/main/resources/META-INF/services/io.jsonwebtoken.io.Deserializer new file mode 100644 index 000000000..40ea24d55 --- /dev/null +++ b/extensions/jsonb/src/main/resources/META-INF/services/io.jsonwebtoken.io.Deserializer @@ -0,0 +1 @@ +io.jsonwebtoken.jsonb.io.JsonbDeserializer diff --git a/extensions/jsonb/src/main/resources/META-INF/services/io.jsonwebtoken.io.Serializer b/extensions/jsonb/src/main/resources/META-INF/services/io.jsonwebtoken.io.Serializer new file mode 100644 index 000000000..100cf3444 --- /dev/null +++ b/extensions/jsonb/src/main/resources/META-INF/services/io.jsonwebtoken.io.Serializer @@ -0,0 +1 @@ +io.jsonwebtoken.jsonb.io.JsonbSerializer diff --git a/extensions/jsonb/src/test/groovy/io/jsonwebtoken/jsonb/io/JsonbDeserializerTest.groovy b/extensions/jsonb/src/test/groovy/io/jsonwebtoken/jsonb/io/JsonbDeserializerTest.groovy new file mode 100644 index 000000000..88386e8f7 --- /dev/null +++ b/extensions/jsonb/src/test/groovy/io/jsonwebtoken/jsonb/io/JsonbDeserializerTest.groovy @@ -0,0 +1,76 @@ +package io.jsonwebtoken.jsonb.io + +import io.jsonwebtoken.io.DeserializationException +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.lang.Strings +import org.junit.Test + +import javax.json.bind.JsonbBuilder + +import static org.easymock.EasyMock.* +import static org.hamcrest.CoreMatchers.instanceOf +import static org.hamcrest.MatcherAssert.assertThat +import static org.junit.Assert.* + +class JsonbDeserializerTest { + + @Test + void loadService() { + def deserializer = ServiceLoader.load(Deserializer).iterator().next() + assertThat(deserializer, instanceOf(JsonbDeserializer)) + } + + + @Test + void testDefaultConstructor() { + def deserializer = new JsonbDeserializer() + assertNotNull deserializer.jsonb + } + + @Test + void testObjectMapperConstructor() { + def customJsonb = JsonbBuilder.create() + def deserializer = new JsonbDeserializer(customJsonb) + assertSame customJsonb, deserializer.jsonb + } + + @Test(expected = NullPointerException) + void testObjectMapperConstructorWithNullArgument() { + new JsonbDeserializer<>(null) + } + + @Test + void testDeserialize() { + byte[] serialized = '{"hello":"世界"}'.getBytes(Strings.UTF_8) + def expected = [hello: '世界'] + def result = new JsonbDeserializer().deserialize(serialized) + assertEquals expected, result + } + + @Test + void testDeserializeFailsWithJsonProcessingException() { + + def ex = createMock javax.json.bind.JsonbException + + expect(ex.getMessage()).andReturn('foo') + + def deserializer = new JsonbDeserializer() { + @Override + protected Object readValue(byte[] bytes) throws javax.json.bind.JsonbException { + throw ex + } + } + + replay ex + + try { + deserializer.deserialize('{"hello":"世界"}'.getBytes(Strings.UTF_8)) + fail() + } catch (DeserializationException se) { + assertEquals 'Unable to deserialize bytes into a java.lang.Object instance: foo', se.getMessage() + assertSame ex, se.getCause() + } + + verify ex + } +} diff --git a/extensions/jsonb/src/test/groovy/io/jsonwebtoken/jsonb/io/JsonbSerializerTest.groovy b/extensions/jsonb/src/test/groovy/io/jsonwebtoken/jsonb/io/JsonbSerializerTest.groovy new file mode 100644 index 000000000..da8d3c096 --- /dev/null +++ b/extensions/jsonb/src/test/groovy/io/jsonwebtoken/jsonb/io/JsonbSerializerTest.groovy @@ -0,0 +1,111 @@ +package io.jsonwebtoken.jsonb.io + +import io.jsonwebtoken.io.SerializationException +import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.lang.Strings +import org.junit.Test + +import javax.json.bind.JsonbBuilder +import javax.json.bind.JsonbException + +import static org.easymock.EasyMock.* +import static org.hamcrest.CoreMatchers.instanceOf +import static org.hamcrest.MatcherAssert.assertThat +import static org.junit.Assert.* + +class JsonbSerializerTest { + + @Test + void loadService() { + def serializer = ServiceLoader.load(Serializer).iterator().next() + assertThat(serializer, instanceOf(JsonbSerializer)) + } + + @Test + void testDefaultConstructor() { + def serializer = new JsonbSerializer() + assertNotNull serializer.jsonb + } + + @Test + void testObjectMapperConstructor() { + def customJsonb = JsonbBuilder.create() + def serializer = new JsonbSerializer<>(customJsonb) + assertSame customJsonb, serializer.jsonb + } + + @Test(expected = NullPointerException) + void testObjectMapperConstructorWithNullArgument() { + new JsonbSerializer<>(null) + } + + @Test + void testByte() { + byte[] expected = "120".getBytes(Strings.UTF_8) //ascii("x") = 120 + byte[] bytes = "x".getBytes(Strings.UTF_8) + byte[] result = new JsonbSerializer().serialize(bytes[0]) //single byte + assertTrue Arrays.equals(expected, result) + } + + @Test + void testByteArray() { //expect Base64 string by default: + byte[] bytes = "hi".getBytes(Strings.UTF_8) + String expected = '"aGk="' as String //base64(hi) --> aGk= + byte[] result = new JsonbSerializer().serialize(bytes) + assertEquals expected, new String(result, Strings.UTF_8) + } + + @Test + void testEmptyByteArray() { //expect Base64 string by default: + byte[] bytes = new byte[0] + byte[] result = new JsonbSerializer().serialize(bytes) + assertEquals '""', new String(result, Strings.UTF_8) + } + + @Test + void testChar() { //expect Base64 string by default: + byte[] result = new JsonbSerializer().serialize('h' as char) + assertEquals "\"h\"", new String(result, Strings.UTF_8) + } + + @Test + void testCharArray() { //expect Base64 string by default: + byte[] result = new JsonbSerializer().serialize("hi".toCharArray()) + assertEquals "\"hi\"", new String(result, Strings.UTF_8) + } + + @Test + void testSerialize() { + byte[] expected = '{"hello":"世界"}'.getBytes(Strings.UTF_8) + byte[] result = new JsonbSerializer().serialize([hello: '世界']) + assertTrue Arrays.equals(expected, result) + } + + + @Test + void testSerializeFailsWithJsonProcessingException() { + + def ex = createMock(JsonbException) + + expect(ex.getMessage()).andReturn('foo') + + def serializer = new JsonbSerializer() { + @Override + protected byte[] writeValueAsBytes(Object o) throws JsonbException { + throw ex + } + } + + replay ex + + try { + serializer.serialize([hello: 'world']) + fail() + } catch (SerializationException se) { + assertEquals 'Unable to serialize object: foo', se.getMessage() + assertSame ex, se.getCause() + } + + verify ex + } +} diff --git a/extensions/pom.xml b/extensions/pom.xml index ee13b0f20..52cc61316 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -37,5 +37,6 @@ jackson orgjson gson + jsonb - \ No newline at end of file + diff --git a/pom.xml b/pom.xml index fe2bea294..89b961166 100644 --- a/pom.xml +++ b/pom.xml @@ -77,6 +77,9 @@ 2.12.6 20180130 2.8.9 + 1.1.6 + 1.0.2 + 1.2.16 1.67 @@ -138,6 +141,24 @@ gson ${gson.version} + + jakarta.json + jakarta.json-api + ${jsonp.version} + test + + + jakarta.json.bind + jakarta.json.bind-api + ${jsonb.version} + provided + + + org.apache.johnzon + johnzon-jsonb + ${johnzon.version} + test + From d20a74daef009a06cac686be0ee3643b7307ac1f Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Fri, 15 Apr 2022 22:33:44 +0200 Subject: [PATCH 2/4] [#719] README.md updates. --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index adf85e070..78531402e 100644 --- a/README.md +++ b/README.md @@ -1269,11 +1269,27 @@ They are checked in order, and the first one found is used: Android applications _unless_ you want to use POJOs as claims. The `org.json` library supports simple Object-to-JSON marshaling, but it *does not* support JSON-to-Object unmarshalling. +4. JSON-B: This will automatically be used if you specify `io.jsonwebtoken:jjwt-jsonb` as a project runtime dependency. + JSON-B also supports POJOs as claims with full marshaling/unmarshaling as necessary. + + **NOTE**: `JSON-B` is just a specification and does not bring an implementation. + + * In Java-SE environments you will need to add three more dependencies: + 1. `jakarta.json:jakarta.json-api` + 2. `jakarta.json.bind:jakarta.json.bind-api` + 3. A JSON-B compliant implementation like [Eclipse Yasson](https://github.com/eclipse-ee4j/yasson) or [Apache Johnzon](https://johnzon.apache.org/johnzon-jsonb/index.html). + * In Java/Jakarta EE environments, you might need to enable the following features: + 1. json-p / json-api + 2. json-b / json-bind + **If you want to use POJOs as claim values, use either the `io.jsonwebtoken:jjwt-jackson` or `io.jsonwebtoken:jjwt-gson` dependency** (or implement your own Serializer and Deserializer if desired). **But beware**, Jackson will force a sizable (> 1 MB) dependency to an Android application thus increasing the app download size for mobile users. +If you want to use POJOs and a JSON-B compliant specification _**or**_ you want to use JJWT on a JakartaEE compliant application server, use +`io.jsonwebtoken:jjwt-jsonb`. + ### Custom JSON Processor From 35847dfe373872b2f3ca215c59fb766bcdef054b Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Tue, 19 Apr 2022 20:33:13 +0200 Subject: [PATCH 3/4] [#719] profile, assert=>requireNonNull, @since --- README.md | 3 +-- .../io/jsonwebtoken/jsonb/io/JsonbDeserializer.java | 2 +- .../io/jsonwebtoken/jsonb/io/JsonbSerializer.java | 4 ++-- extensions/pom.xml | 13 ++++++++++++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 78531402e..a332f0b45 100644 --- a/README.md +++ b/README.md @@ -1287,8 +1287,7 @@ They are checked in order, and the first one found is used: Jackson will force a sizable (> 1 MB) dependency to an Android application thus increasing the app download size for mobile users. -If you want to use POJOs and a JSON-B compliant specification _**or**_ you want to use JJWT on a JakartaEE compliant application server, use -`io.jsonwebtoken:jjwt-jsonb`. +If you want to use POJOs and a JSON-B compliant specification _**or**_ you want to use JJWT on a JakartaEE compliant application server, use `io.jsonwebtoken:jjwt-jsonb`. ### Custom JSON Processor diff --git a/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbDeserializer.java b/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbDeserializer.java index 53bfabb70..6d75b572c 100644 --- a/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbDeserializer.java +++ b/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbDeserializer.java @@ -25,7 +25,7 @@ import static java.util.Objects.requireNonNull; /** - * @since 0.10.0 + * @since JJWT_RELEASE_VERSION */ public class JsonbDeserializer implements Deserializer { diff --git a/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbSerializer.java b/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbSerializer.java index 25b949ada..0c4cd8d0e 100644 --- a/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbSerializer.java +++ b/extensions/jsonb/src/main/java/io/jsonwebtoken/jsonb/io/JsonbSerializer.java @@ -28,7 +28,7 @@ import static java.util.Objects.requireNonNull; /** - * @since 0.10.0 + * @since JJWT_RELEASE_VERSION */ public class JsonbSerializer implements Serializer { @@ -49,7 +49,7 @@ public JsonbSerializer(Jsonb jsonb) { @Override public byte[] serialize(T t) throws SerializationException { - Assert.notNull(t, "Object to serialize cannot be null."); + requireNonNull(t, "Object to serialize cannot be null."); try { return writeValueAsBytes(t); } catch (JsonbException jsonbException) { diff --git a/extensions/pom.xml b/extensions/pom.xml index 52cc61316..031139012 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -37,6 +37,17 @@ jackson orgjson gson - jsonb + + + + java8 + + [8,) + + + jsonb + + + From cb1f42e67a1e948745b56bd28ca17652f6dbef88 Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Tue, 19 Apr 2022 22:14:01 +0200 Subject: [PATCH 4/4] [#719] realign jdk8 profile. --- extensions/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/pom.xml b/extensions/pom.xml index 031139012..e159cdb62 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -41,9 +41,9 @@ - java8 + nonJDK7 - [8,) + [1.8,) jsonb