Merge pull request #414 from patton73/master

Added Gson serialization/deserialization Extension
This commit is contained in:
Les Hazlewood 2019-07-17 22:13:58 -04:00 committed by GitHub
commit ff8a6bfe58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 367 additions and 0 deletions

47
extensions/gson/pom.xml Normal file
View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2018 JWTK
~
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-root</artifactId>
<version>0.10.8-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>jjwt-gson</artifactId>
<name>JJWT :: Extensions :: Gson</name>
<packaging>jar</packaging>
<properties>
<jjwt.root>${basedir}/../..</jjwt.root>
</properties>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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<T> implements Deserializer<T> {
private final Class<T> 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<T>) Object.class);
}
private GsonDeserializer(Gson gson, Class<T> 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);
}
}

View File

@ -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<T> implements Serializer<T> {
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);
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -36,6 +36,7 @@
<modules> <modules>
<module>jackson</module> <module>jackson</module>
<module>orgjson</module> <module>orgjson</module>
<module>gson</module>
</modules> </modules>
</project> </project>

View File

@ -54,6 +54,11 @@
<artifactId>jjwt-orgjson</artifactId> <artifactId>jjwt-orgjson</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-gson</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -34,6 +34,8 @@ public class RuntimeClasspathDeserializerLocator<T> implements InstanceLocator<D
return Classes.newInstance("io.jsonwebtoken.io.JacksonDeserializer"); return Classes.newInstance("io.jsonwebtoken.io.JacksonDeserializer");
} else if (isAvailable("io.jsonwebtoken.io.OrgJsonDeserializer")) { } else if (isAvailable("io.jsonwebtoken.io.OrgJsonDeserializer")) {
return Classes.newInstance("io.jsonwebtoken.io.OrgJsonDeserializer"); return Classes.newInstance("io.jsonwebtoken.io.OrgJsonDeserializer");
} else if (isAvailable("io.jsonwebtoken.gson.io.GsonDeserializer")) {
return Classes.newInstance("io.jsonwebtoken.gson.io.GsonDeserializer");
} else { } else {
throw new IllegalStateException("Unable to discover any JSON Deserializer implementations on the classpath."); throw new IllegalStateException("Unable to discover any JSON Deserializer implementations on the classpath.");
} }

View File

@ -34,6 +34,8 @@ public class RuntimeClasspathSerializerLocator implements InstanceLocator<Serial
return Classes.newInstance("io.jsonwebtoken.io.JacksonSerializer"); return Classes.newInstance("io.jsonwebtoken.io.JacksonSerializer");
} else if (isAvailable("io.jsonwebtoken.io.OrgJsonSerializer")) { } else if (isAvailable("io.jsonwebtoken.io.OrgJsonSerializer")) {
return Classes.newInstance("io.jsonwebtoken.io.OrgJsonSerializer"); return Classes.newInstance("io.jsonwebtoken.io.OrgJsonSerializer");
} else if (isAvailable("io.jsonwebtoken.gson.io.GsonSerializer")) {
return Classes.newInstance("io.jsonwebtoken.gson.io.GsonSerializer");
} else { } else {
throw new IllegalStateException("Unable to discover any JSON Serializer implementations on the classpath."); throw new IllegalStateException("Unable to discover any JSON Serializer implementations on the classpath.");
} }

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.io.JacksonDeserializer import io.jsonwebtoken.io.JacksonDeserializer
import io.jsonwebtoken.io.OrgJsonDeserializer import io.jsonwebtoken.io.OrgJsonDeserializer
import io.jsonwebtoken.gson.io.GsonDeserializer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -96,4 +97,23 @@ class RuntimeClasspathDeserializerLocatorTest {
def deserializer = locator.getInstance() def deserializer = locator.getInstance()
assertTrue deserializer instanceof OrgJsonDeserializer assertTrue deserializer instanceof OrgJsonDeserializer
} }
@Test
void testGson() {
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
if (JacksonDeserializer.class.getName().equals(fqcn)) {
return false; //skip it to allow the Gson impl to be created
}
if (OrgJsonDeserializer.class.getName().equals(fqcn)) {
return false; //skip it to allow the Gson impl to be created
}
return super.isAvailable(fqcn)
}
}
def deserializer = locator.getInstance()
assertTrue deserializer instanceof GsonDeserializer
}
} }

View File

@ -3,6 +3,7 @@ package io.jsonwebtoken.impl.io
import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.io.JacksonSerializer import io.jsonwebtoken.io.JacksonSerializer
import io.jsonwebtoken.io.OrgJsonSerializer import io.jsonwebtoken.io.OrgJsonSerializer
import io.jsonwebtoken.gson.io.GsonSerializer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -95,4 +96,23 @@ class RuntimeClasspathSerializerLocatorTest {
def serializer = locator.getInstance() def serializer = locator.getInstance()
assertTrue serializer instanceof OrgJsonSerializer assertTrue serializer instanceof OrgJsonSerializer
} }
@Test
void testGson() {
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
if (JacksonSerializer.class.getName().equals(fqcn)) {
return false //skip it to allow the Gson impl to be created
}
if (OrgJsonSerializer.class.getName().equals(fqcn)) {
return false //skip it to allow the Gson impl to be created
}
return super.isAvailable(fqcn)
}
}
def serializer = locator.getInstance()
assertTrue serializer instanceof GsonSerializer
}
} }

11
pom.xml
View File

@ -90,6 +90,7 @@
<jackson.version>2.9.9.1</jackson.version> <jackson.version>2.9.9.1</jackson.version>
<orgjson.version>20180130</orgjson.version> <orgjson.version>20180130</orgjson.version>
<gson.version>2.8.5</gson.version>
<!-- Optional Runtime Dependencies: --> <!-- Optional Runtime Dependencies: -->
<bouncycastle.version>1.60</bouncycastle.version> <bouncycastle.version>1.60</bouncycastle.version>
@ -131,6 +132,11 @@
<artifactId>jjwt-orgjson</artifactId> <artifactId>jjwt-orgjson</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-gson</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
@ -141,6 +147,11 @@
<artifactId>json</artifactId> <artifactId>json</artifactId>
<version>${orgjson.version}</version> <version>${orgjson.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<!-- Used only during testing for PS256, PS384 and PS512 since JDK <= 10 doesn't support them: --> <!-- Used only during testing for PS256, PS384 and PS512 since JDK <= 10 doesn't support them: -->
<dependency> <dependency>