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 +