diff --git a/extensions/gson/pom.xml b/extensions/gson/pom.xml new file mode 100644 index 00000000..2e0e09b3 --- /dev/null +++ b/extensions/gson/pom.xml @@ -0,0 +1,47 @@ + + + + + 4.0.0 + + + io.jsonwebtoken + jjwt-root + 0.10.8-SNAPSHOT + ../../pom.xml + + + jjwt-gson + JJWT :: Extensions :: Gson + jar + + + ${basedir}/../.. + + + + + io.jsonwebtoken + jjwt-api + + + com.google.code.gson + gson + + + + \ No newline at end of file diff --git a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonDeserializer.java b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonDeserializer.java new file mode 100644 index 00000000..b3dfc843 --- /dev/null +++ b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonDeserializer.java @@ -0,0 +1,44 @@ +package io.jsonwebtoken.gson.io; + +import com.google.gson.Gson; +import io.jsonwebtoken.io.DeserializationException; +import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.lang.Assert; +import java.io.IOException; + +public class GsonDeserializer implements Deserializer { + + private final Class returnType; + private final Gson gson; + + @SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator + public GsonDeserializer() { + this(GsonSerializer.DEFAULT_GSON); + } + + @SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom gson + public GsonDeserializer(Gson gson) { + this(gson, (Class) Object.class); + } + + private GsonDeserializer(Gson gson, Class returnType) { + Assert.notNull(gson, "gson cannot be null."); + Assert.notNull(returnType, "Return type cannot be null."); + this.gson = gson; + this.returnType = returnType; + } + + @Override + public T deserialize(byte[] bytes) throws DeserializationException { + try { + return readValue(bytes); + } catch (IOException e) { + String msg = "Unable to deserialize bytes into a " + returnType.getName() + " instance: " + e.getMessage(); + throw new DeserializationException(msg, e); + } + } + + protected T readValue(byte[] bytes) throws IOException { + return gson.fromJson(new String(bytes), returnType); + } +} diff --git a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java new file mode 100644 index 00000000..f3050690 --- /dev/null +++ b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java @@ -0,0 +1,50 @@ +package io.jsonwebtoken.gson.io; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.io.SerializationException; +import io.jsonwebtoken.io.Serializer; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +public class GsonSerializer implements Serializer { + + static final Gson DEFAULT_GSON = new GsonBuilder().disableHtmlEscaping().create(); + private Gson gson; + + @SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator + public GsonSerializer() { + this(DEFAULT_GSON); + } + + @SuppressWarnings("WeakerAccess") //intended for end-users to use when providing a custom gson + public GsonSerializer(Gson gson) { + Assert.notNull(gson, "gson cannot be null."); + this.gson = gson; + } + + @Override + public byte[] serialize(T t) throws SerializationException { + Assert.notNull(t, "Object to serialize cannot be null."); + try { + return writeValueAsBytes(t); + } catch (Exception e) { + String msg = "Unable to serialize object: " + e.getMessage(); + throw new SerializationException(msg, e); + } + } + + @SuppressWarnings("WeakerAccess") //for testing + protected byte[] writeValueAsBytes(T t) { + Object o; + if (t instanceof byte[]) { + o = Encoders.BASE64.encode((byte[]) t); + } else if (t instanceof char[]) { + o = new String((char[]) t); + } else { + o = t; + } + return this.gson.toJson(o).getBytes(Strings.UTF_8); + } +} diff --git a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy new file mode 100644 index 00000000..40c5caf8 --- /dev/null +++ b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy @@ -0,0 +1,65 @@ +package io.jsonwebtoken.gson.io + +import com.google.gson.Gson +import io.jsonwebtoken.io.DeserializationException +import io.jsonwebtoken.lang.Strings +import org.junit.Test + +import static org.easymock.EasyMock.* +import static org.junit.Assert.* + +class GsonDeserializerTest { + + @Test + void testDefaultConstructor() { + def deserializer = new GsonDeserializer() + assertNotNull deserializer.gson + } + + @Test + void testObjectMapperConstructor() { + def customGSON = new Gson() + def deserializer = new GsonDeserializer(customGSON) + assertSame customGSON, deserializer.gson + } + + @Test(expected = IllegalArgumentException) + void testObjectMapperConstructorWithNullArgument() { + new GsonDeserializer<>(null) + } + + @Test + void testDeserialize() { + byte[] serialized = '{"hello":"世界"}'.getBytes(Strings.UTF_8) + def expected = [hello: '世界'] + def result = new GsonDeserializer().deserialize(serialized) + assertEquals expected, result + } + + @Test + void testDeserializeFailsWithJsonProcessingException() { + + def ex = createMock(java.io.IOException) + + expect(ex.getMessage()).andReturn('foo') + + def deserializer = new GsonDeserializer() { + @Override + protected Object readValue(byte[] bytes) throws java.io.IOException { + 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/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy new file mode 100644 index 00000000..b65d68a9 --- /dev/null +++ b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy @@ -0,0 +1,100 @@ +package io.jsonwebtoken.gson.io + +import io.jsonwebtoken.lang.Strings +import org.junit.Test + +import static org.easymock.EasyMock.* +import static org.junit.Assert.* +import com.google.gson.Gson +import io.jsonwebtoken.io.SerializationException + +class GsonSerializerTest { + + @Test + void testDefaultConstructor() { + def serializer = new GsonSerializer() + assertNotNull serializer.gson + } + + @Test + void testObjectMapperConstructor() { + def customGSON = new Gson() + def serializer = new GsonSerializer<>(customGSON) + assertSame customGSON, serializer.gson + } + + @Test(expected = IllegalArgumentException) + void testObjectMapperConstructorWithNullArgument() { + new GsonSerializer<>(null) + } + + @Test + void testByte() { + byte[] expected = "120".getBytes(Strings.UTF_8) //ascii("x") = 120 + byte[] bytes = "x".getBytes(Strings.UTF_8) + byte[] result = new GsonSerializer().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 GsonSerializer().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 GsonSerializer().serialize(bytes) + assertEquals '""', new String(result, Strings.UTF_8) + } + + @Test + void testChar() { //expect Base64 string by default: + byte[] result = new GsonSerializer().serialize('h' as char) + assertEquals "\"h\"", new String(result, Strings.UTF_8) + } + + @Test + void testCharArray() { //expect Base64 string by default: + byte[] result = new GsonSerializer().serialize("hi".toCharArray()) + assertEquals "\"hi\"", new String(result, Strings.UTF_8) + } + + @Test + void testSerialize() { + byte[] expected = '{"hello":"世界"}'.getBytes(Strings.UTF_8) + byte[] result = new GsonSerializer().serialize([hello: '世界']) + assertTrue Arrays.equals(expected, result) + } + + + @Test + void testSerializeFailsWithJsonProcessingException() { + + def ex = createMock(SerializationException) + + expect(ex.getMessage()).andReturn('foo') + + def serializer = new GsonSerializer() { + @Override + protected byte[] writeValueAsBytes(Object o) throws SerializationException { + 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 90888ae6..0cf57558 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -36,6 +36,7 @@ jackson orgjson + gson \ No newline at end of file diff --git a/impl/pom.xml b/impl/pom.xml index 951b1f07..2fb4bbe4 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -54,6 +54,11 @@ jjwt-orgjson test + + io.jsonwebtoken + jjwt-gson + test + \ No newline at end of file diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/RuntimeClasspathDeserializerLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/io/RuntimeClasspathDeserializerLocator.java index 034fe34e..022bb289 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/RuntimeClasspathDeserializerLocator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/RuntimeClasspathDeserializerLocator.java @@ -34,6 +34,8 @@ public class RuntimeClasspathDeserializerLocator implements InstanceLocator2.9.9.1 20180130 + 2.8.5 1.60 @@ -131,6 +132,11 @@ jjwt-orgjson ${project.version} + + io.jsonwebtoken + jjwt-gson + ${project.version} + com.fasterxml.jackson.core jackson-databind @@ -141,6 +147,11 @@ json ${orgjson.version} + + com.google.code.gson + gson + ${gson.version} +