From 7090bf39c3e457f274967af50105a880a42af26b Mon Sep 17 00:00:00 2001 From: Brian Demers Date: Mon, 30 Sep 2019 17:24:57 -0400 Subject: [PATCH] Add support for custom type deserialization with Jackson (#495) - Adds new constructor JacksonDeserializer(Map claimTypeMap), which will enable later calls Claims.get("key", CustomType.class) to work as expectd - Adds new Maps utility class to make map creation fluent Fixes: #369 --- CHANGELOG.md | 4 + api/src/main/java/io/jsonwebtoken/Claims.java | 17 ++ .../main/java/io/jsonwebtoken/lang/Maps.java | 87 +++++++++++ .../io/jsonwebtoken/lang/MapsTest.groovy | 38 +++++ .../jackson/io/JacksonDeserializer.java | 68 ++++++++ .../jackson/io/JacksonDeserializerTest.groovy | 66 +++++++- .../jackson/io/JacksonSerializerTest.groovy | 1 - .../jackson/io/stubs/CustomBean.groovy | 146 ++++++++++++++++++ .../io/jsonwebtoken/impl/DefaultClaims.java | 10 +- .../CustomObjectDeserializationTest.groovy | 93 +++++++++++ .../impl/DefaultClaimsTest.groovy | 10 +- 11 files changed, 531 insertions(+), 9 deletions(-) create mode 100644 api/src/main/java/io/jsonwebtoken/lang/Maps.java create mode 100644 api/src/test/groovy/io/jsonwebtoken/lang/MapsTest.groovy create mode 100644 extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/stubs/CustomBean.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy diff --git a/CHANGELOG.md b/CHANGELOG.md index c2cbaeb0..57125212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,12 @@ ### 0.11.0 +This minor release: + * Updates the Jackson dependency version to [2.9.10](https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.9#patches) to address three security vulnerabilities in Jackson. +* Adds support for custom types when deserializing with Jackson. To use configure your parser with `Jwts.parserBuilder().deserializeJsonWith(new JacksonDeserializer(Maps.of("claimName", YourType.class).build())).build()`. +* Adds `io.jsonwebtoken.lang.Maps` utility class to make creation of maps fluent. * Moves JSON Serializer/Deserializer implementations to a different package name. - `io.jsonwebtoken.io.JacksonSerializer` -> `io.jsonwebtoken.jackson.io.JacksonSerializer` - `io.jsonwebtoken.io.JacksonDeserializer` -> `io.jsonwebtoken.jackson.io.JacksonDeserializer` diff --git a/api/src/main/java/io/jsonwebtoken/Claims.java b/api/src/main/java/io/jsonwebtoken/Claims.java index 67fb4486..c5717dfa 100644 --- a/api/src/main/java/io/jsonwebtoken/Claims.java +++ b/api/src/main/java/io/jsonwebtoken/Claims.java @@ -170,5 +170,22 @@ public interface Claims extends Map, ClaimsMutator { @Override //only for better/targeted JavaDoc Claims setId(String jti); + /** + * Returns the JWTs claim ({@code claimName}) value as a type {@code requiredType}, or {@code null} if not present. + * + *

JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. Anything more + * complex is expected to be already converted to your desired type by the JSON + * {@link io.jsonwebtoken.io.Deserializer Deserializer} implementation. You may specify a custom Deserializer for a + * JwtParser with the desired conversion configuration via the {@link JwtParserBuilder#deserializeJsonWith} method. + * See custom JSON processor for more + * information. If using Jackson, you can specify custom claim POJO types as described in + * custom claim types. + * + * @param claimName name of claim + * @param requiredType the type of the value expected to be returned + * @param the type of the value expected to be returned + * @return the JWT {@code claimName} value or {@code null} if not present. + * @throws RequiredTypeException throw if the claim value is not null and not of type {@code requiredType} + */ T get(String claimName, Class requiredType); } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Maps.java b/api/src/main/java/io/jsonwebtoken/lang/Maps.java new file mode 100644 index 00000000..96460e0d --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/lang/Maps.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2019 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.lang; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Utility class to help with the manipulation of working with Maps. + * @since 0.11.0 + */ +public final class Maps { + + private Maps() {} //prevent instantiation + + /** + * Creates a new map builder with a single entry. + *

Typical usage:

{@code
+     * Map result = Maps.of("key1", value1)
+     *     .and("key2", value2)
+     *     // ...
+     *     .build();
+     * }
+ * @param key the key of an map entry to be added + * @param value the value of map entry to be added + * @param the maps key type + * @param the maps value type + * Creates a new map builder with a single entry. + */ + public static MapBuilder of(K key, V value) { + return new HashMapBuilder().and(key, value); + } + + /** + * Utility Builder class for fluently building maps: + *

Typical usage:

{@code
+     * Map result = Maps.of("key1", value1)
+     *     .and("key2", value2)
+     *     // ...
+     *     .build();
+     * }
+ * @param the maps key type + * @param the maps value type + */ + public interface MapBuilder { + /** + * Add a new entry to this map builder + * @param key the key of an map entry to be added + * @param value the value of map entry to be added + * @return the current MapBuilder to allow for method chaining. + */ + MapBuilder and(K key, V value); + + /** + * Returns a the resulting Map object from this MapBuilder. + * @return Returns a the resulting Map object from this MapBuilder. + */ + Map build(); + } + + private static class HashMapBuilder implements MapBuilder { + + private final Map data = new HashMap<>(); + + public MapBuilder and(K key, V value) { + data.put(key, value); + return this; + } + public Map build() { + return Collections.unmodifiableMap(data); + } + } +} diff --git a/api/src/test/groovy/io/jsonwebtoken/lang/MapsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/lang/MapsTest.groovy new file mode 100644 index 00000000..0a52fa8b --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/lang/MapsTest.groovy @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019 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.lang + +import org.junit.Test + +import static org.hamcrest.MatcherAssert.assertThat +import static org.hamcrest.CoreMatchers.is + +class MapsTest { + + @Test + void testSingleMapOf() { + assertThat Maps.of("aKey", "aValue").build(), is([aKey: "aValue"]) + } + + @Test + void testMapOfAnd() { + assertThat Maps.of("aKey1", "aValue1") + .and("aKey2", "aValue2") + .and("aKey3", "aValue3") + .build(), + is([aKey1: "aValue1", aKey2: "aValue2", aKey3: "aValue3"]) + } +} diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java index 9792e083..137268c9 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java @@ -15,12 +15,19 @@ */ package io.jsonwebtoken.jackson.io; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.ObjectMapper; + +import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; import io.jsonwebtoken.io.DeserializationException; import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.lang.Assert; import java.io.IOException; +import java.util.Collections; +import java.util.Map; /** * @since 0.10.0 @@ -35,6 +42,42 @@ public class JacksonDeserializer implements Deserializer { this(JacksonSerializer.DEFAULT_OBJECT_MAPPER); } + /** + * Creates a new JacksonDeserializer where the values of the claims can be parsed into given types. A common usage + * example is to parse custom User object out of a claim, for example the claims: + *
{@code
+     * {
+     *     "issuer": "https://issuer.example.com",
+     *     "user": {
+     *         "firstName": "Jill",
+     *         "lastName": "Coder"
+     *     }
+     * }}
+ * Passing a map of {@code ["user": User.class]} to this constructor would result in the {@code user} claim being + * transformed to an instance of your custom {@code User} class, instead of the default of {@code Map}. + *

+ * Because custom type parsing requires modifying the state of a Jackson {@code ObjectMapper}, this + * constructor creates a new internal {@code ObjectMapper} instance and customizes it to support the + * specified {@code claimTypeMap}. This ensures that the JJWT parsing behavior does not unexpectedly + * modify the state of another application-specific {@code ObjectMapper}. + *

+ * If you would like to use your own {@code ObjectMapper} instance that also supports custom types for + * JWT {@code Claims}, you will need to first customize your {@code ObjectMapper} instance by registering + * your custom types and then use the {@link JacksonDeserializer(ObjectMapper)} constructor instead. + * + * @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type + */ + public JacksonDeserializer(Map claimTypeMap) { + // DO NOT reuse JacksonSerializer.DEFAULT_OBJECT_MAPPER as this could result in sharing the custom deserializer + // between instances + this(new ObjectMapper()); + Assert.notNull(claimTypeMap, "Claim type map cannot be null."); + // register a new Deserializer + SimpleModule module = new SimpleModule(); + module.addDeserializer(Object.class, new MappedTypeDeserializer(Collections.unmodifiableMap(claimTypeMap))); + objectMapper.registerModule(module); + } + @SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom ObjectMapper public JacksonDeserializer(ObjectMapper objectMapper) { this(objectMapper, (Class) Object.class); @@ -60,4 +103,29 @@ public class JacksonDeserializer implements Deserializer { protected T readValue(byte[] bytes) throws IOException { return objectMapper.readValue(bytes, returnType); } + + /** + * A Jackson {@link com.fasterxml.jackson.databind.JsonDeserializer JsonDeserializer}, that will convert claim + * values to types based on {@code claimTypeMap}. + */ + private static class MappedTypeDeserializer extends UntypedObjectDeserializer { + + private final Map claimTypeMap; + + private MappedTypeDeserializer(Map claimTypeMap) { + super(null, null); + this.claimTypeMap = claimTypeMap; + } + + @Override + public Object deserialize(JsonParser parser, DeserializationContext context) throws IOException { + // check if the current claim key is mapped, if so traverse it's value + if (claimTypeMap != null && claimTypeMap.containsKey(parser.currentName())) { + Class type = claimTypeMap.get(parser.currentName()); + return parser.readValueAsTree().traverse(parser.getCodec()).readValueAs(type); + } + // otherwise default to super + return super.deserialize(parser, context); + } + } } diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy index b13a195c..ca77e741 100644 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy @@ -17,7 +17,9 @@ package io.jsonwebtoken.jackson.io import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.io.DeserializationException -import io.jsonwebtoken.jackson.io.JacksonDeserializer +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.jackson.io.stubs.CustomBean +import io.jsonwebtoken.lang.Maps import io.jsonwebtoken.lang.Strings import org.junit.Test @@ -41,7 +43,7 @@ class JacksonDeserializerTest { @Test(expected = IllegalArgumentException) void testObjectMapperConstructorWithNullArgument() { - new JacksonDeserializer<>(null) + new JacksonDeserializer<>((ObjectMapper) null) } @Test @@ -52,6 +54,62 @@ class JacksonDeserializerTest { assertEquals expected, result } + @Test + void testDeserializeWithCustomObject() { + + long currentTime = System.currentTimeMillis() + + byte[] serialized = """{ + "oneKey":"oneValue", + "custom": { + "stringValue": "s-value", + "intValue": "11", + "dateValue": ${currentTime}, + "shortValue": 22, + "longValue": 33, + "byteValue": 15, + "byteArrayValue": "${base64('bytes')}", + "nestedValue": { + "stringValue": "nested-value", + "intValue": "111", + "dateValue": ${currentTime + 1}, + "shortValue": 222, + "longValue": 333, + "byteValue": 10, + "byteArrayValue": "${base64('bytes2')}" + } + } + } + """.getBytes(Strings.UTF_8) + + CustomBean expectedCustomBean = new CustomBean() + .setByteArrayValue("bytes".getBytes("UTF-8")) + .setByteValue(0xF as byte) + .setDateValue(new Date(currentTime)) + .setIntValue(11) + .setShortValue(22 as short) + .setLongValue(33L) + .setStringValue("s-value") + .setNestedValue(new CustomBean() + .setByteArrayValue("bytes2".getBytes("UTF-8")) + .setByteValue(0xA as byte) + .setDateValue(new Date(currentTime+1)) + .setIntValue(111) + .setShortValue(222 as short) + .setLongValue(333L) + .setStringValue("nested-value") + ) + + def expected = [oneKey: "oneValue", custom: expectedCustomBean] + def result = new JacksonDeserializer(Maps.of("custom", CustomBean).build()).deserialize(serialized) + assertEquals expected, result + } + + @Test(expected = IllegalArgumentException) + void testNullClaimTypeMap() { + new JacksonDeserializer((Map) null) + } + @Test void testDeserializeFailsWithJsonProcessingException() { @@ -78,4 +136,8 @@ class JacksonDeserializerTest { verify ex } + + private String base64(String input) { + return Encoders.BASE64.encode(input.getBytes('UTF-8')) + } } diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSerializerTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSerializerTest.groovy index ae3ec961..a29bdae4 100644 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSerializerTest.groovy +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSerializerTest.groovy @@ -18,7 +18,6 @@ package io.jsonwebtoken.jackson.io import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.io.SerializationException -import io.jsonwebtoken.jackson.io.JacksonSerializer import io.jsonwebtoken.lang.Strings import org.junit.Test diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/stubs/CustomBean.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/stubs/CustomBean.groovy new file mode 100644 index 00000000..6f78993d --- /dev/null +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/stubs/CustomBean.groovy @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2019 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.jackson.io.stubs + +class CustomBean { + + private String stringValue + private int intValue + private Date dateValue + private short shortValue + private long longValue + private byte byteValue + private byte[] byteArrayValue + private CustomBean nestedValue + + String getStringValue() { + return stringValue + } + + CustomBean setStringValue(String stringValue) { + this.stringValue = stringValue + return this + } + + int getIntValue() { + return intValue + } + + CustomBean setIntValue(int intValue) { + this.intValue = intValue + return this + } + + Date getDateValue() { + return dateValue + } + + CustomBean setDateValue(Date dateValue) { + this.dateValue = dateValue + return this + } + + short getShortValue() { + return shortValue + } + + CustomBean setShortValue(short shortValue) { + this.shortValue = shortValue + return this + } + + long getLongValue() { + return longValue + } + + CustomBean setLongValue(long longValue) { + this.longValue = longValue + return this + } + + byte getByteValue() { + return byteValue + } + + CustomBean setByteValue(byte byteValue) { + this.byteValue = byteValue + return this + } + + byte[] getByteArrayValue() { + return byteArrayValue + } + + CustomBean setByteArrayValue(byte[] byteArrayValue) { + this.byteArrayValue = byteArrayValue + return this + } + + CustomBean getNestedValue() { + return nestedValue + } + + CustomBean setNestedValue(CustomBean nestedValue) { + this.nestedValue = nestedValue + return this + } + + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + + CustomBean that = (CustomBean) o + + if (byteValue != that.byteValue) return false + if (intValue != that.intValue) return false + if (longValue != that.longValue) return false + if (shortValue != that.shortValue) return false + if (!Arrays.equals(byteArrayValue, that.byteArrayValue)) return false + if (dateValue != that.dateValue) return false + if (nestedValue != that.nestedValue) return false + if (stringValue != that.stringValue) return false + + return true + } + + int hashCode() { + int result + result = stringValue.hashCode() + result = 31 * result + intValue + result = 31 * result + dateValue.hashCode() + result = 31 * result + (int) shortValue + result = 31 * result + (int) (longValue ^ (longValue >>> 32)) + result = 31 * result + (int) byteValue + result = 31 * result + Arrays.hashCode(byteArrayValue) + result = 31 * result + nestedValue.hashCode() + return result + } + + + @Override + public String toString() { + return "CustomBean{" + + "stringValue='" + stringValue + '\'' + + ", intValue=" + intValue + + ", dateValue=" + dateValue?.time+ + ", shortValue=" + shortValue + + ", longValue=" + longValue + + ", byteValue=" + byteValue + +// ", byteArrayValue=" + Arrays.toString(byteArrayValue) + + ", nestedValue=" + nestedValue + + '}'; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index be740778..32194da4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -22,6 +22,14 @@ import java.util.Map; public class DefaultClaims extends JwtMap implements Claims { + private static final String CONVERSION_ERROR_MSG = "Cannot convert existing claim value of type '%s' to desired type " + + "'%s'. JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. " + + "Anything more complex is expected to be already converted to your desired type by the JSON Deserializer " + + "implementation. You may specify a custom Deserializer for a JwtParser with the desired conversion " + + "configuration via the JwtParserBuilder.deserializeJsonWith() method. " + + "See https://github.com/jwtk/jjwt#custom-json-processor for more information. If using Jackson, you can " + + "specify custom claim POJO types as described in https://github.com/jwtk/jjwt#json-jackson-custom-types"; + public DefaultClaims() { super(); } @@ -158,7 +166,7 @@ public class DefaultClaims extends JwtMap implements Claims { } if (!requiredType.isInstance(value)) { - throw new RequiredTypeException("Expected value to be of type: " + requiredType + ", but was " + value.getClass()); + throw new RequiredTypeException(String.format(CONVERSION_ERROR_MSG, value.getClass(), requiredType)); } return requiredType.cast(value); diff --git a/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy new file mode 100644 index 00000000..5652bb00 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2019 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 + +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.jackson.io.JacksonDeserializer +import org.junit.Test + +import static org.hamcrest.CoreMatchers.is +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNotNull +import static org.junit.Assert.assertThat + +class CustomObjectDeserializationTest { + + /** + * Test parsing without and then with a custom deserializer. Ensures custom type is parsed from claims + */ + @Test + void testCustomObjectDeserialization() { + + CustomBean customBean = new CustomBean() + customBean.key1 = "value1" + customBean.key2 = 42 + + String jwtString = Jwts.builder().claim("cust", customBean).compact() + + // no custom deserialization, object is a map + Jwt jwt = Jwts.parser().parseClaimsJwt(jwtString) + assertNotNull jwt + assertEquals jwt.getBody().get('cust'), [key1: 'value1', key2: 42] + + // custom type for 'cust' claim + Deserializer deserializer = new JacksonDeserializer([cust: CustomBean]) + jwt = Jwts.parser().deserializeJsonWith(deserializer).parseClaimsJwt(jwtString) + assertNotNull jwt + CustomBean result = jwt.getBody().get("cust", CustomBean) + assertThat result, is(customBean) + } + + static class CustomBean { + private String key1 + private Integer key2 + + String getKey1() { + return key1 + } + + void setKey1(String key1) { + this.key1 = key1 + } + + Integer getKey2() { + return key2 + } + + void setKey2(Integer key2) { + this.key2 = key2 + } + + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + + CustomBean that = (CustomBean) o + + if (key1 != that.key1) return false + if (key2 != that.key2) return false + + return true + } + + int hashCode() { + int result + result = (key1 != null ? key1.hashCode() : 0) + result = 31 * result + (key2 != null ? key2.hashCode() : 0) + return result + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index a3e02466..e5bc9b24 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -45,7 +45,7 @@ class DefaultClaimsTest { fail() } catch (RequiredTypeException e) { assertEquals( - "Expected value to be of type: class java.lang.String, but was class java.lang.Integer", + String.format(DefaultClaims.CONVERSION_ERROR_MSG, 'class java.lang.Integer', 'class java.lang.String'), e.getMessage() ) } @@ -96,7 +96,7 @@ class DefaultClaimsTest { } catch (RequiredTypeException e) { assertEquals( e.getMessage(), - "Expected value to be of type: class java.lang.Short, but was class java.lang.Integer" + String.format(DefaultClaims.CONVERSION_ERROR_MSG, 'class java.lang.Integer', 'class java.lang.Short') ) } } @@ -110,7 +110,7 @@ class DefaultClaimsTest { } catch (RequiredTypeException e) { assertEquals( e.getMessage(), - "Expected value to be of type: class java.lang.Short, but was class java.lang.Integer" + String.format(DefaultClaims.CONVERSION_ERROR_MSG, 'class java.lang.Integer', 'class java.lang.Short') ) } } @@ -132,7 +132,7 @@ class DefaultClaimsTest { } catch (RequiredTypeException e) { assertEquals( e.getMessage(), - "Expected value to be of type: class java.lang.Byte, but was class java.lang.Integer" + String.format(DefaultClaims.CONVERSION_ERROR_MSG, 'class java.lang.Integer', 'class java.lang.Byte') ) } } @@ -146,7 +146,7 @@ class DefaultClaimsTest { } catch (RequiredTypeException e) { assertEquals( e.getMessage(), - "Expected value to be of type: class java.lang.Byte, but was class java.lang.Integer" + String.format(DefaultClaims.CONVERSION_ERROR_MSG, 'class java.lang.Integer', 'class java.lang.Byte') ) } }