From 7fb7b79b005cc7361a34757623a3b23bec9114c4 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Tue, 26 Sep 2023 16:15:34 -0700 Subject: [PATCH] Removed recently added Reader/Writer concepts for just overloaded methods in existing Serializer/Deserializer interfaces. This results in less change and is probably more intuitive for existing library users. --- api/src/main/java/io/jsonwebtoken/Claims.java | 6 +- .../main/java/io/jsonwebtoken/JwtBuilder.java | 11 +- .../io/jsonwebtoken/JwtParserBuilder.java | 13 +- .../jsonwebtoken/io/AbstractDeserializer.java | 72 +++++ .../jsonwebtoken/io/AbstractSerializer.java | 46 +++ .../java/io/jsonwebtoken/io/Deserializer.java | 18 +- .../io/jsonwebtoken/io/ParserBuilder.java | 8 +- .../main/java/io/jsonwebtoken/io/Reader.java | 36 --- .../java/io/jsonwebtoken/io/Serializer.java | 22 +- .../main/java/io/jsonwebtoken/io/Writer.java | 36 --- .../java/io/jsonwebtoken/lang/Strings.java | 16 +- .../io/AbstractDeserializerTest.groovy | 85 +++++ .../io/AbstractSerializerTest.groovy | 59 ++++ .../gson/io/GsonDeserializer.java | 44 ++- .../io/jsonwebtoken/gson/io/GsonReader.java | 58 ---- .../jsonwebtoken/gson/io/GsonSerializer.java | 76 +++-- .../io/jsonwebtoken/gson/io/GsonWriter.java | 91 ------ .../services/io.jsonwebtoken.io.Reader | 1 - .../services/io.jsonwebtoken.io.Writer | 1 - .../gson/io/GsonDeserializerTest.groovy | 26 +- .../gson/io/GsonReaderTest.groovy | 97 ------ .../gson/io/GsonSerializerTest.groovy | 89 +++++- .../gson/io/GsonWriterTest.groovy | 145 --------- .../jackson/io/JacksonDeserializer.java | 84 +++-- .../jackson/io/JacksonReader.java | 127 -------- .../jackson/io/JacksonSerializer.java | 71 ++--- .../jackson/io/JacksonWriter.java | 70 ---- .../services/io.jsonwebtoken.io.Reader | 1 - .../services/io.jsonwebtoken.io.Writer | 1 - .../jackson/io/JacksonDeserializerTest.groovy | 35 +- .../jackson/io/JacksonReaderTest.groovy | 195 ------------ .../jackson/io/JacksonSerializerTest.groovy | 86 +++-- .../io/JacksonSupplierSerializerTest.groovy | 18 +- .../jackson/io/JacksonWriterTest.groovy | 134 -------- .../jackson/io/stubs/CustomBean.groovy | 6 +- .../orgjson/io/OrgJsonDeserializer.java | 87 ++++- .../orgjson/io/OrgJsonReader.java | 101 ------ .../orgjson/io/OrgJsonSerializer.java | 179 +++++++++-- .../orgjson/io/OrgJsonWriter.java | 188 ----------- .../services/io.jsonwebtoken.io.Reader | 1 - .../services/io.jsonwebtoken.io.Writer | 1 - .../io/AndroidOrgJsonSerializerTest.groovy | 2 +- .../orgjson/io/OrgJsonDeserializerTest.groovy | 133 +++++++- .../orgjson/io/OrgJsonReaderTest.groovy | 156 --------- .../orgjson/io/OrgJsonSerializerTest.groovy | 237 +++++++++++++- .../orgjson/io/OrgJsonWriterTest.groovy | 301 ------------------ .../jsonwebtoken/impl/DefaultJwtBuilder.java | 49 ++- .../jsonwebtoken/impl/DefaultJwtParser.java | 16 +- .../impl/DefaultJwtParserBuilder.java | 17 +- .../impl/io/AbstractParserBuilder.java | 12 +- .../impl/io/ConvertingParser.java | 16 +- .../impl/io/DeserializingMapReader.java | 50 --- .../impl/io/JsonObjectDeserializer.java | 19 +- ...ngSerializer.java => NamedSerializer.java} | 21 +- .../impl/io/SerializingMapWriter.java | 40 --- .../java/io/jsonwebtoken/impl/io/Streams.java | 9 + .../security/DefaultJwkParserBuilder.java | 2 +- .../security/DefaultJwkSetParserBuilder.java | 2 +- .../impl/security/JwkDeserializer.java | 6 +- .../impl/security/JwkSetDeserializer.java | 6 +- .../impl/security/JwksBridge.java | 25 +- .../CustomObjectDeserializationTest.groovy | 6 +- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 12 +- .../jsonwebtoken/RFC7515AppendixETest.groovy | 30 +- .../groovy/io/jsonwebtoken/RFC7797Test.groovy | 5 +- .../impl/DefaultJwtBuilderTest.groovy | 55 ++-- .../impl/DefaultJwtParserBuilderTest.groovy | 14 +- .../impl/DefaultJwtParserTest.groovy | 22 +- .../io/jsonwebtoken/impl/RfcTests.groovy | 6 +- .../impl/io/JsonObjectDeserializerTest.groovy | 33 +- .../impl/io/TestSerializer.groovy | 48 +++ .../DefaultJwkParserBuilderTest.groovy | 21 +- .../DefaultJwkSetParserBuilderTest.groovy | 8 +- .../impl/security/JwkSerializationTest.groovy | 68 ++-- .../impl/security/RFC7517AppendixCTest.groovy | 13 +- .../impl/security/RFC7518AppendixCTest.groovy | 5 +- .../impl/security/RFC7520Section4Test.groovy | 55 ++-- .../impl/security/RFC7520Section5Test.groovy | 40 ++- 78 files changed, 1590 insertions(+), 2411 deletions(-) create mode 100644 api/src/main/java/io/jsonwebtoken/io/AbstractDeserializer.java create mode 100644 api/src/main/java/io/jsonwebtoken/io/AbstractSerializer.java delete mode 100644 api/src/main/java/io/jsonwebtoken/io/Reader.java delete mode 100644 api/src/main/java/io/jsonwebtoken/io/Writer.java create mode 100644 api/src/test/groovy/io/jsonwebtoken/io/AbstractDeserializerTest.groovy create mode 100644 api/src/test/groovy/io/jsonwebtoken/io/AbstractSerializerTest.groovy delete mode 100644 extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonReader.java delete mode 100644 extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonWriter.java delete mode 100644 extensions/gson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Reader delete mode 100644 extensions/gson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Writer delete mode 100644 extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonReaderTest.groovy delete mode 100644 extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonWriterTest.groovy delete mode 100644 extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonReader.java delete mode 100644 extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonWriter.java delete mode 100644 extensions/jackson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Reader delete mode 100644 extensions/jackson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Writer delete mode 100644 extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonReaderTest.groovy delete mode 100644 extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonWriterTest.groovy delete mode 100644 extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonReader.java delete mode 100644 extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonWriter.java delete mode 100644 extensions/orgjson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Reader delete mode 100644 extensions/orgjson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Writer delete mode 100644 extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonReaderTest.groovy delete mode 100644 extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonWriterTest.groovy delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/io/DeserializingMapReader.java rename impl/src/main/java/io/jsonwebtoken/impl/io/{WritingSerializer.java => NamedSerializer.java} (62%) delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/io/SerializingMapWriter.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/io/TestSerializer.groovy diff --git a/api/src/main/java/io/jsonwebtoken/Claims.java b/api/src/main/java/io/jsonwebtoken/Claims.java index 8854232ce..a85268f15 100644 --- a/api/src/main/java/io/jsonwebtoken/Claims.java +++ b/api/src/main/java/io/jsonwebtoken/Claims.java @@ -15,7 +15,7 @@ */ package io.jsonwebtoken; -import io.jsonwebtoken.io.Reader; +import io.jsonwebtoken.io.Deserializer; import java.util.Date; import java.util.Map; @@ -150,8 +150,8 @@ public interface Claims extends Map, Identifiable { * *

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 parser. You may specify a custom - * JSON {@link io.jsonwebtoken.io.Reader Reader} with the desired configuration via the - * {@link JwtParserBuilder#jsonReader(Reader)} method. + * JSON {@link io.jsonwebtoken.io.Deserializer Deserializer} with the desired configuration via the + * {@link JwtParserBuilder#json(Deserializer)} method. * See custom JSON processor for more * information. If using Jackson, you can specify custom claim POJO types as described in * custom claim types. diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index 7b105352c..65754185e 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -20,7 +20,6 @@ import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Encoder; import io.jsonwebtoken.io.Serializer; -import io.jsonwebtoken.io.Writer; import io.jsonwebtoken.lang.MapMutator; import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.InvalidKeyException; @@ -1017,25 +1016,25 @@ public interface JwtBuilder extends ClaimsMutator { * @param serializer the serializer to use when converting Map objects to JSON strings. * @return the builder for method chaining. * @since 0.10.0 - * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #jsonWriter(Writer)} + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #json(Serializer)} */ @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated JwtBuilder serializeToJsonWith(Serializer> serializer); /** - * Perform Map-to-JSON serialization with the specified writer. This is used by the builder to convert + * Perform Map-to-JSON serialization with the specified Serializer. This is used by the builder to convert * JWT/JWS/JWE headers and Claims Maps to JSON strings as required by the JWT specification. * - *

If this method is not called, JJWT will use whatever Writer it can find at runtime, checking for the + *

If this method is not called, JJWT will use whatever Serializer it can find at runtime, checking for the * presence of well-known implementations such Jackson, Gson, and org.json. If one of these is not found * in the runtime classpath, an exception will be thrown when the {@link #compact()} method is invoked.

* - * @param writer the writer to use when converting Map objects to JSON strings. + * @param serializer the Serializer to use when converting Map objects to JSON strings. * @return the builder for method chaining. * @since JJWT_RELEASE_VERSION */ - JwtBuilder jsonWriter(Writer> writer); + JwtBuilder json(Serializer> serializer); /** * Actually builds the JWT and serializes it to a compact, URL-safe string according to the diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index e78c13cd2..381e2a8ac 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -18,7 +18,6 @@ import io.jsonwebtoken.io.CompressionAlgorithm; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Deserializer; -import io.jsonwebtoken.io.Reader; import io.jsonwebtoken.lang.Builder; import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.KeyAlgorithm; @@ -729,27 +728,27 @@ public interface JwtParserBuilder extends Builder { * * @param deserializer the deserializer to use when converting JSON Strings (UTF-8 byte arrays) into Map objects. * @return the builder for method chaining. - * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #jsonReader(Reader)}. + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #json(Deserializer)}. * This method will be removed before the JJWT 1.0 release. */ @Deprecated JwtParserBuilder deserializeJsonWith(Deserializer> deserializer); /** - * Uses the specified JSON {@link Reader} to deserialize JSON (UTF-8 byte streams) into Java Map objects. This is - * used by the parser after Base64Url-decoding to convert JWT/JWS/JWT headers and Claims into Java Map + * Uses the specified JSON {@link Deserializer} to deserialize JSON (UTF-8 byte streams) into Java Map objects. + * This is used by the parser after Base64Url-decoding to convert JWT/JWS/JWT headers and Claims into Java Map * instances. * - *

If this method is not called, JJWT will use whatever Reader it can find at runtime, checking for the + *

If this method is not called, JJWT will use whatever Deserializer it can find at runtime, checking for the * presence of well-known implementations such Jackson, Gson, and org.json. If one of these is not found * in the runtime classpath, an exception will be thrown when one of the various {@code parse}* methods is * invoked.

* - * @param reader the reader to use to deserialize JSON (UTF-8 byte streams) into Map instances. + * @param deserializer the deserializer to use to deserialize JSON (UTF-8 byte streams) into Map instances. * @return the builder for method chaining. * @since JJWT_RELEASE_VERSION */ - JwtParserBuilder jsonReader(Reader> reader); + JwtParserBuilder json(Deserializer> deserializer); /** * Returns an immutable/thread-safe {@link JwtParser} created from the configuration from this JwtParserBuilder. diff --git a/api/src/main/java/io/jsonwebtoken/io/AbstractDeserializer.java b/api/src/main/java/io/jsonwebtoken/io/AbstractDeserializer.java new file mode 100644 index 000000000..bca39ad9c --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/io/AbstractDeserializer.java @@ -0,0 +1,72 @@ +/* + * Copyright © 2023 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.io; + +import io.jsonwebtoken.lang.Assert; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * Convenient base class to use to implement {@link Deserializer}s, with only the {@link #doDeserialize(InputStream)}. + * + * @param the type of object returned after deserialization + * @since JJWT_RELEASE_VERSION + */ +public abstract class AbstractDeserializer implements Deserializer { + + /** + * EOF (End of File) marker, equal to {@code -1}. + */ + protected static final int EOF = -1; + + private static final byte[] EMPTY_BYTES = new byte[0]; + + /** + * {@inheritDoc} + */ + @Override + public final T deserialize(byte[] bytes) throws DeserializationException { + bytes = bytes == null ? EMPTY_BYTES : bytes; // null safe + return deserialize(new ByteArrayInputStream(bytes)); + } + + /** + * {@inheritDoc} + */ + @Override + public final T deserialize(InputStream in) throws DeserializationException { + Assert.notNull(in, "InputStream argument cannot be null."); + try { + return doDeserialize(in); + } catch (Throwable t) { + if (t instanceof DeserializationException) { + throw (DeserializationException) t; + } + String msg = "Unable to deserialize: " + t.getMessage(); + throw new DeserializationException(msg, t); + } + } + + /** + * Reads the specified {@code InputStream} and returns the corresponding Java object. + * + * @param in the input stream to read + * @return the deserialized Java object + * @throws DeserializationException if there is a problem reading the stream or creating the expected Java object + */ + protected abstract T doDeserialize(InputStream in) throws Exception; +} diff --git a/api/src/main/java/io/jsonwebtoken/io/AbstractSerializer.java b/api/src/main/java/io/jsonwebtoken/io/AbstractSerializer.java new file mode 100644 index 000000000..e4737bddc --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/io/AbstractSerializer.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2023 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.io; + +import io.jsonwebtoken.lang.Objects; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +public abstract class AbstractSerializer implements Serializer { + + @Override + public final byte[] serialize(T t) throws SerializationException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + serialize(t, out); + return out.toByteArray(); + } + + @Override + public void serialize(T t, OutputStream out) throws SerializationException { + try { + doSerialize(t, out); + } catch (Throwable e) { + if (e instanceof SerializationException) { + throw (SerializationException) e; + } + String msg = "Unable to serialize object of type " + Objects.nullSafeClassName(t) + ": " + e.getMessage(); + throw new SerializationException(msg, e); + } + } + + protected abstract void doSerialize(T t, OutputStream out) throws Exception; +} diff --git a/api/src/main/java/io/jsonwebtoken/io/Deserializer.java b/api/src/main/java/io/jsonwebtoken/io/Deserializer.java index a8adb1adb..5b73b5e1c 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Deserializer.java +++ b/api/src/main/java/io/jsonwebtoken/io/Deserializer.java @@ -15,14 +15,14 @@ */ package io.jsonwebtoken.io; +import java.io.InputStream; + /** - * A {@code Deserializer} is able to convert serialized data byte arrays into Java objects. + * A {@code Deserializer} is able to convert serialized byte streams into Java objects. * * @param the type of object to be returned as a result of deserialization. * @since 0.10.0 - * @deprecated since JJWT_RELEASE_VERSION in favor of {@link io.jsonwebtoken.io.Reader} */ -@Deprecated public interface Deserializer { /** @@ -31,8 +31,18 @@ public interface Deserializer { * @param bytes the formatted data byte array to convert * @return the reconstituted Java object * @throws DeserializationException if there is a problem converting the byte array to an object. - * @deprecated since JJWT_RELEASE_VERSION in favor of {@link Reader#read(java.io.Reader)} + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #deserialize(InputStream)} */ @Deprecated T deserialize(byte[] bytes) throws DeserializationException; + + /** + * Reads the specified {@code InputStream} and returns the corresponding Java object. + * + * @param in the input stream to read + * @return the deserialized Java object + * @throws DeserializationException if there is a problem reading the stream or creating the expected Java object + * @since JJWT_RELEASE_VERSION + */ + T deserialize(InputStream in) throws DeserializationException; } diff --git a/api/src/main/java/io/jsonwebtoken/io/ParserBuilder.java b/api/src/main/java/io/jsonwebtoken/io/ParserBuilder.java index faf62f940..0b28d0bd9 100644 --- a/api/src/main/java/io/jsonwebtoken/io/ParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/io/ParserBuilder.java @@ -40,15 +40,15 @@ public interface ParserBuilder> extends Builder B provider(Provider provider); /** - * Uses the specified reader to convert JSON Strings (UTF-8 byte streams) into Java Map objects. The + * Uses the specified {@code Deserializer} to convert JSON Strings (UTF-8 byte streams) into Java Map objects. The * resulting Maps are then used to construct respective JWT objects (JWTs, JWKs, etc). * - *

If this method is not called, JJWT will use whatever Reader it can find at runtime, checking for the + *

If this method is not called, JJWT will use whatever Deserializer it can find at runtime, checking for the * presence of well-known implementations such as Jackson, Gson, and org.json. If one of these is not found * in the runtime classpath, an exception will be thrown when the {@link #build()} method is called. * - * @param reader the Reader to use when converting JSON Strings (UTF-8 byte streams) into Map objects. + * @param reader the Deserializer to use when converting JSON Strings (UTF-8 byte streams) into Map objects. * @return the builder for method chaining. */ - B jsonReader(Reader> reader); + B json(Deserializer> reader); } diff --git a/api/src/main/java/io/jsonwebtoken/io/Reader.java b/api/src/main/java/io/jsonwebtoken/io/Reader.java deleted file mode 100644 index da4bc4603..000000000 --- a/api/src/main/java/io/jsonwebtoken/io/Reader.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright © 2023 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.io; - -import java.io.IOException; - -/** - * A {@code Reader} reads an object from an input stream. - * - * @param the type of object read. - * @since JJWT_RELEASE_VERSION - */ -public interface Reader { - - /** - * Reads an object from an input stream, but does not close it; the caller must close the stream as necessary. - * - * @param in the input stream. - * @return the object read from the stream. - * @throws IOException if there is a problem reading from the stream or creating the expected object. - */ - T read(java.io.Reader in) throws IOException; -} diff --git a/api/src/main/java/io/jsonwebtoken/io/Serializer.java b/api/src/main/java/io/jsonwebtoken/io/Serializer.java index 34b5b37fe..d0d68a51d 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Serializer.java +++ b/api/src/main/java/io/jsonwebtoken/io/Serializer.java @@ -15,25 +15,37 @@ */ package io.jsonwebtoken.io; +import java.io.OutputStream; + /** - * A {@code Serializer} is able to convert a Java object into a formatted data byte array. It is expected this data + * A {@code Serializer} is able to convert a Java object into a formatted byte stream. It is expected this byte stream * can be reconstituted back into a Java object with a matching {@link Deserializer}. * * @param The type of object to serialize. * @since 0.10.0 - * @deprecated since JJWT_RELEASE_VERSION in favor of {@link io.jsonwebtoken.io.Writer} */ -@Deprecated public interface Serializer { /** - * Convert the specified Java object into a formatted data byte array. + * Converts the specified Java object into a formatted data byte array. * * @param t the object to serialize * @return the serialized byte array representing the specified object. * @throws SerializationException if there is a problem converting the object to a byte array. - * @deprecated since JJWT_RELEASE_VERSION in favor of {@link Writer#write(java.io.Writer, Object)} + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #serialize(Object, OutputStream)} */ @Deprecated byte[] serialize(T t) throws SerializationException; + + /** + * Converts the specified Java object into a formatted data byte stream, writing the bytes to the specified + * {@code out}put stream. + * + * @param t the object to convert to a byte stream + * @param out the stream to write to + * @throws SerializationException if there is a problem converting the object to a byte stream or writing the + * bytes to the {@code out}put stream. + * @since JJWT_RELEASE_VERSION + */ + void serialize(T t, OutputStream out) throws SerializationException; } diff --git a/api/src/main/java/io/jsonwebtoken/io/Writer.java b/api/src/main/java/io/jsonwebtoken/io/Writer.java deleted file mode 100644 index d99515df8..000000000 --- a/api/src/main/java/io/jsonwebtoken/io/Writer.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright © 2023 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.io; - -import java.io.IOException; - -/** - * A {@code Writer} writes an object to an output stream. - * - * @param the type of object to write. - * @since JJWT_RELEASE_VERSION - */ -public interface Writer { - - /** - * Writes {@code t} to the output stream, but does not close it; the caller must close the stream as necessary. - * - * @param out the output stream. - * @param t the object to write. - * @throws IOException if there is a problem writing. - */ - void write(java.io.Writer out, T t) throws IOException; -} diff --git a/api/src/main/java/io/jsonwebtoken/lang/Strings.java b/api/src/main/java/io/jsonwebtoken/lang/Strings.java index a405e5c54..0d9d78a0b 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Strings.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Strings.java @@ -243,15 +243,13 @@ public static CharSequence clean(CharSequence str) { * @since JJWT_RELEASE_VERSION */ public static byte[] utf8(CharSequence s) { - byte[] bytes = null; - if (s != null) { - CharBuffer cb = s instanceof CharBuffer ? (CharBuffer) s : CharBuffer.wrap(s); - cb.mark(); - ByteBuffer buf = UTF_8.encode(cb); - bytes = new byte[buf.remaining()]; - buf.get(bytes); - cb.reset(); - } + if (s == null) return null; + CharBuffer cb = s instanceof CharBuffer ? (CharBuffer) s : CharBuffer.wrap(s); + cb.mark(); + ByteBuffer buf = UTF_8.encode(cb); + byte[] bytes = new byte[buf.remaining()]; + buf.get(bytes); + cb.reset(); return bytes; } diff --git a/api/src/test/groovy/io/jsonwebtoken/io/AbstractDeserializerTest.groovy b/api/src/test/groovy/io/jsonwebtoken/io/AbstractDeserializerTest.groovy new file mode 100644 index 000000000..c54061416 --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/io/AbstractDeserializerTest.groovy @@ -0,0 +1,85 @@ +/* + * Copyright © 2023 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.io + +import org.junit.Test + +import static org.junit.Assert.* + +class AbstractDeserializerTest { + + @Test + void deserializeNullByteArray() { + boolean invoked = false + def deser = new AbstractDeserializer() { + @Override + protected Object doDeserialize(InputStream inputStream) throws Exception { + assertEquals EOF, inputStream.read() + invoked = true + } + } + deser.deserialize((byte[]) null) + assertTrue invoked + } + + @Test + void deserializeEmptyByteArray() { + boolean invoked = false + def deser = new AbstractDeserializer() { + @Override + protected Object doDeserialize(InputStream inputStream) throws Exception { + assertEquals EOF, inputStream.read() + invoked = true + } + } + deser.deserialize(new byte[0]) + assertTrue invoked + } + + @Test + void deserializeByteArray() { + byte b = 0x01 + def bytes = new byte[]{b} + def des = new AbstractDeserializer() { + @Override + protected Object doDeserialize(InputStream inputStream) throws Exception { + assertEquals b, inputStream.read() + return 42 + } + } + assertEquals 42, des.deserialize(bytes) + } + + @Test + void deserializeException() { + + def ex = new RuntimeException('foo') + def des = new AbstractDeserializer() { + @Override + protected Object doDeserialize(InputStream inputStream) throws Exception { + throw ex + } + } + + try { + des.deserialize(new byte[]{0x01}) + } catch (DeserializationException expected) { + String msg = 'Unable to deserialize: foo' + assertEquals msg, expected.message + assertSame ex, expected.cause + } + } +} diff --git a/api/src/test/groovy/io/jsonwebtoken/io/AbstractSerializerTest.groovy b/api/src/test/groovy/io/jsonwebtoken/io/AbstractSerializerTest.groovy new file mode 100644 index 000000000..ad4107db0 --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/io/AbstractSerializerTest.groovy @@ -0,0 +1,59 @@ +/* + * Copyright © 2023 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.io + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class AbstractSerializerTest { + + @Test + void serializeByteArray() { + def value = 42 + def ser = new AbstractSerializer() { + @Override + protected void doSerialize(Object o, OutputStream out) throws Exception { + assertEquals value, o + out.write(0x01) + } + } + + def out = ser.serialize(value) + assertEquals 0x01, out[0] + } + + @Test + void serializeException() { + + def ex = new RuntimeException('foo') + def ser = new AbstractSerializer() { + @Override + protected void doSerialize(Object o, OutputStream out) throws Exception { + throw ex + } + } + + try { + ser.serialize(42, new ByteArrayOutputStream()) + } catch (SerializationException expected) { + String msg = 'Unable to serialize object of type java.lang.Integer: foo' + assertEquals msg, expected.message + assertSame ex, expected.cause + } + } +} 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 index 88a3cb4b9..f4987516c 100644 --- a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonDeserializer.java +++ b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonDeserializer.java @@ -16,42 +16,38 @@ package io.jsonwebtoken.gson.io; import com.google.gson.Gson; -import io.jsonwebtoken.io.DeserializationException; -import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.io.AbstractDeserializer; +import io.jsonwebtoken.lang.Assert; -import java.io.ByteArrayInputStream; -import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; +import java.nio.charset.StandardCharsets; -@SuppressWarnings("DeprecatedIsStillUsed") -@Deprecated -public class GsonDeserializer extends GsonReader implements Deserializer { +public class GsonDeserializer extends AbstractDeserializer { + + private final Class returnType; + protected final Gson gson; public GsonDeserializer() { - super(); + this(GsonSerializer.DEFAULT_GSON); } + @SuppressWarnings("unchecked") public GsonDeserializer(Gson gson) { - super(gson); + this(gson, (Class) Object.class); } - @SuppressWarnings("deprecation") - @Deprecated - @Override - public T deserialize(byte[] bytes) throws DeserializationException { - try { - return readValue(bytes); - } catch (Throwable t) { - String msg = "Unable to deserialize JSON: " + t.getMessage(); - throw new DeserializationException(msg, t); - } + 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; } - @Deprecated - protected T readValue(byte[] bytes) throws IOException { - try (Reader reader = new InputStreamReader(new ByteArrayInputStream(bytes))) { - return read(reader); - } + @Override + protected T doDeserialize(InputStream in) throws Exception { + Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8); + return gson.fromJson(reader, returnType); } } diff --git a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonReader.java b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonReader.java deleted file mode 100644 index c8a1ddb75..000000000 --- a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonReader.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright © 2023 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.gson.io; - -import com.google.gson.Gson; -import io.jsonwebtoken.io.Reader; -import io.jsonwebtoken.lang.Assert; - -import java.io.IOException; - -public class GsonReader implements Reader { - - private final Class returnType; - protected final Gson gson; - - public GsonReader() { - this(GsonWriter.DEFAULT_GSON); - } - - @SuppressWarnings("unchecked") - public GsonReader(Gson gson) { - this(gson, (Class) Object.class); - } - - private GsonReader(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 read(java.io.Reader in) throws IOException { - try { - return readValue(in); - } catch (Throwable t) { - String msg = "Unable to read JSON as a " + returnType.getName() + " instance: " + t.getMessage(); - throw new IOException(msg, t); - } - } - - protected T readValue(java.io.Reader reader) { - return gson.fromJson(reader, 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 index ac969c71f..c30a491ec 100644 --- a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java +++ b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java @@ -16,44 +16,78 @@ package io.jsonwebtoken.gson.io; import com.google.gson.Gson; -import io.jsonwebtoken.io.SerializationException; -import io.jsonwebtoken.io.Serializer; +import com.google.gson.GsonBuilder; +import io.jsonwebtoken.io.AbstractSerializer; +import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Supplier; -import java.io.ByteArrayOutputStream; -import java.io.IOException; +import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.Writer; import java.nio.charset.StandardCharsets; -@SuppressWarnings("DeprecatedIsStillUsed") -@Deprecated -public class GsonSerializer extends GsonWriter implements Serializer { +public class GsonSerializer extends AbstractSerializer { + + static final Gson DEFAULT_GSON = new GsonBuilder() + .registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE) + .disableHtmlEscaping().create(); + + protected final Gson gson; public GsonSerializer() { - super(); + this(DEFAULT_GSON); } public GsonSerializer(Gson gson) { - super(gson); + Assert.notNull(gson, "gson cannot be null."); + this.gson = gson; + + //ensure the necessary type adapter has been registered, and if not, throw an error: + String json = this.gson.toJson(TestSupplier.INSTANCE); + if (json.contains("value")) { + String msg = "Invalid Gson instance - it has not been registered with the necessary " + + Supplier.class.getName() + " type adapter. When using the GsonBuilder, ensure this " + + "type adapter is registered by calling gsonBuilder.registerTypeHierarchyAdapter(" + + Supplier.class.getName() + ".class, " + + GsonSupplierSerializer.class.getName() + ".INSTANCE) before calling gsonBuilder.create()"; + throw new IllegalArgumentException(msg); + } } - @SuppressWarnings("deprecation") @Override - public byte[] serialize(T t) throws SerializationException { - Assert.notNull(t, "Object to serialize cannot be null."); + protected void doSerialize(T t, OutputStream out) { + Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); try { - return writeValueAsBytes(t); - } catch (Throwable ex) { - String msg = "Unable to serialize object: " + ex.getMessage(); - throw new SerializationException(msg, ex); + Object o = t; + if (o instanceof byte[]) { + o = Encoders.BASE64.encode((byte[]) o); + } else if (o instanceof char[]) { + o = new String((char[]) o); + } + writeValue(o, writer); + } finally { + Objects.nullSafeClose(writer); } } - protected byte[] writeValueAsBytes(T t) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(256); - try (OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8)) { - write(writer, t); + protected void writeValue(Object o, java.io.Writer writer) { + this.gson.toJson(o, writer); + } + + private static class TestSupplier implements Supplier { + + private static final TestSupplier INSTANCE = new TestSupplier<>("test"); + private final T value; + + private TestSupplier(T value) { + this.value = value; + } + + @Override + public T get() { + return value; } - return baos.toByteArray(); } } diff --git a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonWriter.java b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonWriter.java deleted file mode 100644 index bdd94de53..000000000 --- a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonWriter.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright © 2023 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.gson.io; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import io.jsonwebtoken.io.Encoders; -import io.jsonwebtoken.io.Writer; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Supplier; - -import java.io.IOException; - -public class GsonWriter implements Writer { - - static final Gson DEFAULT_GSON = new GsonBuilder() - .registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE) - .disableHtmlEscaping().create(); - - protected final Gson gson; - - public GsonWriter() { - this(DEFAULT_GSON); - } - - public GsonWriter(Gson gson) { - Assert.notNull(gson, "gson cannot be null."); - this.gson = gson; - - //ensure the necessary type adapter has been registered, and if not, throw an error: - String json = this.gson.toJson(TestSupplier.INSTANCE); - if (json.contains("value")) { - String msg = "Invalid Gson instance - it has not been registered with the necessary " + - Supplier.class.getName() + " type adapter. When using the GsonBuilder, ensure this " + - "type adapter is registered by calling gsonBuilder.registerTypeHierarchyAdapter(" + - Supplier.class.getName() + ".class, " + - GsonSupplierSerializer.class.getName() + ".INSTANCE) before calling gsonBuilder.create()"; - throw new IllegalArgumentException(msg); - } - } - - @Override - public void write(java.io.Writer out, T t) throws IOException { - Assert.notNull(t, "Object to write cannot be null."); - Assert.notNull(out, "Writer cannot be null."); - Object o = t; - try { - if (o instanceof byte[]) { - o = Encoders.BASE64.encode((byte[]) t); - } else if (o instanceof char[]) { - o = new String((char[]) o); - } - writeValue(o, out); - } catch (Throwable ex) { - String msg = "Unable to serialize object of type " + o.getClass().getName() + ": " + ex.getMessage(); - throw new IOException(msg, ex); - } - } - - protected void writeValue(Object o, java.io.Writer writer) { - this.gson.toJson(o, writer); - } - - private static class TestSupplier implements Supplier { - - private static final TestSupplier INSTANCE = new TestSupplier<>("test"); - private final T value; - - private TestSupplier(T value) { - this.value = value; - } - - @Override - public T get() { - return value; - } - } -} diff --git a/extensions/gson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Reader b/extensions/gson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Reader deleted file mode 100644 index 2a735a7b4..000000000 --- a/extensions/gson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Reader +++ /dev/null @@ -1 +0,0 @@ -io.jsonwebtoken.gson.io.GsonReader \ No newline at end of file diff --git a/extensions/gson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Writer b/extensions/gson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Writer deleted file mode 100644 index b7d0605ad..000000000 --- a/extensions/gson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Writer +++ /dev/null @@ -1 +0,0 @@ -io.jsonwebtoken.gson.io.GsonWriter \ No newline at end of file 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 index 098f691cd..aca9f1321 100644 --- a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy +++ b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy @@ -29,6 +29,14 @@ class GsonDeserializerTest { private GsonDeserializer deserializer + private def deser(byte[] data) { + deserializer.deserialize(new ByteArrayInputStream(data)) + } + + private def deser(String s) { + return deser(Strings.utf8(s)) + } + @Before void setUp() { deserializer = new GsonDeserializer() @@ -46,37 +54,37 @@ class GsonDeserializerTest { } @Test - void testObjectMapperConstructor() { + void testGsonConstructor() { def customGSON = new Gson() - def deserializer = new GsonDeserializer(customGSON) + deserializer = new GsonDeserializer(customGSON) assertSame customGSON, deserializer.gson } @Test(expected = IllegalArgumentException) - void testObjectMapperConstructorWithNullArgument() { - new GsonDeserializer<>(null) + void testGsonConstructorNullArgument() { + new GsonDeserializer(null) } @Test void testDeserialize() { def expected = [hello: '世界'] - assertEquals expected, deserializer.deserialize(Strings.utf8('{"hello":"世界"}')) + assertEquals expected, deser('{"hello":"世界"}') } @Test - void testDeserializeFailsWithJsonProcessingException() { + void testDeserializeThrows() { def ex = new IOException('foo') deserializer = new GsonDeserializer() { @Override - protected Object readValue(byte[] bytes) throws IOException { + protected Object doDeserialize(InputStream inputStream) throws Exception { throw ex } } try { - deserializer.deserialize(Strings.utf8('{"hello":"世界"}')) + deser('{"hello":"世界"}') fail() } catch (DeserializationException expected) { - String msg = 'Unable to deserialize JSON: foo' + String msg = 'Unable to deserialize: foo' assertEquals msg, expected.message assertSame ex, expected.cause } diff --git a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonReaderTest.groovy b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonReaderTest.groovy deleted file mode 100644 index 9be8a64f9..000000000 --- a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonReaderTest.groovy +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright © 2023 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.gson.io - -import com.google.gson.Gson -import io.jsonwebtoken.io.Reader -import io.jsonwebtoken.lang.Strings -import org.junit.Before -import org.junit.Test - -import java.nio.charset.StandardCharsets - -import static org.junit.Assert.* - -class GsonReaderTest { - - static def bytesReader(byte[] bytes) { - return new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8) - } - - private GsonReader reader - - private def read(byte[] data) { - def r = bytesReader(data) - return reader.read(r) - } - - private def read(String s) { - return read(Strings.utf8(s)) - } - - @Before - void setUp() { - reader = new GsonReader() - } - - @Test - void loadService() { - def reader = ServiceLoader.load(Reader).iterator().next() - assertTrue reader instanceof GsonReader - } - - @Test - void testDefaultConstructor() { - assertNotNull reader.gson - } - - @Test - void testGsonConstructor() { - def customGSON = new Gson() - def reader = new GsonReader(customGSON) - assertSame customGSON, reader.gson - } - - @Test(expected = IllegalArgumentException) - void testGsonConstructorWithNullArgument() { - new GsonReader<>(null) - } - - @Test - void testRead() { - def expected = [hello: '世界'] - assertEquals expected, read('{"hello":"世界"}') - } - - @Test - void testReadThrows() { - def ex = new IllegalArgumentException('foo') - reader = new GsonReader() { - @Override - protected Object readValue(java.io.Reader reader) { - throw ex - } - } - try { - read('{"hello":"世界"}') - fail() - } catch (IOException expected) { - String msg = 'Unable to read JSON as a java.lang.Object instance: foo' - assertEquals msg, expected.message - assertSame ex, expected.cause - } - } -} 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 index 291a97808..624831b77 100644 --- a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy +++ b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy @@ -16,6 +16,7 @@ //file:noinspection GrDeprecatedAPIUsage package io.jsonwebtoken.gson.io +import com.google.gson.Gson import com.google.gson.GsonBuilder import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.Serializer @@ -64,12 +65,96 @@ class GsonSerializerTest { assertEquals '"hello"', ser('hello') } + private byte[] bytes(def o) { + ByteArrayOutputStream out = new ByteArrayOutputStream() + s.serialize(o, out) + return out.toByteArray() + } + + private String json(def o) { + return Strings.utf8(bytes(o)) + } + + @Test + void testGsonConstructorWithIncorrectlyConfiguredGson() { + try { + //noinspection GroovyResultOfObjectAllocationIgnored + new GsonSerializer<>(new Gson()) + fail() + } catch (IllegalArgumentException expected) { + String msg = 'Invalid Gson instance - it has not been registered with the necessary ' + + 'io.jsonwebtoken.lang.Supplier type adapter. When using the GsonBuilder, ensure this type ' + + 'adapter is registered by calling ' + + 'gsonBuilder.registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, ' + + 'io.jsonwebtoken.gson.io.GsonSupplierSerializer.INSTANCE) before calling gsonBuilder.create()' + assertEquals msg, expected.message + } + } + + @Test(expected = IllegalArgumentException) + void testConstructorWithNullArgument() { + new GsonSerializer<>(null) + } + + @Test + void testByte() { + byte[] expected = Strings.utf8("120") //ascii("x") = 120 + byte[] result = bytes(Strings.utf8("x")[0]) //single byte + assertArrayEquals expected, result + } + + @Test + void testByteArray() { //expect Base64 string by default: + String expected = '"aGk="' as String //base64(hi) --> aGk= + assertEquals expected, json(Strings.utf8('hi')) + } + + @Test + void testEmptyByteArray() { //expect Base64 string by default: + byte[] result = bytes(new byte[0]) + assertEquals '""', Strings.utf8(result) + } + + @Test + void testChar() { //expect Base64 string by default: + assertEquals '"h"', json('h' as char) + } + + @Test + void testCharArray() { //expect string by default: + assertEquals '"hi"', json('hi'.toCharArray()) + } + + @Test + void testWrite() { + assertEquals '{"hello":"世界"}', json([hello: '世界']) + } + + @Test + void testWriteFailure() { + def ex = new IOException('foo') + s = new GsonSerializer() { + @Override + protected void doSerialize(Object o, OutputStream out) { + throw ex + } + } + try { + ser([hello: 'world']) + fail() + } catch (SerializationException expected) { + String msg = 'Unable to serialize object of type java.util.LinkedHashMap: foo' + assertEquals msg, expected.message + assertSame ex, expected.cause + } + } + @Test void testIOExceptionConvertedToSerializationException() { def ex = new IOException('foo') s = new GsonSerializer() { @Override - protected byte[] writeValueAsBytes(Object o) throws IOException { + protected void doSerialize(Object o, OutputStream out) { throw ex } } @@ -78,7 +163,7 @@ class GsonSerializerTest { fail() } catch (SerializationException expected) { String causeMsg = 'foo' - String msg = "Unable to serialize object: $causeMsg" + String msg = "Unable to serialize object of type java.lang.Object: $causeMsg" assertEquals causeMsg, expected.cause.message assertEquals msg, expected.message } diff --git a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonWriterTest.groovy b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonWriterTest.groovy deleted file mode 100644 index 7b18d2599..000000000 --- a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonWriterTest.groovy +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright © 2023 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.gson.io - -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import io.jsonwebtoken.io.Writer -import io.jsonwebtoken.lang.Strings -import io.jsonwebtoken.lang.Supplier -import org.junit.Before -import org.junit.Test - -import java.nio.charset.StandardCharsets - -import static org.junit.Assert.* - -class GsonWriterTest { - - private GsonWriter writer - - @Before - void setUp() { - writer = new GsonWriter() - } - - private byte[] bytes(def o) { - ByteArrayOutputStream out = new ByteArrayOutputStream() - java.io.Writer w = new OutputStreamWriter(out, StandardCharsets.UTF_8) - writer.write(w, o) - w.close() - return out.toByteArray() - } - - private String json(def o) { - return Strings.utf8(bytes(o)) - } - - @Test - void loadService() { - def writer = ServiceLoader.load(Writer).iterator().next() - assertTrue writer instanceof GsonWriter - } - - @Test - void testDefaultConstructor() { - assertNotNull writer.gson - } - - @Test - void testGsonConstructor() { - def customGSON = new GsonBuilder() - .registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE) - .disableHtmlEscaping().create() - def writer = new GsonWriter<>(customGSON) - assertSame customGSON, writer.gson - } - - @Test - void testGsonConstructorWithIncorrectlyConfiguredGson() { - try { - //noinspection GroovyResultOfObjectAllocationIgnored - new GsonWriter<>(new Gson()) - fail() - } catch (IllegalArgumentException expected) { - String msg = 'Invalid Gson instance - it has not been registered with the necessary ' + - 'io.jsonwebtoken.lang.Supplier type adapter. When using the GsonBuilder, ensure this type ' + - 'adapter is registered by calling ' + - 'gsonBuilder.registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, ' + - 'io.jsonwebtoken.gson.io.GsonSupplierSerializer.INSTANCE) before calling gsonBuilder.create()' - assertEquals msg, expected.message - } - } - - @Test(expected = IllegalArgumentException) - void testConstructorWithNullArgument() { - new GsonWriter<>(null) - } - - @Test - void testByte() { - byte[] expected = Strings.utf8("120") //ascii("x") = 120 - byte[] result = bytes(Strings.utf8("x")[0]) //single byte - assertArrayEquals expected, result - } - - @Test - void testByteArray() { //expect Base64 string by default: - String expected = '"aGk="' as String //base64(hi) --> aGk= - assertEquals expected, json(Strings.utf8('hi')) - } - - @Test - void testEmptyByteArray() { //expect Base64 string by default: - byte[] result = bytes(new byte[0]) - assertEquals '""', Strings.utf8(result) - } - - @Test - void testChar() { //expect Base64 string by default: - assertEquals '"h"', json('h' as char) - } - - @Test - void testCharArray() { //expect string by default: - assertEquals '"hi"', json('hi'.toCharArray()) - } - - @Test - void testWrite() { - assertEquals '{"hello":"世界"}', json([hello: '世界']) - } - - - @Test - void testWriteFailure() { - def ex = new IOException('foo') - writer = new GsonWriter() { - @Override - protected void writeValue(Object o, java.io.Writer writer) { - throw ex - } - } - try { - json([hello: 'world']) - fail() - } catch (IOException expected) { - String msg = 'Unable to serialize object of type java.util.LinkedHashMap: foo' - assertEquals msg, expected.message - assertSame ex, expected.cause - } - } -} 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 2f32ef2b5..b32cc3a30 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,33 +15,35 @@ */ package io.jsonwebtoken.jackson.io; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.io.DeserializationException; -import io.jsonwebtoken.io.Deserializer; +import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import io.jsonwebtoken.io.AbstractDeserializer; import io.jsonwebtoken.lang.Assert; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.StandardCharsets; +import java.io.InputStream; +import java.util.Collections; import java.util.Map; /** * Deserializer using a Jackson {@link ObjectMapper}. * * @since 0.10.0 - * @deprecated since JJWT_RELEASE_VERSION in favor of {@link JacksonReader} */ -@SuppressWarnings("DeprecatedIsStillUsed") -@Deprecated -public class JacksonDeserializer extends JacksonReader implements Deserializer { +public class JacksonDeserializer extends AbstractDeserializer { + + private final Class returnType; + + private final ObjectMapper objectMapper; /** * Constructor using JJWT's default {@link ObjectMapper} singleton for deserialization. */ public JacksonDeserializer() { - super(); + this(JacksonSerializer.DEFAULT_OBJECT_MAPPER); } /** @@ -70,7 +72,14 @@ public JacksonDeserializer() { * @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type */ public JacksonDeserializer(Map> claimTypeMap) { - super(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); } /** @@ -78,32 +87,47 @@ public JacksonDeserializer(Map> claimTypeMap) { * * @param objectMapper the ObjectMapper to use for deserialization. */ + @SuppressWarnings("unchecked") public JacksonDeserializer(ObjectMapper objectMapper) { - super(objectMapper); + this(objectMapper, (Class) Object.class); + } + + private JacksonDeserializer(ObjectMapper objectMapper, Class returnType) { + Assert.notNull(objectMapper, "ObjectMapper cannot be null."); + Assert.notNull(returnType, "Return type cannot be null."); + this.objectMapper = objectMapper; + this.returnType = returnType; } - @SuppressWarnings("deprecation") @Override - public T deserialize(byte[] bytes) throws DeserializationException { - try { - return readValue(bytes); - } catch (IOException e) { - String msg = "Unable to deserialize JSON bytes: " + e.getMessage(); - throw new DeserializationException(msg, e); - } + protected T doDeserialize(InputStream in) throws Exception { + return objectMapper.readValue(in, returnType); } /** - * Converts the specified byte array value to the desired typed instance using the Jackson {@link ObjectMapper}. - * - * @param bytes the byte array value to convert - * @return the desired typed instance - * @throws IOException if there is a problem during reading or instance creation + * A Jackson {@link com.fasterxml.jackson.databind.JsonDeserializer JsonDeserializer}, that will convert claim + * values to types based on {@code claimTypeMap}. */ - protected T readValue(byte[] bytes) throws IOException { - Assert.notNull(bytes, "byte array argument cannot be null."); - try (Reader reader = new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8)) { - return read(reader); + 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 + String name = parser.currentName(); + if (claimTypeMap != null && name != null && claimTypeMap.containsKey(name)) { + Class type = claimTypeMap.get(name); + //noinspection resource + return parser.readValueAsTree().traverse(parser.getCodec()).readValueAs(type); + } + // otherwise default to super + return super.deserialize(parser, context); } } } diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonReader.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonReader.java deleted file mode 100644 index 644a49e79..000000000 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonReader.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright © 2023 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; - -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.Reader; -import io.jsonwebtoken.lang.Assert; - -import java.io.IOException; -import java.util.Collections; -import java.util.Map; - -public class JacksonReader implements Reader { - - private final Class returnType; - - private final ObjectMapper objectMapper; - - /** - * Constructor using JJWT's default {@link ObjectMapper} singleton for deserialization. - */ - public JacksonReader() { - this(JacksonWriter.DEFAULT_OBJECT_MAPPER); - } - - /** - * Creates a new JacksonReader 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 #JacksonReader(ObjectMapper)} constructor instead. - * - * @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type - */ - public JacksonReader(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); - } - - /** - * Constructor using the specified Jackson {@link ObjectMapper}. - * - * @param objectMapper the ObjectMapper to use for deserialization. - */ - @SuppressWarnings("unchecked") - public JacksonReader(ObjectMapper objectMapper) { - this(objectMapper, (Class) Object.class); - } - - private JacksonReader(ObjectMapper objectMapper, Class returnType) { - Assert.notNull(objectMapper, "ObjectMapper cannot be null."); - Assert.notNull(returnType, "Return type cannot be null."); - this.objectMapper = objectMapper; - this.returnType = returnType; - } - - @Override - public T read(java.io.Reader in) throws IOException { - return objectMapper.readValue(in, 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 - String name = parser.currentName(); - if (claimTypeMap != null && name != null && claimTypeMap.containsKey(name)) { - Class type = claimTypeMap.get(name); - //noinspection resource - return parser.readValueAsTree().traverse(parser.getCodec()).readValueAs(type); - } - // otherwise default to super - return super.deserialize(parser, context); - } - } -} diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java index 489b6fb52..31c5ae00f 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java @@ -15,30 +15,41 @@ */ package io.jsonwebtoken.jackson.io; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.io.SerializationException; -import io.jsonwebtoken.io.Serializer; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.module.SimpleModule; +import io.jsonwebtoken.io.AbstractSerializer; import io.jsonwebtoken.lang.Assert; -import java.io.ByteArrayOutputStream; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; +import java.io.OutputStream; /** * Serializer using a Jackson {@link ObjectMapper}. * * @since 0.10.0 - * @deprecated since JJWT_RELEASE_VERSION in favor of {@link JacksonWriter} */ -@SuppressWarnings("DeprecatedIsStillUsed") -@Deprecated -public class JacksonSerializer extends JacksonWriter implements Serializer { +public class JacksonSerializer extends AbstractSerializer { + + static final String MODULE_ID = "jjwt-jackson"; + static final Module MODULE; + + static { + SimpleModule module = new SimpleModule(MODULE_ID); + module.addSerializer(JacksonSupplierSerializer.INSTANCE); + MODULE = module; + } + + static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper().registerModule(MODULE); + + protected final ObjectMapper objectMapper; /** * Constructor using JJWT's default {@link ObjectMapper} singleton for serialization. */ public JacksonSerializer() { - super(); + this(DEFAULT_OBJECT_MAPPER); } /** @@ -47,42 +58,14 @@ public JacksonSerializer() { * @param objectMapper the ObjectMapper to use for serialization. */ public JacksonSerializer(ObjectMapper objectMapper) { - super(objectMapper); + Assert.notNull(objectMapper, "ObjectMapper cannot be null."); + this.objectMapper = objectMapper.registerModule(MODULE); } - @SuppressWarnings("deprecation") @Override - public byte[] serialize(T t) throws SerializationException { - Assert.notNull(t, "Object to serialize cannot be null."); - try { - return writeValueAsBytes(t); - } catch (com.fasterxml.jackson.core.JsonProcessingException e) { - String msg = "Unable to serialize object: " + e.getMessage(); - throw new SerializationException(msg, e); - } - } - - /** - * Serializes the specified instance value to a byte array using the underlying Jackson {@link ObjectMapper}. - * - * @param t the instance to serialize to a byte array - * @return the byte array serialization of the specified instance - * @throws com.fasterxml.jackson.core.JsonProcessingException if there is a problem during serialization - */ - protected byte[] writeValueAsBytes(T t) throws com.fasterxml.jackson.core.JsonProcessingException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(256); - try (OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8)) { - write(writer, t); - } catch (Throwable ex) { - String msg = "Unable to write value as bytes: " + ex.getMessage(); - throw new JsonProcessingException(msg, ex); - } - return baos.toByteArray(); - } - - private static class JsonProcessingException extends com.fasterxml.jackson.core.JsonProcessingException { - protected JsonProcessingException(String msg, Throwable rootCause) { - super(msg, rootCause); - } + protected void doSerialize(T t, OutputStream out) throws Exception { + Assert.notNull(out, "OutputStream cannot be null."); + ObjectWriter writer = this.objectMapper.writer().without(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + writer.writeValue(out, t); } } diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonWriter.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonWriter.java deleted file mode 100644 index b1ff5a22f..000000000 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonWriter.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright © 2023 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; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.Module; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.module.SimpleModule; -import io.jsonwebtoken.lang.Assert; - -import java.io.IOException; -import java.io.Writer; - -public class JacksonWriter implements io.jsonwebtoken.io.Writer { - - static final String MODULE_ID = "jjwt-jackson"; - static final Module MODULE; - - static { - SimpleModule module = new SimpleModule(MODULE_ID); - module.addSerializer(JacksonSupplierSerializer.INSTANCE); - MODULE = module; - } - - static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper().registerModule(MODULE); - - protected final ObjectMapper objectMapper; - - /** - * Constructor using JJWT's default {@link ObjectMapper} singleton for serialization. - */ - public JacksonWriter() { - this(DEFAULT_OBJECT_MAPPER); - } - - /** - * Creates a new Jackson Serializer that uses the specified {@link ObjectMapper} for serialization. - * - * @param objectMapper the ObjectMapper to use for serialization. - */ - public JacksonWriter(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper cannot be null."); - this.objectMapper = objectMapper.registerModule(MODULE); - } - - @Override - public void write(Writer out, T t) throws IOException { - Assert.notNull(out, "Writer cannot be null."); - ObjectWriter writer = this.objectMapper.writer().without(JsonGenerator.Feature.AUTO_CLOSE_TARGET); - writer.writeValue(out, t); -// out.close(); -// byte[] array = writer.writeValueAsBytes(t); -// //this.objectMapper.writeValue(out, t); -// System.out.println("done"); - } -} diff --git a/extensions/jackson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Reader b/extensions/jackson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Reader deleted file mode 100644 index 4d9e842fa..000000000 --- a/extensions/jackson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Reader +++ /dev/null @@ -1 +0,0 @@ -io.jsonwebtoken.jackson.io.JacksonReader \ No newline at end of file diff --git a/extensions/jackson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Writer b/extensions/jackson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Writer deleted file mode 100644 index 6d059d0d5..000000000 --- a/extensions/jackson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Writer +++ /dev/null @@ -1 +0,0 @@ -io.jsonwebtoken.jackson.io.JacksonWriter \ No newline at end of file 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 0d4667983..3b4c30411 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 @@ -26,7 +26,6 @@ import io.jsonwebtoken.lang.Strings import org.junit.Before import org.junit.Test -import static org.hamcrest.CoreMatchers.instanceOf import static org.junit.Assert.* class JacksonDeserializerTest { @@ -41,27 +40,19 @@ class JacksonDeserializerTest { @Test void loadService() { def deserializer = ServiceLoader.load(Deserializer).iterator().next() - assertThat(deserializer, instanceOf(JacksonDeserializer)) + assertTrue deserializer instanceof JacksonDeserializer } @Test void testDefaultConstructor() { - def fields = deserializer.getClass().superclass.declaredFields as List - def field = fields.find { it.type == ObjectMapper } - field.accessible = true - ObjectMapper found = field.get(deserializer) as ObjectMapper - assertSame JacksonWriter.DEFAULT_OBJECT_MAPPER, found + assertSame JacksonSerializer.DEFAULT_OBJECT_MAPPER, deserializer.objectMapper } @Test void testObjectMapperConstructor() { def customOM = new ObjectMapper() - deserializer = new JacksonDeserializer(customOM) - def fields = deserializer.getClass().superclass.declaredFields as List - def field = fields.find { it.type == ObjectMapper } - field.accessible = true - ObjectMapper found = field.get(deserializer) as ObjectMapper - assertSame customOM, found + deserializer = new JacksonDeserializer<>(customOM) + assertSame customOM, deserializer.objectMapper } @Test(expected = IllegalArgumentException) @@ -71,9 +62,9 @@ class JacksonDeserializerTest { @Test void testDeserialize() { - byte[] serialized = '{"hello":"世界"}'.getBytes(Strings.UTF_8) + byte[] data = Strings.utf8('{"hello":"世界"}') def expected = [hello: '世界'] - def result = new JacksonDeserializer().deserialize(serialized) + def result = deserializer.deserialize(new ByteArrayInputStream(data)) assertEquals expected, result } @@ -127,7 +118,8 @@ class JacksonDeserializerTest { ) def expected = [oneKey: "oneValue", custom: expectedCustomBean] - def result = new JacksonDeserializer(Maps.of("custom", CustomBean).build()).deserialize(serialized) + def result = new JacksonDeserializer(Maps.of("custom", CustomBean).build()) + .deserialize(new ByteArrayInputStream(serialized)) assertEquals expected, result } @@ -162,7 +154,8 @@ class JacksonDeserializerTest { typeMap.put("custom", CustomBean) def deserializer = new JacksonDeserializer(typeMap) - def result = deserializer.deserialize('{"alg":"HS256"}'.getBytes("UTF-8")) + def ins = new ByteArrayInputStream(Strings.utf8('{"alg":"HS256"}')) + def result = deserializer.deserialize(ins) assertEquals(["alg": "HS256"], result) } @@ -172,21 +165,21 @@ class JacksonDeserializerTest { } @Test - void testDeserializeFailsWithJsonProcessingException() { + void testDeserializeFailsWithException() { def ex = new IOException('foo') deserializer = new JacksonDeserializer() { @Override - protected Object readValue(byte[] bytes) throws IOException { + protected Object doDeserialize(InputStream inputStream) throws Exception { throw ex } } try { - deserializer.deserialize(Strings.utf8('{"hello":"世界"}')) + deserializer.deserialize(new ByteArrayInputStream(Strings.utf8('{"hello":"世界"}'))) fail() } catch (DeserializationException se) { - String msg = 'Unable to deserialize JSON bytes: foo' + String msg = 'Unable to deserialize: foo' assertEquals msg, se.getMessage() assertSame ex, se.getCause() } diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonReaderTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonReaderTest.groovy deleted file mode 100644 index 93bdc78ad..000000000 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonReaderTest.groovy +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright © 2023 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 - -import com.fasterxml.jackson.databind.ObjectMapper -import io.jsonwebtoken.io.Encoders -import io.jsonwebtoken.io.IOException -import io.jsonwebtoken.io.Reader -import io.jsonwebtoken.jackson.io.stubs.CustomBean -import io.jsonwebtoken.lang.Maps -import io.jsonwebtoken.lang.Strings -import org.junit.Before -import org.junit.Test - -import java.nio.charset.StandardCharsets - -import static org.junit.Assert.* - -class JacksonReaderTest { - - static def bytesReader(byte[] bytes) { - return new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8) - } - - static def bytesReader(String s) { - return bytesReader(Strings.utf8(s)) - } - - private static String base64(String input) { - return Encoders.BASE64.encode(input.getBytes('UTF-8')) - } - - private JacksonReader reader - - @Before - void setUp() { - reader = new JacksonReader() - } - - @Test - void loadService() { - def reader = ServiceLoader.load(Reader).iterator().next() - assertTrue reader instanceof JacksonReader - } - - @Test - void testDefaultConstructor() { - assertSame JacksonWriter.DEFAULT_OBJECT_MAPPER, reader.objectMapper - } - - @Test - void testObjectMapperConstructor() { - def customOM = new ObjectMapper() - reader = new JacksonReader(customOM) - assertSame customOM, reader.objectMapper - } - - @Test(expected = IllegalArgumentException) - void testObjectMapperConstructorWithNullArgument() { - new JacksonReader((ObjectMapper) null) - } - - @Test - void testRead() { - byte[] data = Strings.utf8('{"hello":"世界"}') - def expected = [hello: '世界'] - def result = reader.read(bytesReader(data)) - assertEquals expected, result - } - - @Test - void testReadWithCustomObject() { - - long currentTime = System.currentTimeMillis() - - byte[] jsonBytes = Strings.utf8("""{ - "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')}" - } - } - } - """) - - 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 JacksonReader(Maps.of("custom", CustomBean).build()).read(bytesReader(jsonBytes)) - assertEquals expected, result - } - - /** - * For: https://github.com/jwtk/jjwt/issues/564 - */ - @Test - void testMappedTypeReaderWithMapNullCheck() { - - // mimic map implementations that do NOT allow for null keys, or containsKey(null) - Map typeMap = new HashMap() { - @Override - boolean containsKey(Object key) { - if (key == null) { - throw new NullPointerException("key is null, expected for this test") - } - return super.containsKey(key) - } - } - - // TODO: the following does NOT work with Java 1.7 - // when we stop supporting that version we can use a partial mock instead - // the `typeMap.put("custom", CustomBean)` line below results in an NPE, (only on 1.7) - -// Map typeMap = partialMockBuilder(HashMap) -// .addMockedMethod("containsKey") -// .createNiceMock() -// -// expect(typeMap.containsKey(null)).andThrow(new NullPointerException("key is null, expected for this test")) -// replay(typeMap) - - typeMap.put("custom", CustomBean) - - def jr = new JacksonReader(typeMap) - String json = '{"alg":"HS256"}' - def result = jr.read(bytesReader(json)) - assertEquals(["alg": "HS256"], result) - } - - @Test(expected = IllegalArgumentException) - void testNullClaimTypeMap() { - new JacksonReader((Map) null) - } - - @Test - void testReadFailsWithException() { - - def ex = new IOException('foo') - - reader = new JacksonReader() { - @Override - Object read(java.io.Reader reader) throws IOException { - throw ex - } - } - - try { - reader.read(bytesReader('{"hello":"世界"}')) - fail() - } catch (IOException expected) { - assertSame ex, expected - } - } -} 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 0f2594a06..0b620ae53 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 @@ -15,42 +15,46 @@ */ 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.io.Serializer import io.jsonwebtoken.lang.Strings import org.junit.Before import org.junit.Test -import static org.hamcrest.CoreMatchers.instanceOf +import static org.easymock.EasyMock.* import static org.junit.Assert.* class JacksonSerializerTest { - private JacksonSerializer serializer + private JacksonSerializer ser @Before void setUp() { - serializer = new JacksonSerializer() + ser = new JacksonSerializer() + } + + byte[] serialize(def value) { + def os = new ByteArrayOutputStream() + ser.serialize(value, os) + return os.toByteArray() } @Test void loadService() { def serializer = ServiceLoader.load(Serializer).iterator().next() - assertThat(serializer, instanceOf(JacksonSerializer)) + assertTrue serializer instanceof JacksonSerializer } @Test void testDefaultConstructor() { - assertSame JacksonWriter.DEFAULT_OBJECT_MAPPER, serializer.objectMapper + assertSame JacksonSerializer.DEFAULT_OBJECT_MAPPER, ser.objectMapper } @Test void testObjectMapperConstructor() { ObjectMapper customOM = new ObjectMapper() - def serializer = new JacksonSerializer(customOM) - assertSame customOM, serializer.objectMapper + ser = new JacksonSerializer(customOM) + assertSame customOM, ser.objectMapper } @Test(expected = IllegalArgumentException) @@ -58,28 +62,60 @@ class JacksonSerializerTest { new JacksonSerializer<>(null) } + @Test + void testObjectMapperConstructorAutoRegistersModule() { + ObjectMapper om = createMock(ObjectMapper) + expect(om.registerModule(same(JacksonSerializer.MODULE))).andReturn(om) + replay om + //noinspection GroovyResultOfObjectAllocationIgnored + new JacksonSerializer<>(om) + verify om + } + @Test void testSerialize() { byte[] expected = '{"hello":"世界"}'.getBytes(Strings.UTF_8) - byte[] result = new JacksonSerializer().serialize([hello: '世界']) + byte[] result = ser.serialize([hello: '世界']) assertTrue Arrays.equals(expected, result) } @Test - void testSerializeFailsWithJsonProcessingException() { - def ex = new IOException('foo') - def serializer = new JacksonSerializer() { - @Override - void write(Writer out, Object o) throws IOException { - throw ex - } - } - try { - serializer.serialize([hello: 'world']) - fail() - } catch (SerializationException se) { - assertEquals 'Unable to serialize object: Unable to write value as bytes: foo', se.getMessage() - assertTrue se.cause instanceof JsonProcessingException - } + void testByte() { + byte[] expected = Strings.utf8("120") //ascii("x") = 120 + byte[] bytes = Strings.utf8("x") + assertArrayEquals expected, serialize(bytes[0]) // single byte + } + + @Test + void testByteArray() { //expect Base64 string by default: + byte[] bytes = Strings.utf8("hi") + String expected = '"aGk="' as String //base64(hi) --> aGk= + assertEquals expected, Strings.utf8(serialize(bytes)) + } + + @Test + void testEmptyByteArray() { //expect Base64 string by default: + byte[] bytes = new byte[0] + byte[] result = serialize(bytes) + assertEquals '""', Strings.utf8(result) + } + + @Test + void testChar() { //expect Base64 string by default: + byte[] result = serialize('h' as char) + assertEquals "\"h\"", Strings.utf8(result) + } + + @Test + void testCharArray() { //expect Base64 string by default: + byte[] result = serialize('hi'.toCharArray()) + assertEquals "\"hi\"", Strings.utf8(result) + } + + @Test + void testWriteObject() { + byte[] expected = Strings.utf8('{"hello":"世界"}' as String) + byte[] result = serialize([hello: '世界']) + assertArrayEquals expected, result } } diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSupplierSerializerTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSupplierSerializerTest.groovy index 98fd0fb84..90399c95a 100644 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSupplierSerializerTest.groovy +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSupplierSerializerTest.groovy @@ -15,7 +15,7 @@ */ package io.jsonwebtoken.jackson.io - +import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.lang.Supplier import org.junit.Test @@ -25,29 +25,29 @@ class JacksonSupplierSerializerTest { @Test void testSupplierNullValue() { - def serializer = new JacksonWriter() + def serializer = new JacksonSerializer() def supplier = new Supplier() { @Override Object get() { return null } } - StringWriter w = new StringWriter(4) - serializer.write(w, supplier) - assertEquals 'null', w.toString() + ByteArrayOutputStream out = new ByteArrayOutputStream() + serializer.serialize(supplier, out) + assertEquals 'null', Strings.utf8(out.toByteArray()) } @Test void testSupplierStringValue() { - def serializer = new JacksonWriter() + def serializer = new JacksonSerializer() def supplier = new Supplier() { @Override Object get() { return 'hello' } } - StringWriter w = new StringWriter(7) - serializer.write(w, supplier) - assertEquals '"hello"', w.toString() + ByteArrayOutputStream out = new ByteArrayOutputStream() + serializer.serialize(supplier, out) + assertEquals '"hello"', Strings.utf8(out.toByteArray()) } } diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonWriterTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonWriterTest.groovy deleted file mode 100644 index 049ea5a04..000000000 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonWriterTest.groovy +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright © 2023 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 - -import com.fasterxml.jackson.databind.ObjectMapper -import io.jsonwebtoken.io.IOException -import io.jsonwebtoken.io.Writer -import io.jsonwebtoken.lang.Strings -import org.junit.Before -import org.junit.Test - -import static org.easymock.EasyMock.* -import static org.junit.Assert.* - -class JacksonWriterTest { - - private JacksonWriter writer - - @Before - void setUp() { - writer = new JacksonWriter() - } - - @Test - void loadService() { - def writer = ServiceLoader.load(Writer).iterator().next() - assertTrue writer instanceof JacksonWriter - } - - @Test - void testDefaultConstructor() { - assertSame JacksonWriter.DEFAULT_OBJECT_MAPPER, writer.objectMapper - } - - @Test - void testObjectMapperConstructor() { - def customOM = new ObjectMapper() - writer = new JacksonWriter(customOM) - assertSame customOM, writer.objectMapper - } - - @Test(expected = IllegalArgumentException) - void testObjectMapperConstructorWithNullArgument() { - new JacksonWriter(null) - } - - @Test - void testObjectMapperConstructorAutoRegistersModule() { - ObjectMapper om = createMock(ObjectMapper) - expect(om.registerModule(same(JacksonWriter.MODULE))).andReturn(om) - replay om - //noinspection GroovyResultOfObjectAllocationIgnored - new JacksonWriter(om) - verify om - } - - byte[] write(def value) { - def os = new ByteArrayOutputStream() - def osw = new OutputStreamWriter(os) - writer.write(osw, value) - osw.close() - return os.toByteArray() - } - - @Test - void testByte() { - byte[] expected = Strings.utf8("120") //ascii("x") = 120 - byte[] bytes = Strings.utf8("x") - assertArrayEquals expected, write(bytes[0]) // single byte - } - - @Test - void testByteArray() { //expect Base64 string by default: - byte[] bytes = Strings.utf8("hi") - String expected = '"aGk="' as String //base64(hi) --> aGk= - assertEquals expected, Strings.utf8(write(bytes)) - } - - @Test - void testEmptyByteArray() { //expect Base64 string by default: - byte[] bytes = new byte[0] - byte[] result = write(bytes) - assertEquals '""', Strings.utf8(result) - } - - @Test - void testChar() { //expect Base64 string by default: - byte[] result = write('h' as char) - assertEquals "\"h\"", Strings.utf8(result) - } - - @Test - void testCharArray() { //expect Base64 string by default: - byte[] result = write('hi'.toCharArray()) - assertEquals "\"hi\"", Strings.utf8(result) - } - - @Test - void testWriteObject() { - byte[] expected = Strings.utf8('{"hello":"世界"}' as String) - byte[] result = write([hello: '世界']) - assertArrayEquals expected, result - } - - @Test - void testWriteFailsWithJsonProcessingException() { - def ex = new IOException('foo') - writer = new JacksonWriter() { - @Override - void write(java.io.Writer out, Object o) throws java.io.IOException { - throw ex - } - } - try { - write([hello: 'world']) - fail() - } catch (IOException expected) { - assertSame ex, expected - } - } -} 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 index 6f78993d8..f046ec7d0 100644 --- 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 @@ -131,16 +131,16 @@ class CustomBean { @Override - public String toString() { + String toString() { return "CustomBean{" + "stringValue='" + stringValue + '\'' + ", intValue=" + intValue + - ", dateValue=" + dateValue?.time+ + ", dateValue=" + dateValue?.time + ", shortValue=" + shortValue + ", longValue=" + longValue + ", byteValue=" + byteValue + // ", byteArrayValue=" + Arrays.toString(byteArrayValue) + ", nestedValue=" + nestedValue + - '}'; + '}' } } diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java index 3f672dd71..7bb975c42 100644 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java +++ b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java @@ -15,34 +15,87 @@ */ package io.jsonwebtoken.orgjson.io; -import io.jsonwebtoken.io.DeserializationException; -import io.jsonwebtoken.io.Deserializer; -import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.io.AbstractDeserializer; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; -import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; /** * @since 0.10.0 - * @deprecated since JJWT_RELEASE_VERSION in favor of {@link OrgJsonReader} */ -@SuppressWarnings("DeprecatedIsStillUsed") -@Deprecated -public class OrgJsonDeserializer extends OrgJsonReader implements Deserializer { +public class OrgJsonDeserializer extends AbstractDeserializer { - @SuppressWarnings("deprecation") @Override - public Object deserialize(byte[] bytes) throws DeserializationException { - if (Objects.isEmpty(bytes)) { - throw new DeserializationException("Invalid JSON: null or zero length byte array."); + protected Object doDeserialize(InputStream in) { + Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8); + return parse(reader); + } + + private Object parse(java.io.Reader reader) throws JSONException { + + JSONTokener tokener = new JSONTokener(reader); + + char c = tokener.nextClean(); //peak ahead + tokener.back(); //revert + + if (c == '{') { //json object + JSONObject o = new JSONObject(tokener); + return toMap(o); + } else if (c == '[') { + JSONArray a = new JSONArray(tokener); + return toList(a); + } else { + //raw json value + Object value = tokener.nextValue(); + return convertIfNecessary(value); } - try (Reader reader = new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8)) { - return read(reader); - } catch (Throwable t) { - String msg = "Unable to deserialize JSON bytes: " + t.getMessage(); - throw new DeserializationException(msg, t); + } + + private Map toMap(JSONObject o) { + Map map = new LinkedHashMap<>(); + // https://github.com/jwtk/jjwt/issues/380: use .keys() and *not* .keySet() for Android compatibility: + Iterator iterator = o.keys(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = o.get(key); + value = convertIfNecessary(value); + map.put(key, value); + } + return map; + } + + private List toList(JSONArray a) { + int length = a.length(); + List list = new ArrayList<>(length); + // https://github.com/jwtk/jjwt/issues/380: use a.get(i) and *not* a.toList() for Android compatibility: + for (int i = 0; i < length; i++) { + Object value = a.get(i); + value = convertIfNecessary(value); + list.add(value); + } + return list; + } + + private Object convertIfNecessary(Object v) { + Object value = v; + if (JSONObject.NULL.equals(value)) { + value = null; + } else if (value instanceof JSONArray) { + value = toList((JSONArray) value); + } else if (value instanceof JSONObject) { + value = toMap((JSONObject) value); } + return value; } } diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonReader.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonReader.java deleted file mode 100644 index c5f68cda7..000000000 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonReader.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright © 2023 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.orgjson.io; - -import io.jsonwebtoken.io.Reader; -import io.jsonwebtoken.lang.Assert; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONTokener; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -public class OrgJsonReader implements Reader { - - @Override - public Object read(java.io.Reader in) throws IOException { - Assert.notNull(in, "Reader cannot be null."); - try { - return parse(in); - } catch (Throwable t) { - String msg = "Unable to read JSON: " + t.getMessage(); - throw new IOException(msg, t); - } - } - - private Object parse(java.io.Reader reader) throws JSONException { - - JSONTokener tokener = new JSONTokener(reader); - - char c = tokener.nextClean(); //peak ahead - tokener.back(); //revert - - if (c == '{') { //json object - JSONObject o = new JSONObject(tokener); - return toMap(o); - } else if (c == '[') { - JSONArray a = new JSONArray(tokener); - return toList(a); - } else { - //raw json value - Object value = tokener.nextValue(); - return convertIfNecessary(value); - } - } - - private Map toMap(JSONObject o) { - Map map = new LinkedHashMap<>(); - // https://github.com/jwtk/jjwt/issues/380: use .keys() and *not* .keySet() for Android compatibility: - Iterator iterator = o.keys(); - while (iterator.hasNext()) { - String key = iterator.next(); - Object value = o.get(key); - value = convertIfNecessary(value); - map.put(key, value); - } - return map; - } - - private List toList(JSONArray a) { - int length = a.length(); - List list = new ArrayList<>(length); - // https://github.com/jwtk/jjwt/issues/380: use a.get(i) and *not* a.toList() for Android compatibility: - for (int i = 0; i < length; i++) { - Object value = a.get(i); - value = convertIfNecessary(value); - list.add(value); - } - return list; - } - - private Object convertIfNecessary(Object v) { - Object value = v; - if (JSONObject.NULL.equals(value)) { - value = null; - } else if (value instanceof JSONArray) { - value = toList((JSONArray) value); - } else if (value instanceof JSONObject) { - value = toMap((JSONObject) value); - } - return value; - } -} diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java index b57b48d37..0c81f5ce4 100644 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java +++ b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java @@ -15,35 +15,170 @@ */ package io.jsonwebtoken.orgjson.io; -import io.jsonwebtoken.io.SerializationException; -import io.jsonwebtoken.io.Serializer; +import io.jsonwebtoken.io.AbstractSerializer; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Classes; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.DateFormats; +import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.lang.Supplier; +import org.json.JSONArray; +import org.json.JSONObject; -import java.io.ByteArrayOutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.nio.charset.StandardCharsets; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Map; /** * @since 0.10.0 - * @deprecated since JJWT_RELEASE_VERSION in favor of {@link OrgJsonWriter} */ -@SuppressWarnings("DeprecatedIsStillUsed") -@Deprecated -public class OrgJsonSerializer extends OrgJsonWriter implements Serializer { +public class OrgJsonSerializer extends AbstractSerializer { + + // we need reflection for these because of Android - see https://github.com/jwtk/jjwt/issues/388 + private static final String JSON_WRITER_CLASS_NAME = "org.json.JSONWriter"; + private static final Class[] VALUE_TO_STRING_ARG_TYPES = new Class[]{Object.class}; + private static final String JSON_STRING_CLASS_NAME = "org.json.JSONString"; + private static final Class JSON_STRING_CLASS; + + static { // see see https://github.com/jwtk/jjwt/issues/388 + if (Classes.isAvailable(JSON_STRING_CLASS_NAME)) { + JSON_STRING_CLASS = Classes.forName(JSON_STRING_CLASS_NAME); + } else { + JSON_STRING_CLASS = null; + } + } - @SuppressWarnings("deprecation") @Override - public byte[] serialize(T object) throws SerializationException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(256); - try (Writer writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8)) { - write(writer, object); - } catch (Throwable t) { - String msg = "Unable to serialize object of type " + object.getClass().getName() + - " to JSON: " + t.getMessage(); - throw new SerializationException(msg, t); + protected void doSerialize(T t, OutputStream out) throws Exception { + Object o = toJSONInstance(t); + String s = toString(o); + byte[] bytes = Strings.utf8(s); + out.write(bytes); + } + + /** + * @since 0.10.5 see https://github.com/jwtk/jjwt/issues/388 + */ + private static boolean isJSONString(Object o) { + if (JSON_STRING_CLASS != null) { + return JSON_STRING_CLASS.isInstance(o); + } + return false; + } + + private Object toJSONInstance(Object object) throws IOException { + + if (object == null) { + return JSONObject.NULL; + } + + if (object instanceof Supplier) { + object = ((Supplier) object).get(); + } + + if (object instanceof JSONObject || object instanceof JSONArray + || JSONObject.NULL.equals(object) || isJSONString(object) + || object instanceof Byte || object instanceof Character + || object instanceof Short || object instanceof Integer + || object instanceof Long || object instanceof Boolean + || object instanceof Float || object instanceof Double + || object instanceof String || object instanceof BigInteger + || object instanceof BigDecimal || object instanceof Enum) { + return object; + } + + if (object instanceof Calendar) { + object = ((Calendar) object).getTime(); //sets object to date, will be converted in next if-statement: + } + + if (object instanceof Date) { + Date date = (Date) object; + return DateFormats.formatIso8601(date); + } + + if (object instanceof byte[]) { + return Encoders.BASE64.encode((byte[]) object); + } + + if (object instanceof char[]) { + return new String((char[]) object); + } + + if (object instanceof Map) { + Map map = (Map) object; + return toJSONObject(map); + } + + if (Objects.isArray(object)) { + object = Collections.arrayToList(object); //sets object to List, will be converted in next if-statement: + } + + if (object instanceof Collection) { + Collection coll = (Collection) object; + return toJSONArray(coll); + } + + //not an immediately JSON-compatible object and probably a JavaBean (or similar). We can't convert that + //directly without using a marshaller of some sort: + String msg = "Unable to serialize object of type " + object.getClass().getName() + " to JSON using known heuristics."; + throw new IOException(msg); + } + + private JSONObject toJSONObject(Map m) throws IOException { + + JSONObject obj = new JSONObject(); + + for (Map.Entry entry : m.entrySet()) { + Object k = entry.getKey(); + Object value = entry.getValue(); + + String key = String.valueOf(k); + value = toJSONInstance(value); + obj.put(key, value); + } + + return obj; + } + + private JSONArray toJSONArray(Collection c) throws IOException { + + JSONArray array = new JSONArray(); + + for (Object o : c) { + o = toJSONInstance(o); + array.put(o); + } + + return array; + } + + /** + * Serializes the specified org.json instance a JSON String. + * + * @param o the org.json instance to convert to a String + * @return the JSON String + */ + protected String toString(Object o) { + // https://github.com/jwtk/jjwt/issues/380 for Android compatibility (Android doesn't have org.json.JSONWriter): + // This instanceof check is a sneaky (hacky?) heuristic: A JwtBuilder only ever provides Map + // instances to its serializer instances, so by the time this method is invoked, 'o' will always be a + // JSONObject. + // + // This is sufficient for all JJWT-supported scenarios on Android since Android users shouldn't ever use + // JJWT's internal Serializer implementation for general JSON serialization. That is, its intended use + // is within the context of JwtBuilder execution and not for application use beyond that. + if (o instanceof JSONObject) { + return o.toString(); } - return baos.toByteArray(); + // we still call JSONWriter for all other values 'just in case', and this works for all valid JSON values + // This would fail on Android unless they include the newer org.json dependency and ignore Android's. + return Classes.invokeStatic(JSON_WRITER_CLASS_NAME, "valueToString", VALUE_TO_STRING_ARG_TYPES, o); } /** @@ -55,7 +190,7 @@ public byte[] serialize(T object) throws SerializationException { */ @Deprecated protected byte[] toBytes(Object o) { - String s = super.toString(o); - return s.getBytes(Strings.UTF_8); + String s = toString(o); + return Strings.utf8(s); } } diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonWriter.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonWriter.java deleted file mode 100644 index bc72ab4e1..000000000 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonWriter.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright © 2023 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.orgjson.io; - -import io.jsonwebtoken.io.Encoders; -import io.jsonwebtoken.io.Writer; -import io.jsonwebtoken.lang.Classes; -import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.lang.DateFormats; -import io.jsonwebtoken.lang.Objects; -import io.jsonwebtoken.lang.Supplier; -import org.json.JSONArray; -import org.json.JSONObject; - -import java.io.IOException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; -import java.util.Map; - -public class OrgJsonWriter implements Writer { - - // we need reflection for these because of Android - see https://github.com/jwtk/jjwt/issues/388 - private static final String JSON_WRITER_CLASS_NAME = "org.json.JSONWriter"; - private static final Class[] VALUE_TO_STRING_ARG_TYPES = new Class[]{Object.class}; - private static final String JSON_STRING_CLASS_NAME = "org.json.JSONString"; - private static final Class JSON_STRING_CLASS; - - static { // see see https://github.com/jwtk/jjwt/issues/388 - if (Classes.isAvailable(JSON_STRING_CLASS_NAME)) { - JSON_STRING_CLASS = Classes.forName(JSON_STRING_CLASS_NAME); - } else { - JSON_STRING_CLASS = null; - } - } - - @Override - public void write(java.io.Writer out, T t) throws IOException { - try { - writeObject(out, t); - } catch (IOException ioe) { - throw ioe; // propagate - } catch (Throwable ex) { - String msg = "Cannot write object of type " + Objects.nullSafeClassName(t) + " to JSON: " + ex.getMessage(); - throw new IOException(msg, ex); - } - } - - protected void writeObject(java.io.Writer out, T t) throws IOException { - Object o = toJSONInstance(t); - String s = toString(o); - out.write(s); - } - - /** - * @since 0.10.5 see https://github.com/jwtk/jjwt/issues/388 - */ - private static boolean isJSONString(Object o) { - if (JSON_STRING_CLASS != null) { - return JSON_STRING_CLASS.isInstance(o); - } - return false; - } - - private Object toJSONInstance(Object object) throws IOException { - - if (object == null) { - return JSONObject.NULL; - } - - if (object instanceof Supplier) { - object = ((Supplier) object).get(); - } - - if (object instanceof JSONObject || object instanceof JSONArray - || JSONObject.NULL.equals(object) || isJSONString(object) - || object instanceof Byte || object instanceof Character - || object instanceof Short || object instanceof Integer - || object instanceof Long || object instanceof Boolean - || object instanceof Float || object instanceof Double - || object instanceof String || object instanceof BigInteger - || object instanceof BigDecimal || object instanceof Enum) { - return object; - } - - if (object instanceof Calendar) { - object = ((Calendar) object).getTime(); //sets object to date, will be converted in next if-statement: - } - - if (object instanceof Date) { - Date date = (Date) object; - return DateFormats.formatIso8601(date); - } - - if (object instanceof byte[]) { - return Encoders.BASE64.encode((byte[]) object); - } - - if (object instanceof char[]) { - return new String((char[]) object); - } - - if (object instanceof Map) { - Map map = (Map) object; - return toJSONObject(map); - } - - if (Objects.isArray(object)) { - object = Collections.arrayToList(object); //sets object to List, will be converted in next if-statement: - } - - if (object instanceof Collection) { - Collection coll = (Collection) object; - return toJSONArray(coll); - } - - //not an immediately JSON-compatible object and probably a JavaBean (or similar). We can't convert that - //directly without using a marshaller of some sort: - String msg = "Unable to serialize object of type " + object.getClass().getName() + " to JSON using known heuristics."; - throw new IOException(msg); - } - - private JSONObject toJSONObject(Map m) throws IOException { - - JSONObject obj = new JSONObject(); - - for (Map.Entry entry : m.entrySet()) { - Object k = entry.getKey(); - Object value = entry.getValue(); - - String key = String.valueOf(k); - value = toJSONInstance(value); - obj.put(key, value); - } - - return obj; - } - - private JSONArray toJSONArray(Collection c) throws IOException { - - JSONArray array = new JSONArray(); - - for (Object o : c) { - o = toJSONInstance(o); - array.put(o); - } - - return array; - } - - /** - * Serializes the specified org.json instance a JSON String. - * - * @param o the org.json instance to convert to a String - * @return the JSON String - */ - protected String toString(Object o) { - // https://github.com/jwtk/jjwt/issues/380 for Android compatibility (Android doesn't have org.json.JSONWriter): - // This instanceof check is a sneaky (hacky?) heuristic: A JwtBuilder only ever provides Map - // instances to its serializer instances, so by the time this method is invoked, 'o' will always be a - // JSONObject. - // - // This is sufficient for all JJWT-supported scenarios on Android since Android users shouldn't ever use - // JJWT's internal Serializer implementation for general JSON serialization. That is, its intended use - // is within the context of JwtBuilder execution and not for application use beyond that. - if (o instanceof JSONObject) { - return o.toString(); - } - // we still call JSONWriter for all other values 'just in case', and this works for all valid JSON values - // This would fail on Android unless they include the newer org.json dependency and ignore Android's. - return Classes.invokeStatic(JSON_WRITER_CLASS_NAME, "valueToString", VALUE_TO_STRING_ARG_TYPES, o); - } -} diff --git a/extensions/orgjson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Reader b/extensions/orgjson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Reader deleted file mode 100644 index 2e3d30b39..000000000 --- a/extensions/orgjson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Reader +++ /dev/null @@ -1 +0,0 @@ -io.jsonwebtoken.orgjson.io.OrgJsonReader \ No newline at end of file diff --git a/extensions/orgjson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Writer b/extensions/orgjson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Writer deleted file mode 100644 index c0ecfc909..000000000 --- a/extensions/orgjson/src/main/resources/META-INF/services/io.jsonwebtoken.io.Writer +++ /dev/null @@ -1 +0,0 @@ -io.jsonwebtoken.orgjson.io.OrgJsonWriter \ No newline at end of file diff --git a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/AndroidOrgJsonSerializerTest.groovy b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/AndroidOrgJsonSerializerTest.groovy index 3224dde7b..d47af2327 100644 --- a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/AndroidOrgJsonSerializerTest.groovy +++ b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/AndroidOrgJsonSerializerTest.groovy @@ -39,7 +39,7 @@ class AndroidOrgJsonSerializerTest { replay Classes - assertFalse OrgJsonWriter.isJSONString('foo') + assertFalse OrgJsonSerializer.isJSONString('foo') verify Classes } diff --git a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy index d426353f5..1273afbf2 100644 --- a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy +++ b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy @@ -22,16 +22,125 @@ import io.jsonwebtoken.lang.Strings import org.junit.Before import org.junit.Test -import static org.junit.Assert.assertEquals -import static org.junit.Assert.fail +import static org.junit.Assert.* class OrgJsonDeserializerTest { - private OrgJsonDeserializer deserializer + private OrgJsonDeserializer des + + private Object fromBytes(byte[] data) { + return des.deserialize(new ByteArrayInputStream(data)) + } + + private Object read(String s) { + return fromBytes(Strings.utf8(s)) + } + + @Test(expected = IllegalArgumentException) + void testNullArgument() { + des.deserialize((InputStream) null) + } + + @Test(expected = DeserializationException) + void testEmptyByteArray() { + fromBytes(new byte[0]) + } + + @Test(expected = DeserializationException) + void testInvalidJson() { + read('"') + } + + @Test + void testLiteralNull() { + assertNull read('null') + } + + @Test + void testLiteralTrue() { + assertTrue read('true') as boolean + } + + @Test + void testLiteralFalse() { + assertFalse read('false') as boolean + } + + @Test + void testLiteralInteger() { + assertEquals 1 as Integer, read('1') + } + + @Test + void testLiteralDecimal() { + assertEquals 3.14159 as Double, read('3.14159') as BigDecimal, 0d + } + + @Test + void testEmptyArray() { + def value = read('[]') + assert value instanceof List + assertEquals 0, value.size() + } + + @Test + void testSimpleArray() { + def value = read('[1, 2]') + assert value instanceof List + def expected = [1, 2] + assertEquals expected, value + } + + @Test + void testArrayWithNullElements() { + def value = read('[1, null, 3]') + assert value instanceof List + def expected = [1, null, 3] + assertEquals expected, value + } + + @Test + void testEmptyObject() { + def value = read('{}') + assert value instanceof Map + assertEquals 0, value.size() + } + + @Test + void testSimpleObject() { + def value = read('{"hello": "世界"}') + assert value instanceof Map + def expected = [hello: '世界'] + assertEquals expected, value + } + + @Test + void testObjectWithKeyHavingNullValue() { + def value = read('{"hello": "世界", "test": null}') + assert value instanceof Map + def expected = [hello: '世界', test: null] + assertEquals expected, value + } + + @Test + void testObjectWithKeyHavingArrayValue() { + def value = read('{"hello": "世界", "test": [1, 2]}') + assert value instanceof Map + def expected = [hello: '世界', test: [1, 2]] + assertEquals expected, value + } + + @Test + void testObjectWithKeyHavingObjectValue() { + def value = read('{"hello": "世界", "test": {"foo": "bar"}}') + assert value instanceof Map + def expected = [hello: '世界', test: [foo: 'bar']] + assertEquals expected, value + } @Before void setUp() { - deserializer = new OrgJsonDeserializer() + des = new OrgJsonDeserializer() } @Test @@ -43,17 +152,17 @@ class OrgJsonDeserializerTest { @Test void deserialize() { def m = [hello: 42] - assertEquals m, deserializer.deserialize(Strings.utf8('{"hello":42}')) + assertEquals m, des.deserialize(Strings.utf8('{"hello":42}')) } - @Test(expected = DeserializationException) + @Test(expected = IllegalArgumentException) void deserializeNull() { - deserializer.deserialize(null) + des.deserialize((InputStream) null) } @Test(expected = DeserializationException) void deserializeEmpty() { - deserializer.deserialize(new byte[0]) + read('') } @Test @@ -61,18 +170,18 @@ class OrgJsonDeserializerTest { def t = new Throwable("foo") - deserializer = new OrgJsonDeserializer() { + des = new OrgJsonDeserializer() { @Override - Object read(Reader reader) throws IOException { + protected Object doDeserialize(InputStream inputStream) { throw t } } try { - deserializer.deserialize(Strings.utf8('whatever')) + des.deserialize(Strings.utf8('whatever')) fail() } catch (DeserializationException expected) { - String msg = 'Unable to deserialize JSON bytes: foo' + String msg = 'Unable to deserialize: foo' assertEquals msg, expected.message } } diff --git a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonReaderTest.groovy b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonReaderTest.groovy deleted file mode 100644 index a2e104053..000000000 --- a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonReaderTest.groovy +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright © 2023 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.orgjson.io - -import io.jsonwebtoken.io.Reader -import io.jsonwebtoken.lang.Strings -import org.junit.Before -import org.junit.Test - -import java.nio.charset.StandardCharsets - -import static org.junit.Assert.* - -class OrgJsonReaderTest { - - static InputStreamReader bytesReader(byte[] bytes) { - return new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8) - } - - private Reader reader - - private Object fromBytes(byte[] data) { - InputStreamReader r = bytesReader(data) - return reader.read(r) - } - - private Object read(String s) { - return fromBytes(Strings.utf8(s)) - } - - @Before - void setUp() { - reader = new OrgJsonReader() - } - - @Test - void loadService() { - def reader = ServiceLoader.load(Reader).iterator().next() - assertTrue reader instanceof OrgJsonReader - } - - @Test(expected = IllegalArgumentException) - void testNullArgument() { - reader.read(null) - } - - @Test(expected = IOException) - void testEmptyByteArray() { - fromBytes(new byte[0]) - } - - @Test(expected = IOException) - void testInvalidJson() { - read('"') - } - - @Test - void testLiteralNull() { - assertNull read('null') - } - - @Test - void testLiteralTrue() { - assertTrue read('true') as boolean - } - - @Test - void testLiteralFalse() { - assertFalse read('false') as boolean - } - - @Test - void testLiteralInteger() { - assertEquals 1 as Integer, read('1') - } - - @Test - void testLiteralDecimal() { - assertEquals 3.14159 as Double, read('3.14159') as BigDecimal, 0d - } - - @Test - void testEmptyArray() { - def value = read('[]') - assert value instanceof List - assertEquals 0, value.size() - } - - @Test - void testSimpleArray() { - def value = read('[1, 2]') - assert value instanceof List - def expected = [1, 2] - assertEquals expected, value - } - - @Test - void testArrayWithNullElements() { - def value = read('[1, null, 3]') - assert value instanceof List - def expected = [1, null, 3] - assertEquals expected, value - } - - @Test - void testEmptyObject() { - def value = read('{}') - assert value instanceof Map - assertEquals 0, value.size() - } - - @Test - void testSimpleObject() { - def value = read('{"hello": "世界"}') - assert value instanceof Map - def expected = [hello: '世界'] - assertEquals expected, value - } - - @Test - void testObjectWithKeyHavingNullValue() { - def value = read('{"hello": "世界", "test": null}') - assert value instanceof Map - def expected = [hello: '世界', test: null] - assertEquals expected, value - } - - @Test - void testObjectWithKeyHavingArrayValue() { - def value = read('{"hello": "世界", "test": [1, 2]}') - assert value instanceof Map - def expected = [hello: '世界', test: [1, 2]] - assertEquals expected, value - } - - @Test - void testObjectWithKeyHavingObjectValue() { - def value = read('{"hello": "世界", "test": {"foo": "bar"}}') - assert value instanceof Map - def expected = [hello: '世界', test: [foo: 'bar']] - assertEquals expected, value - } -} diff --git a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonSerializerTest.groovy b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonSerializerTest.groovy index d18b59a69..382c5bb14 100644 --- a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonSerializerTest.groovy +++ b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonSerializerTest.groovy @@ -16,13 +16,17 @@ //file:noinspection GrDeprecatedAPIUsage package io.jsonwebtoken.orgjson.io +import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.lang.DateFormats import io.jsonwebtoken.lang.Strings +import io.jsonwebtoken.lang.Supplier +import org.json.JSONObject +import org.json.JSONString import org.junit.Before import org.junit.Test -import static org.hamcrest.CoreMatchers.instanceOf import static org.junit.Assert.* class OrgJsonSerializerTest { @@ -41,7 +45,234 @@ class OrgJsonSerializerTest { @Test void loadService() { def serializer = ServiceLoader.load(Serializer).iterator().next() - assertThat(serializer, instanceOf(OrgJsonSerializer)) + assertTrue serializer instanceof OrgJsonSerializer + } + + @Test + void testInvalidArgument() { + try { + ser(new Object()) + fail() + } catch (SerializationException expected) { + String causeMsg = 'Unable to serialize object of type java.lang.Object to JSON using known heuristics.' + String msg = "Unable to serialize object of type java.lang.Object: $causeMsg" + assertEquals msg, expected.message + } + } + + @Test + void testNull() { + assertEquals 'null', ser(null) + } + + @Test + void testJSONObjectNull() { + assertEquals 'null', ser(JSONObject.NULL) + } + + @Test + void testJSONString() { + def jsonString = new JSONString() { + @Override + String toJSONString() { + return '"foo"' + } + } + assertEquals '"foo"', ser(jsonString) + } + + @Test + void testTrue() { + assertEquals 'true', ser(Boolean.TRUE) + } + + @Test + void testFalse() { + assertEquals 'false', ser(Boolean.FALSE) + } + + @Test + void testByte() { + assertEquals '120', ser("x".getBytes(Strings.UTF_8)[0]) //ascii("x") == 120 + } + + @Test + void testByteArray() { //expect Base64 string by default: + byte[] bytes = "hi".getBytes(Strings.UTF_8) + String expected = '"aGk="' as String //base64(hi) --> aGk= + assertEquals expected, ser(bytes) + } + + @Test + void testEmptyByteArray() { //base64 --> zero bytes == zero-length string: + assertEquals "\"\"", ser(new byte[0]) + } + + @Test + void testChar() { + assertEquals "\"h\"", ser('h' as char) + } + + @Test + void testCharArray() { + assertEquals "\"hi\"", ser("hi".toCharArray()) + } + + @Test + void testEmptyCharArray() { //no chars == empty string: + assertEquals "\"\"", ser(new char[0]) + } + + @Test + void testShort() { + assertEquals '8', ser(8 as short) + } + + @Test + void testInteger() { + assertEquals '1', ser(1 as Integer) + } + + @Test + void testLong() { + assertEquals '42', ser(42 as Long) + } + + @Test + void testBigInteger() { + assertEquals '42', ser(BigInteger.valueOf(42 as Long)) + } + + @Test + void testFloat() { + assertEquals '3.14159', ser(3.14159 as Float) + } + + @Test + void testDouble() { + assertEquals '3.14159', ser(3.14159 as Double) + } + + @Test + void testBigDecimal() { + assertEquals '3.14159', ser(BigDecimal.valueOf(3.14159 as Double)) + } + + @Test + void testEnum() { + assertEquals '"HS256"', ser(SignatureAlgorithm.HS256) + } + + @Test + void testSupplier() { + def supplier = new Supplier() { + @Override + Object get() { + return 'test' + } + } + assertEquals '"test"', ser(supplier) + } + + @Test + void testEmptyString() { + assertEquals '""', ser('') + } + + @Test + void testWhitespaceString() { + String value = " \n\r\t " + assertEquals '" \\n\\r\\t "' as String, ser(value) + } + + @Test + void testSimpleString() { + assertEquals '"hello 世界"', ser('hello 世界') + } + + @Test + void testDate() { + Date now = new Date() + String formatted = DateFormats.formatIso8601(now) + assertEquals "\"$formatted\"" as String, ser(now) + } + + @Test + void testCalendar() { + def cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + def now = cal.getTime() + String formatted = DateFormats.formatIso8601(now) + assertEquals "\"$formatted\"" as String, ser(cal) + } + + @Test + void testSimpleIntArray() { + assertEquals '[1,2]', ser([1, 2] as int[]) + } + + @Test + void testIntegerArrayWithNullElements() { + assertEquals '[1,null]', ser([1, null] as Integer[]) + } + + @Test + void testIntegerList() { + assertEquals '[1,2]', ser([1, 2] as List) + } + + @Test + void testEmptyObject() { + assertEquals '{}', ser([:]) + } + + @Test + void testSimpleObject() { + assertEquals '{"hello":"世界"}', ser([hello: '世界']) + } + + @Test + void testObjectWithKeyHavingNullValue() { + //depending on the test platform, and that JSON doesn't require members to be ordered, either of the + //two strings are fine (they're the same data, just the member order is different): + String acceptable1 = '{"hello":"世界","test":null}' + String acceptable2 = '{"test":null,"hello":"世界"}' + String result = ser([test: null, hello: '世界']) + assertTrue acceptable1.equals(result) || acceptable2.equals(result) + } + + @Test + void testObjectWithKeyHavingArrayValue() { + //depending on the test platform, and that JSON doesn't require members to be ordered, either of the + //two strings are fine (they're the same data, just the member order is different): + String acceptable1 = '{"test":[1,2],"hello":"世界"}' + String acceptable2 = '{"hello":"世界","test":[1,2]}' + String result = ser([test: [1, 2], hello: '世界']) + assertTrue acceptable1.equals(result) || acceptable2.equals(result) + } + + @Test + void testObjectWithKeyHavingObjectValue() { + //depending on the test platform, and that JSON doesn't require members to be ordered, either of the + //two strings are fine (they're the same data, just the member order is different): + String acceptable1 = '{"test":{"foo":"bar"},"hello":"世界"}' + String acceptable2 = '{"hello":"世界","test":{"foo":"bar"}}' + String result = ser([test: [foo: 'bar'], hello: '世界']) + assertTrue acceptable1.equals(result) || acceptable2.equals(result) + } + + @Test + void testListWithNullElements() { + assertEquals '[1,null,null]', ser([1, null, null] as List) + } + + @Test + void testListWithSingleNullElement() { + assertEquals '[null]', ser([null] as List) + } + + @Test + void testListWithNestedObject() { + assertEquals '[1,null,{"hello":"世界"}]', ser([1, null, [hello: '世界']]) } @Test @@ -56,7 +287,7 @@ class OrgJsonSerializerTest { fail() } catch (SerializationException expected) { String causeMsg = 'Unable to serialize object of type java.lang.Object to JSON using known heuristics.' - String msg = "Unable to serialize object of type java.lang.Object to JSON: $causeMsg" + String msg = "Unable to serialize object of type java.lang.Object: $causeMsg" assertEquals causeMsg, expected.cause.message assertEquals msg, expected.message } diff --git a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonWriterTest.groovy b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonWriterTest.groovy deleted file mode 100644 index c83439a0b..000000000 --- a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonWriterTest.groovy +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright © 2023 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.orgjson.io - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.io.Writer -import io.jsonwebtoken.lang.DateFormats -import io.jsonwebtoken.lang.Strings -import io.jsonwebtoken.lang.Supplier -import org.json.JSONObject -import org.json.JSONString -import org.junit.Before -import org.junit.Test - -import java.nio.charset.StandardCharsets - -import static org.junit.Assert.* - -class OrgJsonWriterTest { - - private OrgJsonWriter writer - - @Before - void setUp() { - writer = new OrgJsonWriter() - } - - private String ser(Object o) { - ByteArrayOutputStream out = new ByteArrayOutputStream() - java.io.Writer w = new OutputStreamWriter(out, StandardCharsets.UTF_8) - writer.write(w, o) - w.close() - return Strings.utf8(out.toByteArray()) - } - - @Test - void loadService() { - def writer = ServiceLoader.load(Writer).iterator().next() - assertTrue writer instanceof OrgJsonWriter - } - - @Test - void testInvalidArgument() { - try { - ser(new Object()) - fail() - } catch (IOException expected) { - String msg = 'Unable to serialize object of type java.lang.Object to JSON using known heuristics.' - assertEquals msg, expected.message - } - } - - @Test - void testWriteObjectRuntimeException() { - - final IllegalArgumentException iae = new IllegalArgumentException("foo") - - writer = new OrgJsonWriter() { - @Override - protected void writeObject(java.io.Writer out, Object o) throws IOException { - throw iae - } - } - try { - ser('hello') - fail() - } catch (IOException expected) { - String msg = 'Cannot write object of type java.lang.String to JSON: foo' - assertEquals msg, expected.getMessage() - assertSame iae, expected.getCause() - } - } - - @Test - void testNull() { - assertEquals 'null', ser(null) - } - - @Test - void testJSONObjectNull() { - assertEquals 'null', ser(JSONObject.NULL) - } - - @Test - void testJSONString() { - def jsonString = new JSONString() { - @Override - String toJSONString() { - return '"foo"' - } - } - assertEquals '"foo"', ser(jsonString) - } - - @Test - void testTrue() { - assertEquals 'true', ser(Boolean.TRUE) - } - - @Test - void testFalse() { - assertEquals 'false', ser(Boolean.FALSE) - } - - @Test - void testByte() { - assertEquals '120', ser("x".getBytes(Strings.UTF_8)[0]) //ascii("x") == 120 - } - - @Test - void testByteArray() { //expect Base64 string by default: - byte[] bytes = "hi".getBytes(Strings.UTF_8) - String expected = '"aGk="' as String //base64(hi) --> aGk= - assertEquals expected, ser(bytes) - } - - @Test - void testEmptyByteArray() { //base64 --> zero bytes == zero-length string: - assertEquals "\"\"", ser(new byte[0]) - } - - @Test - void testChar() { - assertEquals "\"h\"", ser('h' as char) - } - - @Test - void testCharArray() { - assertEquals "\"hi\"", ser("hi".toCharArray()) - } - - @Test - void testEmptyCharArray() { //no chars == empty string: - assertEquals "\"\"", ser(new char[0]) - } - - @Test - void testShort() { - assertEquals '8', ser(8 as short) - } - - @Test - void testInteger() { - assertEquals '1', ser(1 as Integer) - } - - @Test - void testLong() { - assertEquals '42', ser(42 as Long) - } - - @Test - void testBigInteger() { - assertEquals '42', ser(BigInteger.valueOf(42 as Long)) - } - - @Test - void testFloat() { - assertEquals '3.14159', ser(3.14159 as Float) - } - - @Test - void testDouble() { - assertEquals '3.14159', ser(3.14159 as Double) - } - - @Test - void testBigDecimal() { - assertEquals '3.14159', ser(BigDecimal.valueOf(3.14159 as Double)) - } - - @Test - void testEnum() { - assertEquals '"HS256"', ser(SignatureAlgorithm.HS256) - } - - @Test - void testSupplier() { - def supplier = new Supplier() { - @Override - Object get() { - return 'test' - } - } - assertEquals '"test"', ser(supplier) - } - - @Test - void testEmptyString() { - assertEquals '""', ser('') - } - - @Test - void testWhitespaceString() { - String value = " \n\r\t " - assertEquals '" \\n\\r\\t "' as String, ser(value) - } - - @Test - void testSimpleString() { - assertEquals '"hello 世界"', ser('hello 世界') - } - - @Test - void testDate() { - Date now = new Date() - String formatted = DateFormats.formatIso8601(now) - assertEquals "\"$formatted\"" as String, ser(now) - } - - @Test - void testCalendar() { - def cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - def now = cal.getTime() - String formatted = DateFormats.formatIso8601(now) - assertEquals "\"$formatted\"" as String, ser(cal) - } - - @Test - void testSimpleIntArray() { - assertEquals '[1,2]', ser([1, 2] as int[]) - } - - @Test - void testIntegerArrayWithNullElements() { - assertEquals '[1,null]', ser([1, null] as Integer[]) - } - - @Test - void testIntegerList() { - assertEquals '[1,2]', ser([1, 2] as List) - } - - @Test - void testEmptyObject() { - assertEquals '{}', ser([:]) - } - - @Test - void testSimpleObject() { - assertEquals '{"hello":"世界"}', ser([hello: '世界']) - } - - @Test - void testObjectWithKeyHavingNullValue() { - //depending on the test platform, and that JSON doesn't require members to be ordered, either of the - //two strings are fine (they're the same data, just the member order is different): - String acceptable1 = '{"hello":"世界","test":null}' - String acceptable2 = '{"test":null,"hello":"世界"}' - String result = ser([test: null, hello: '世界']) - assertTrue acceptable1.equals(result) || acceptable2.equals(result) - } - - @Test - void testObjectWithKeyHavingArrayValue() { - //depending on the test platform, and that JSON doesn't require members to be ordered, either of the - //two strings are fine (they're the same data, just the member order is different): - String acceptable1 = '{"test":[1,2],"hello":"世界"}' - String acceptable2 = '{"hello":"世界","test":[1,2]}' - String result = ser([test: [1, 2], hello: '世界']) - assertTrue acceptable1.equals(result) || acceptable2.equals(result) - } - - @Test - void testObjectWithKeyHavingObjectValue() { - //depending on the test platform, and that JSON doesn't require members to be ordered, either of the - //two strings are fine (they're the same data, just the member order is different): - String acceptable1 = '{"test":{"foo":"bar"},"hello":"世界"}' - String acceptable2 = '{"hello":"世界","test":{"foo":"bar"}}' - String result = ser([test: [foo: 'bar'], hello: '世界']) - assertTrue acceptable1.equals(result) || acceptable2.equals(result) - } - - @Test - void testListWithNullElements() { - assertEquals '[1,null,null]', ser([1, null, null] as List) - } - - @Test - void testListWithSingleNullElement() { - assertEquals '[null]', ser([null] as List) - } - - @Test - void testListWithNestedObject() { - assertEquals '[1,null,{"hello":"世界"}]', ser([1, null, [hello: '世界']]) - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index bed2202d5..1e5f8fc5f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -25,14 +25,12 @@ import io.jsonwebtoken.impl.io.ByteBase64UrlStreamEncoder; import io.jsonwebtoken.impl.io.CountingInputStream; import io.jsonwebtoken.impl.io.EncodingOutputStream; -import io.jsonwebtoken.impl.io.SerializingMapWriter; +import io.jsonwebtoken.impl.io.NamedSerializer; import io.jsonwebtoken.impl.io.Streams; import io.jsonwebtoken.impl.io.UncloseableInputStream; -import io.jsonwebtoken.impl.io.WritingSerializer; import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.impl.lang.Functions; -import io.jsonwebtoken.impl.lang.Nameable; import io.jsonwebtoken.impl.lang.Parameter; import io.jsonwebtoken.impl.lang.Services; import io.jsonwebtoken.impl.security.DefaultAeadRequest; @@ -45,7 +43,6 @@ import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Encoder; import io.jsonwebtoken.io.Serializer; -import io.jsonwebtoken.io.Writer; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Objects; @@ -70,9 +67,7 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.io.SequenceInputStream; -import java.nio.charset.StandardCharsets; import java.security.Key; import java.security.PrivateKey; import java.security.Provider; @@ -111,7 +106,7 @@ public class DefaultJwtBuilder implements JwtBuilder { private Key key; - private Writer> jsonWriter; + private Serializer> serializer; protected Encoder encoder = Base64UrlStreamEncoder.INSTANCE; private boolean encodePayload = true; @@ -148,15 +143,14 @@ protected Function wrap(Function fn, String fmt, Object... ar return Functions.wrap(fn, SecurityException.class, fmt, args); } - @SuppressWarnings("deprecation") @Override public JwtBuilder serializeToJsonWith(final Serializer> serializer) { - return jsonWriter(new SerializingMapWriter(serializer)); + return json(serializer); } @Override - public JwtBuilder jsonWriter(Writer> writer) { - this.jsonWriter = Assert.notNull(writer, "JSON Writer cannot be null."); + public JwtBuilder json(Serializer> serializer) { + this.serializer = Assert.notNull(serializer, "JSON Serializer cannot be null."); return this; } @@ -535,9 +529,9 @@ public String compact() { throw new IllegalStateException("Both 'content' and 'claims' cannot be specified. Choose either one."); } - if (this.jsonWriter == null) { // try to find one based on the services available + if (this.serializer == null) { // try to find one based on the services available //noinspection unchecked - jsonWriter(Services.loadFirst(Writer.class)); + json(Services.loadFirst(Serializer.class)); } if (!Collections.isEmpty(claims)) { // normalize so we have one object to deal with: @@ -566,22 +560,19 @@ public String compact() { } // automatically closes the OutputStream - private void writeAndClose(OutputStream os, Map map) { - Nameable nameable = Assert.isInstanceOf(Nameable.class, map, "JWT internal maps implement Nameable."); - Writer> jsonWriter = Assert.stateNotNull(this.jsonWriter, "JSON Writer cannot be null."); - WritingSerializer> serializer = new WritingSerializer<>(jsonWriter, nameable.getName()); - java.io.Writer writer = new OutputStreamWriter(os, StandardCharsets.UTF_8); + private void writeAndClose(String name, Map map, OutputStream out) { try { - serializer.accept(writer, map); + Serializer> named = new NamedSerializer(name, this.serializer); + named.serialize(map, out); } finally { - Objects.nullSafeClose(writer); + Objects.nullSafeClose(out); } } - private void writeAndClose(OutputStream out, final Payload payload) { + private void writeAndClose(String name, final Payload payload, OutputStream out) { out = payload.compress(out); // compression if necessary if (payload.isClaims()) { - writeAndClose(out, payload.getRequiredClaims()); + writeAndClose(name, payload.getRequiredClaims(), out); } else { try { InputStream in = payload.toInputStream(); @@ -608,7 +599,7 @@ private String sign(final Payload payload, final Key key, final Provider provide final JwsHeader header = Assert.isInstanceOf(JwsHeader.class, this.headerBuilder.build()); final ByteArrayOutputStream jws = new ByteArrayOutputStream(4096); OutputStream out = encode(jws, "JWS Protected Header"); - writeAndClose(out, header); + writeAndClose("JWS Protected Header", header, out); // ----- separator ----- jws.write(DefaultJwtParser.SEPARATOR_CHAR); @@ -619,7 +610,7 @@ private String sign(final Payload payload, final Key key, final Provider provide InputStream payloadStream = null; // not needed unless b64 is enabled if (this.encodePayload) { out = encode(jws, "JWS Payload"); - writeAndClose(out, payload); + writeAndClose("JWS Payload", payload, out); signingInput = new ByteArrayInputStream(jws.toByteArray()); } else { // b64 @@ -630,7 +621,7 @@ private String sign(final Payload payload, final Key key, final Provider provide // so we ensure we have an input stream for that: if (payload.isClaims() || payload.isCompressed()) { ByteArrayOutputStream claimsOut = new ByteArrayOutputStream(8192); - writeAndClose(claimsOut, payload); + writeAndClose("JWS Unencoded Payload", payload, claimsOut); payloadStream = new ByteArrayInputStream(claimsOut.toByteArray()); } else { // No claims and not compressed, so just get the direct InputStream: @@ -693,14 +684,14 @@ private String unprotected(final Payload content) { final Header header = this.headerBuilder.build(); final ByteArrayOutputStream jwt = new ByteArrayOutputStream(512); OutputStream out = encode(jwt, "JWT Header"); - writeAndClose(out, header); + writeAndClose("JWT Header", header, out); // ----- separator ----- jwt.write(DefaultJwtParser.SEPARATOR_CHAR); // ----- payload ----- out = encode(jwt, "JWT Payload"); - writeAndClose(out, content); + writeAndClose("JWT Payload", content, out); // ----- period terminator ----- jwt.write(DefaultJwtParser.SEPARATOR_CHAR); // https://www.rfc-editor.org/rfc/rfc7519#section-6.1 @@ -719,7 +710,7 @@ private String encrypt(final Payload content, final Key key, final Provider keyP assertPayloadEncoding("JWE"); ByteArrayOutputStream pos = new ByteArrayOutputStream(4096); - writeAndClose(pos, content); + writeAndClose("JWE Payload", content, pos); final byte[] payload = Assert.notEmpty(pos.toByteArray(), "JWE payload bytes cannot be empty."); // JWE invariant (JWS can be empty however) @@ -742,7 +733,7 @@ private String encrypt(final Payload content, final Key key, final Provider keyP ByteArrayOutputStream jwe = new ByteArrayOutputStream(4096); OutputStream out = encode(jwe, "JWE Protected Header"); // automatically base64url-encode as we write - writeAndClose(out, header); // closes/flushes the base64url-encoding stream, not 'jwe' (since BAOSs don't close) + writeAndClose("JWE Protected Header", header, out); // closes/flushes the base64url-encoding stream, not 'jwe' (since BAOSs don't close) // JWE RFC requires AAD to be the ASCII bytes of the Base64URL-encoded header. Since the header bytes are // already Base64URL-encoded at this point (via the encoder.wrap call just above), and Base64Url-encoding uses diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 6f8513941..123158f3a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -53,7 +53,7 @@ import io.jsonwebtoken.io.CompressionAlgorithm; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.DeserializationException; -import io.jsonwebtoken.io.Reader; +import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Classes; import io.jsonwebtoken.lang.Collections; @@ -76,7 +76,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.SequenceInputStream; import java.nio.ByteBuffer; import java.nio.CharBuffer; @@ -202,7 +201,7 @@ public class DefaultJwtParser implements JwtParser { private final Decoder decoder; - private final Reader> jsonReader; + private final Deserializer> deserializer; private final ClaimsBuilder expectedClaims; @@ -224,7 +223,7 @@ public class DefaultJwtParser implements JwtParser { long allowedClockSkewMillis, DefaultClaims expectedClaims, Decoder base64UrlDecoder, - Reader> jsonReader, + Deserializer> deserializer, CompressionCodecResolver compressionCodecResolver, Collection extraZipAlgs, Collection> extraSigAlgs, @@ -240,7 +239,7 @@ public class DefaultJwtParser implements JwtParser { this.allowedClockSkewMillis = allowedClockSkewMillis; this.expectedClaims = Jwts.claims().add(expectedClaims); this.decoder = Assert.notNull(base64UrlDecoder, "base64UrlDecoder cannot be null."); - this.jsonReader = Assert.notNull(jsonReader, "jsonReader cannot be null."); + this.deserializer = Assert.notNull(deserializer, "JSON Deserializer cannot be null."); this.sigAlgFn = new IdLocator<>(DefaultHeader.ALGORITHM, Jwts.SIG.get(), extraSigAlgs, MISSING_JWS_ALG_MSG); this.keyAlgFn = new IdLocator<>(DefaultHeader.ALGORITHM, Jwts.KEY.get(), extraKeyAlgs, MISSING_JWE_ALG_MSG); @@ -934,12 +933,11 @@ protected byte[] decode(CharSequence base64UrlEncoded, String name) { } protected Map deserialize(InputStream in, final String name) { - java.io.Reader r = new InputStreamReader(in); try { - JsonObjectDeserializer deserializer = new JsonObjectDeserializer(jsonReader, name); - return deserializer.apply(r); + JsonObjectDeserializer deserializer = new JsonObjectDeserializer(this.deserializer, name); + return deserializer.apply(in); } finally { - Objects.nullSafeClose(r); + Objects.nullSafeClose(in); } } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 352d43e26..9a2c9418f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -24,14 +24,12 @@ import io.jsonwebtoken.Locator; import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.impl.io.DelegateStringDecoder; -import io.jsonwebtoken.impl.io.DeserializingMapReader; import io.jsonwebtoken.impl.lang.Services; import io.jsonwebtoken.impl.security.ConstantKeyLocator; import io.jsonwebtoken.io.CompressionAlgorithm; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Deserializer; -import io.jsonwebtoken.io.Reader; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; @@ -94,7 +92,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { private Decoder decoder = new DelegateStringDecoder(Decoders.BASE64URL); - private Reader> jsonReader; + private Deserializer> deserializer; private final ClaimsBuilder expectedClaims = Jwts.claims(); @@ -127,13 +125,12 @@ public JwtParserBuilder provider(Provider provider) { @Override public JwtParserBuilder deserializeJsonWith(Deserializer> deserializer) { - Assert.notNull(deserializer, "deserializer cannot be null."); - return jsonReader(new DeserializingMapReader(deserializer)); + return json(deserializer); } @Override - public JwtParserBuilder jsonReader(Reader> reader) { - this.jsonReader = Assert.notNull(reader, "JSON Reader cannot be null."); + public JwtParserBuilder json(Deserializer> reader) { + this.deserializer = Assert.notNull(reader, "JSON Deserializer cannot be null."); return this; } @@ -355,9 +352,9 @@ public JwtParserBuilder setCompressionCodecResolver(CompressionCodecResolver res @Override public JwtParser build() { - if (this.jsonReader == null) { + if (this.deserializer == null) { //noinspection unchecked - jsonReader(Services.loadFirst(Reader.class)); + json(Services.loadFirst(Deserializer.class)); } if (this.signingKeyResolver != null && this.signatureVerificationKey != null) { String msg = "Both a 'signingKeyResolver and a 'verifyWith' key cannot be configured. " + @@ -410,7 +407,7 @@ public JwtParser build() { allowedClockSkewMillis, expClaims, decoder, - jsonReader, + deserializer, compressionCodecResolver, extraZipAlgs, extraSigAlgs, diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/AbstractParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/io/AbstractParserBuilder.java index cfb85f0a0..b7d8eb78d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/AbstractParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/AbstractParserBuilder.java @@ -16,9 +16,9 @@ package io.jsonwebtoken.impl.io; import io.jsonwebtoken.impl.lang.Services; +import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.io.Parser; import io.jsonwebtoken.io.ParserBuilder; -import io.jsonwebtoken.io.Reader; import java.security.Provider; import java.util.Map; @@ -27,7 +27,7 @@ public abstract class AbstractParserBuilder> im protected Provider provider; - protected Reader> jsonReader; + protected Deserializer> deserializer; @SuppressWarnings("unchecked") protected final B self() { @@ -41,16 +41,16 @@ public B provider(Provider provider) { } @Override - public B jsonReader(Reader> reader) { - this.jsonReader = reader; + public B json(Deserializer> reader) { + this.deserializer = reader; return self(); } @Override public final Parser build() { - if (this.jsonReader == null) { + if (this.deserializer == null) { //noinspection unchecked - this.jsonReader = Services.loadFirst(Reader.class); + this.deserializer = Services.loadFirst(Deserializer.class); } return doBuild(); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/ConvertingParser.java b/impl/src/main/java/io/jsonwebtoken/impl/io/ConvertingParser.java index 1b390b647..66ae6f171 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/ConvertingParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/ConvertingParser.java @@ -19,17 +19,18 @@ import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.io.Parser; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; -import java.io.Reader; -import java.io.StringReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.util.Map; public class ConvertingParser implements Parser { - private final Function> deserializer; + private final Function> deserializer; private final Converter converter; - public ConvertingParser(Function> deserializer, Converter converter) { + public ConvertingParser(Function> deserializer, Converter converter) { this.deserializer = Assert.notNull(deserializer, "Deserializer function cannot be null."); this.converter = Assert.notNull(converter, "Converter cannot be null."); } @@ -37,11 +38,12 @@ public ConvertingParser(Function> deserializer, C @Override public final T parse(String input) { Assert.hasText(input, "Parse input String cannot be null or empty."); - return parse(new StringReader(input)); + InputStream in = new ByteArrayInputStream(Strings.utf8(input)); + return parse(in); } - public final T parse(Reader reader) { - Map m = this.deserializer.apply(reader); + public final T parse(InputStream in) { + Map m = this.deserializer.apply(in); return this.converter.applyFrom(m); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/DeserializingMapReader.java b/impl/src/main/java/io/jsonwebtoken/impl/io/DeserializingMapReader.java deleted file mode 100644 index 1e7ae4172..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/DeserializingMapReader.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright © 2023 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.impl.io; - -import io.jsonwebtoken.io.Deserializer; -import io.jsonwebtoken.io.Reader; -import io.jsonwebtoken.lang.Assert; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; -import java.util.Map; - -public class DeserializingMapReader implements Reader> { - - private final Deserializer> deserializer; - - public DeserializingMapReader(Deserializer> deserializer) { - this.deserializer = Assert.notNull(deserializer, "Deserializer cannot be null."); - } - - @Override - public Map read(java.io.Reader in) throws IOException { - int len = 256; - ByteArrayOutputStream baos = new ByteArrayOutputStream(len); - try (OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8)) { - char[] buf = new char[len]; - while (len != -1) { - len = in.read(buf, 0, buf.length); - if (len > 0) writer.write(buf, 0, len); - } - } - byte[] data = baos.toByteArray(); - return deserializer.deserialize(data); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/JsonObjectDeserializer.java b/impl/src/main/java/io/jsonwebtoken/impl/io/JsonObjectDeserializer.java index 05def5c83..d9fa74362 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/JsonObjectDeserializer.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/JsonObjectDeserializer.java @@ -18,37 +18,38 @@ import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.io.DeserializationException; -import io.jsonwebtoken.io.Reader; +import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.lang.Assert; +import java.io.InputStream; import java.util.Map; /** - * Function that wraps a {@link Reader} to add JWT-related error handling. + * Function that wraps a {@link Deserializer} to add JWT-related error handling. * * @since 0.11.3 (renamed from JwtDeserializer) */ -public class JsonObjectDeserializer implements Function> { +public class JsonObjectDeserializer implements Function> { private static final String MALFORMED_ERROR = "Malformed %s JSON: %s"; private static final String MALFORMED_COMPLEX_ERROR = "Malformed or excessively complex %s JSON. " + "If experienced in a production environment, this could reflect a potential malicious %s, please " + "investigate the source further. Cause: %s"; - private final Reader reader; + private final Deserializer deserializer; private final String name; - public JsonObjectDeserializer(Reader reader, String name) { - this.reader = Assert.notNull(reader, "reader cannot be null."); + public JsonObjectDeserializer(Deserializer deserializer, String name) { + this.deserializer = Assert.notNull(deserializer, "JSON Deserializer cannot be null."); this.name = Assert.hasText(name, "name cannot be null or empty."); } @Override - public Map apply(java.io.Reader reader) { - Assert.notNull(reader, "Reader argument cannot be null."); + public Map apply(InputStream in) { + Assert.notNull(in, "InputStream argument cannot be null."); Object value; try { - value = this.reader.read(reader); + value = this.deserializer.deserialize(in); if (value == null) { String msg = "Deserialized data resulted in a null value; cannot create Map"; throw new DeserializationException(msg); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/WritingSerializer.java b/impl/src/main/java/io/jsonwebtoken/impl/io/NamedSerializer.java similarity index 62% rename from impl/src/main/java/io/jsonwebtoken/impl/io/WritingSerializer.java rename to impl/src/main/java/io/jsonwebtoken/impl/io/NamedSerializer.java index 34c03948f..2f4ce2458 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/WritingSerializer.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/NamedSerializer.java @@ -15,27 +15,30 @@ */ package io.jsonwebtoken.impl.io; -import io.jsonwebtoken.impl.lang.BiConsumer; +import io.jsonwebtoken.io.AbstractSerializer; import io.jsonwebtoken.io.SerializationException; -import io.jsonwebtoken.io.Writer; +import io.jsonwebtoken.io.Serializer; import io.jsonwebtoken.lang.Assert; -public class WritingSerializer implements BiConsumer { +import java.io.OutputStream; +import java.util.Map; + +public class NamedSerializer extends AbstractSerializer> { - private final Writer DELEGATE; private final String name; + private final Serializer> DELEGATE; - public WritingSerializer(Writer jsonWriter, String name) { - this.DELEGATE = Assert.notNull(jsonWriter, "JSON Writer cannot be null."); + public NamedSerializer(String name, Serializer> serializer) { + this.DELEGATE = Assert.notNull(serializer, "JSON Serializer cannot be null."); this.name = Assert.hasText(name, "Name cannot be null or empty."); } @Override - public void accept(java.io.Writer writer, T object) { + protected void doSerialize(Map m, OutputStream out) throws SerializationException { try { - this.DELEGATE.write(writer, object); + this.DELEGATE.serialize(m, out); } catch (Throwable t) { - String msg = String.format("Cannot serialize %s to JSON. Cause: %s", name, t.getMessage()); + String msg = String.format("Cannot serialize %s to JSON. Cause: %s", this.name, t.getMessage()); throw new SerializationException(msg, t); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/SerializingMapWriter.java b/impl/src/main/java/io/jsonwebtoken/impl/io/SerializingMapWriter.java deleted file mode 100644 index 901191900..000000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/SerializingMapWriter.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright © 2023 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.impl.io; - -import io.jsonwebtoken.io.Serializer; -import io.jsonwebtoken.io.Writer; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Strings; - -import java.io.IOException; -import java.util.Map; - -public class SerializingMapWriter implements Writer> { - - private final Serializer> DELEGATE; - - public SerializingMapWriter(Serializer> delegate) { - this.DELEGATE = Assert.notNull(delegate, "Serializer cannot be null."); - } - - @Override - public void write(java.io.Writer out, Map map) throws IOException { - byte[] bytes = DELEGATE.serialize(map); - String s = Strings.utf8(bytes); - out.write(s); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/Streams.java b/impl/src/main/java/io/jsonwebtoken/impl/io/Streams.java index c1a796960..7c7ea667e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/Streams.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/Streams.java @@ -19,6 +19,7 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Classes; import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Strings; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; @@ -47,6 +48,14 @@ public static byte[] bytes(final InputStream in, String exmsg) { return out.toByteArray(); } + public static InputStream stream(byte[] bytes) { + return new ByteArrayInputStream(bytes); + } + + public static InputStream stream(CharSequence seq) { + return stream(Strings.utf8(seq)); + } + /** * Copies bytes from a {@link InputStream} to an {@link OutputStream} using the specified {@code buffer}, avoiding * the need for a {@link BufferedInputStream}. diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParserBuilder.java index 25f8cdae3..5337a779c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParserBuilder.java @@ -24,7 +24,7 @@ public class DefaultJwkParserBuilder extends AbstractJwkParserBuilder, Jw implements JwkParserBuilder { @Override public Parser> doBuild() { - JwkDeserializer deserializer = new JwkDeserializer(this.jsonReader); + JwkDeserializer deserializer = new JwkDeserializer(this.deserializer); JwkBuilderSupplier supplier = new JwkBuilderSupplier(this.provider, this.operationPolicy); JwkConverter> converter = new JwkConverter<>(supplier); return new ConvertingParser<>(deserializer, converter); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkSetParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkSetParserBuilder.java index 4236691e1..fa6e42882 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkSetParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkSetParserBuilder.java @@ -33,7 +33,7 @@ public JwkSetParserBuilder ignoreUnsupported(boolean ignore) { @Override public Parser doBuild() { - JwkSetDeserializer deserializer = new JwkSetDeserializer(this.jsonReader); + JwkSetDeserializer deserializer = new JwkSetDeserializer(this.deserializer); JwkBuilderSupplier supplier = new JwkBuilderSupplier(this.provider, this.operationPolicy); JwkSetConverter converter = new JwkSetConverter(supplier, this.ignoreUnsupported); return new ConvertingParser<>(deserializer, converter); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkDeserializer.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkDeserializer.java index b6c998964..6a8e9ecff 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkDeserializer.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkDeserializer.java @@ -16,13 +16,13 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.io.JsonObjectDeserializer; -import io.jsonwebtoken.io.Reader; +import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.security.MalformedKeyException; public class JwkDeserializer extends JsonObjectDeserializer { - public JwkDeserializer(Reader reader) { - super(reader, "JWK"); + public JwkDeserializer(Deserializer deserializer) { + super(deserializer, "JWK"); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkSetDeserializer.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkSetDeserializer.java index 5fa11f544..ccce6d420 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkSetDeserializer.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkSetDeserializer.java @@ -16,13 +16,13 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.io.JsonObjectDeserializer; -import io.jsonwebtoken.io.Reader; +import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.security.MalformedKeySetException; public class JwkSetDeserializer extends JsonObjectDeserializer { - public JwkSetDeserializer(Reader reader) { - super(reader, "JWK Set"); + public JwkSetDeserializer(Deserializer deserializer) { + super(deserializer, "JWK Set"); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwksBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwksBridge.java index 343fcebe5..8b5de6a80 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwksBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwksBridge.java @@ -15,17 +15,15 @@ */ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.impl.io.WritingSerializer; +import io.jsonwebtoken.impl.io.NamedSerializer; import io.jsonwebtoken.impl.lang.Services; -import io.jsonwebtoken.io.Writer; +import io.jsonwebtoken.io.Serializer; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.Jwk; import java.io.ByteArrayOutputStream; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; +import java.util.Map; public final class JwksBridge { @@ -34,16 +32,11 @@ private JwksBridge() { @SuppressWarnings({"unchecked", "unused"}) // used via reflection by io.jsonwebtoken.security.Jwks public static String UNSAFE_JSON(Jwk jwk) { - Writer> writer = Services.loadFirst(Writer.class); - Assert.stateNotNull(writer, "Writer lookup failed. Ensure JSON impl .jar is in the runtime classpath."); - WritingSerializer> ser = new WritingSerializer<>(writer, "JWK"); - ByteArrayOutputStream baos = new ByteArrayOutputStream(512); - OutputStreamWriter w = new OutputStreamWriter(baos, StandardCharsets.UTF_8); - try { - ser.accept(w, jwk); - } finally { - Objects.nullSafeClose(w); - } - return Strings.utf8(baos.toByteArray()); + Serializer> serializer = Services.loadFirst(Serializer.class); + Assert.stateNotNull(serializer, "Serializer lookup failed. Ensure JSON impl .jar is in the runtime classpath."); + NamedSerializer ser = new NamedSerializer("JWK", serializer); + ByteArrayOutputStream out = new ByteArrayOutputStream(512); + ser.serialize(jwk, out); + return Strings.utf8(out.toByteArray()); } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy index 03895ed3d..e5aaa11f5 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy @@ -15,7 +15,7 @@ */ package io.jsonwebtoken -import io.jsonwebtoken.jackson.io.JacksonReader +import io.jsonwebtoken.jackson.io.JacksonDeserializer import org.junit.Test import static org.junit.Assert.assertEquals @@ -41,8 +41,8 @@ class CustomObjectDeserializationTest { assertEquals jwt.getPayload().get('cust'), [key1: 'value1', key2: 42] // custom type for 'cust' claim - def reader = new JacksonReader([cust: CustomBean]) - jwt = Jwts.parser().enableUnsecured().jsonReader(reader).build().parseClaimsJwt(jwtString) + def des = new JacksonDeserializer([cust: CustomBean]) + jwt = Jwts.parser().enableUnsecured().json(des).build().parseClaimsJwt(jwtString) assertNotNull jwt CustomBean result = jwt.getPayload().get("cust", CustomBean) assertEquals customBean, result diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 1f2c7d0ea..9000a4086 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -23,7 +23,7 @@ import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.impl.security.* import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders -import io.jsonwebtoken.io.Writer +import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.* import org.junit.Test @@ -73,10 +73,10 @@ class JwtsTest { } static def toJson(def o) { - def writer = Services.loadFirst(Writer) - StringWriter w = new StringWriter(512) - writer.write(w, o) - return w.toString() + def serializer = Services.loadFirst(Serializer) + def out = new ByteArrayOutputStream() + serializer.serialize(o, out) + return Strings.utf8(out.toByteArray()) } @Test @@ -211,7 +211,7 @@ class JwtsTest { @Test void testContentStreamNull() { - String compact = Jwts.builder().content((InputStream)null).compact() + String compact = Jwts.builder().content((InputStream) null).compact() def jwt = Jwts.parser().enableUnsecured().build().parseContentJwt(compact) assertEquals 'none', jwt.header.getAlgorithm() assertTrue Bytes.isEmpty(jwt.getPayload()) diff --git a/impl/src/test/groovy/io/jsonwebtoken/RFC7515AppendixETest.groovy b/impl/src/test/groovy/io/jsonwebtoken/RFC7515AppendixETest.groovy index 1d4b3cc88..25ca2eada 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/RFC7515AppendixETest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/RFC7515AppendixETest.groovy @@ -17,30 +17,32 @@ package io.jsonwebtoken import io.jsonwebtoken.impl.DefaultJwtParser import io.jsonwebtoken.impl.RfcTests +import io.jsonwebtoken.impl.io.Streams import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.io.Encoders -import io.jsonwebtoken.io.Reader -import io.jsonwebtoken.io.Writer +import io.jsonwebtoken.io.Serializer import org.junit.Test -import java.nio.charset.StandardCharsets - import static org.junit.Assert.assertEquals import static org.junit.Assert.fail class RFC7515AppendixETest { - static final Writer> writer = Services.loadFirst(Writer) - static final Reader> reader = Services.loadFirst(Reader) + static final Serializer> serializer = Services.loadFirst(Serializer) + static final Deserializer> deserializer = Services.loadFirst(Deserializer) - static byte[] ser(Writer writer, def value) { + static byte[] ser(def value) { ByteArrayOutputStream baos = new ByteArrayOutputStream(512) - OutputStreamWriter w = new OutputStreamWriter(baos, StandardCharsets.UTF_8) - writer.write(w, value) - w.close() + serializer.serialize(value, baos) return baos.toByteArray() } + static T deser(String s) { + T t = deserializer.deserialize(Streams.stream(s)) as Map as T + return t + } + @Test void test() { @@ -49,8 +51,8 @@ class RFC7515AppendixETest { "crit":["http://example.invalid/UNDEFINED"], "http://example.invalid/UNDEFINED":true }''') - Map header = reader.read(new StringReader(headerString)) - String b64url = Encoders.BASE64URL.encode(ser(writer, header)) + Map header = deser(headerString) + String b64url = Encoders.BASE64URL.encode(ser(header)) String jws = b64url + '.RkFJTA.' @@ -80,8 +82,8 @@ class RFC7515AppendixETest { "crit":["$critVal"], "$critVal":true }""") - Map header = reader.read(new StringReader(headerString)) - String b64url = Encoders.BASE64URL.encode(ser(writer, header)) + Map header = deser(headerString) + String b64url = Encoders.BASE64URL.encode(ser(header)) String jws = b64url + '.RkFJTA.fakesignature' // needed to parse a JWS properly diff --git a/impl/src/test/groovy/io/jsonwebtoken/RFC7797Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/RFC7797Test.groovy index 88f11e424..23877461c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/RFC7797Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/RFC7797Test.groovy @@ -20,11 +20,10 @@ import io.jsonwebtoken.impl.DefaultJwtParser import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.impl.security.TestKeys -import io.jsonwebtoken.io.Writer +import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings import org.junit.Test -import java.nio.charset.StandardCharsets import java.security.Key import static org.junit.Assert.* @@ -100,7 +99,7 @@ class RFC7797Test { def claims = Jwts.claims().subject('me').build() ByteArrayOutputStream out = new ByteArrayOutputStream() - Services.loadFirst(Writer).write(new OutputStreamWriter(out, StandardCharsets.UTF_8), claims) + Services.loadFirst(Serializer).serialize(claims, out) byte[] content = out.toByteArray() //byte[] content = Services.loadFirst(Serializer).serialize(claims) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 46d4e70e7..f5d55a28b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -19,12 +19,13 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.impl.io.Streams +import io.jsonwebtoken.impl.io.TestSerializer import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.impl.security.Randoms import io.jsonwebtoken.impl.security.TestKey import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.* -import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.* import org.junit.Before import org.junit.Test @@ -44,12 +45,15 @@ class DefaultJwtBuilderTest { private DefaultJwtBuilder builder private static byte[] serialize(Map map) { - def writer = Services.loadFirst(Writer) - ByteArrayOutputStream baos = new ByteArrayOutputStream(512) - OutputStreamWriter w = new OutputStreamWriter(baos, StandardCharsets.UTF_8) - writer.write(w, map) - w.close() - return baos.toByteArray() + def serializer = Services.loadFirst(Serializer) + ByteArrayOutputStream out = new ByteArrayOutputStream(512) + serializer.serialize(map, out) + return out.toByteArray() + } + + private static Map deser(byte[] data) { + Map m = Services.loadFirst(Deserializer).deserialize(Streams.stream(data)) as Map + return m } @Before @@ -286,39 +290,29 @@ class DefaultJwtBuilderTest { @Test void testHeaderSerializationErrorException() { def ex = new IOException('foo') - def writer = new Writer() { - @Override - void write(java.io.Writer out, Object o) throws java.io.IOException { - throw ex - } - } - def b = new DefaultJwtBuilder().jsonWriter(writer) + def ser = new TestSerializer(ex: ex) + def b = new DefaultJwtBuilder().json(ser) try { b.content('bar').compact() fail() } catch (SerializationException expected) { - assertEquals 'Cannot serialize JWT header to JSON. Cause: foo', expected.getMessage() + assertEquals 'Cannot serialize JWT Header to JSON. Cause: foo', expected.getMessage() } } @Test void testCompactCompressionCodecJsonProcessingException() { def ex = new IOException('dummy text') - def writer = new Writer() { - @Override - void write(java.io.Writer out, Object o) throws java.io.IOException { - throw ex - } - } + def ser = new TestSerializer(ex: ex) def b = new DefaultJwtBuilder() .setSubject("Joe") // ensures claims instance .compressWith(Jwts.ZIP.DEF) - .jsonWriter(writer) + .json(ser) try { b.compact() fail() } catch (SerializationException expected) { - assertEquals 'Cannot serialize JWT header to JSON. Cause: dummy text', expected.message + assertEquals 'Cannot serialize JWT Header to JSON. Cause: dummy text', expected.message } } @@ -509,11 +503,12 @@ class DefaultJwtBuilderTest { @Test void testSerializeToJsonWithCustomSerializer() { boolean invoked = false - def serializer = new Serializer() { + def serializer = new AbstractSerializer() { @Override - byte[] serialize(Object o) throws SerializationException { + protected void doSerialize(Object o, OutputStream out) throws Exception { invoked = true - return objectMapper.writeValueAsBytes(o) + byte[] data = objectMapper.writeValueAsBytes(o) + out.write(data) } } @@ -604,7 +599,7 @@ class DefaultJwtBuilderTest { // so we need to check the raw payload: def encoded = new JwtTokenizer().tokenize(jwt).getPayload() byte[] bytes = Decoders.BASE64URL.decode(encoded) - def claims = Services.loadFirst(Reader).read(new StringReader(Strings.utf8(bytes))) + def claims = deser(bytes) assertEquals audienceSingleString, claims.aud } @@ -622,7 +617,7 @@ class DefaultJwtBuilderTest { // so we need to check the raw payload: def encoded = new JwtTokenizer().tokenize(jwt).getPayload() byte[] bytes = Decoders.BASE64URL.decode(encoded) - def claims = Services.loadFirst(Reader).read(new StringReader(Strings.utf8(bytes))) + def claims = deser(bytes) assertEquals second, claims.aud // second audienceSingle call replaces first value } @@ -754,7 +749,7 @@ class DefaultJwtBuilderTest { // so we need to check the raw payload: def encoded = new JwtTokenizer().tokenize(jwt).getPayload() byte[] bytes = Decoders.BASE64URL.decode(encoded) - def claims = Services.loadFirst(Reader).read(new StringReader(Strings.utf8(bytes))) as Map + def claims = Services.loadFirst(Deserializer).deserialize(Streams.stream(bytes)) assertEquals two, claims.aud } @@ -790,7 +785,7 @@ class DefaultJwtBuilderTest { // so we need to check the raw payload: def encoded = new JwtTokenizer().tokenize(jwt).getPayload() byte[] bytes = Decoders.BASE64URL.decode(encoded) - def claims = Services.loadFirst(Reader).read(new StringReader(Strings.utf8(bytes))) as Map + def claims = deser(bytes) assertEquals three, claims.aud } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index d53bddc9e..723820e86 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -22,7 +22,6 @@ import io.jsonwebtoken.impl.security.LocatingKeyResolver import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.* import io.jsonwebtoken.security.* -import org.hamcrest.CoreMatchers import org.junit.Before import org.junit.Test @@ -30,7 +29,6 @@ import javax.crypto.SecretKey import java.security.Provider import static org.easymock.EasyMock.* -import static org.hamcrest.MatcherAssert.assertThat import static org.junit.Assert.* // NOTE to the casual reader: even though this test class appears mostly empty, the DefaultJwtParserBuilder @@ -124,14 +122,14 @@ class DefaultJwtParserBuilderTest { @Test void testDeserializeJsonWithCustomSerializer() { - def deserializer = new Deserializer() { + def deserializer = new AbstractDeserializer() { @Override - Object deserialize(byte[] bytes) throws DeserializationException { - return OBJECT_MAPPER.readValue(bytes, Map.class) + protected Object doDeserialize(InputStream inputStream) throws Exception { + return OBJECT_MAPPER.readValue(inputStream, Map.class) } } def p = builder.deserializeJsonWith(deserializer) - assertSame deserializer, p.jsonReader.deserializer + assertSame deserializer, p.@deserializer def alg = Jwts.SIG.HS256 def key = alg.key().build() @@ -343,14 +341,14 @@ class DefaultJwtParserBuilderTest { @Test void testDefaultDeserializer() { JwtParser parser = builder.build() // perform ServiceLoader lookup - assertThat parser.jsonReader, CoreMatchers.instanceOf(Reader) + assertTrue parser.@deserializer instanceof Deserializer } @Test void testUserSetDeserializerWrapped() { Deserializer deserializer = niceMock(Deserializer) JwtParser parser = builder.deserializeJsonWith(deserializer).build() - assertSame deserializer, parser.jsonReader.deserializer + assertSame deserializer, parser.@deserializer } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy index a1cfc4d40..3cfa87241 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy @@ -20,10 +20,9 @@ import io.jsonwebtoken.* import io.jsonwebtoken.impl.lang.JwtDateConverter import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.impl.security.TestKeys -import io.jsonwebtoken.io.DeserializationException -import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.io.AbstractDeserializer import io.jsonwebtoken.io.Encoders -import io.jsonwebtoken.io.Writer +import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Collections import io.jsonwebtoken.lang.DateFormats import io.jsonwebtoken.lang.Strings @@ -33,7 +32,6 @@ import org.junit.Test import javax.crypto.Mac import javax.crypto.SecretKey -import java.nio.charset.StandardCharsets import static org.junit.Assert.* @@ -56,12 +54,10 @@ class DefaultJwtParserTest { } private static byte[] serialize(Map map) { - def writer = Services.loadFirst(Writer) - ByteArrayOutputStream baos = new ByteArrayOutputStream(512) - OutputStreamWriter w = new OutputStreamWriter(baos, StandardCharsets.UTF_8) - writer.write(w, map) - w.close() - return baos.toByteArray() + def serializer = Services.loadFirst(Serializer) + ByteArrayOutputStream out = new ByteArrayOutputStream(512) + serializer.serialize(map, out) + return out.toByteArray() } @Before @@ -77,11 +73,11 @@ class DefaultJwtParserTest { @Test void testDesrializeJsonWithCustomSerializer() { boolean invoked = false - def deserializer = new Deserializer() { + def deserializer = new AbstractDeserializer() { @Override - Object deserialize(byte[] bytes) throws DeserializationException { + protected Object doDeserialize(InputStream inputStream) throws Exception { invoked = true - return OBJECT_MAPPER.readValue(bytes, Map.class) + return OBJECT_MAPPER.readValue(inputStream, Map.class) } } def pb = Jwts.parser().deserializeJsonWith(deserializer) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy index 442bf52c8..93820ced2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy @@ -15,11 +15,12 @@ */ package io.jsonwebtoken.impl +import io.jsonwebtoken.impl.io.Streams import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.impl.security.Randoms import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.io.Encoders -import io.jsonwebtoken.io.Reader class RfcTests { @@ -36,7 +37,8 @@ class RfcTests { } static final Map jsonToMap(String json) { - return Services.loadFirst(Reader).read(new StringReader(json)) as Map + Map m = Services.loadFirst(Deserializer).deserialize(Streams.stream(json)) as Map + return m } /** diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/io/JsonObjectDeserializerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/io/JsonObjectDeserializerTest.groovy index defef0b6a..eff5e9671 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/io/JsonObjectDeserializerTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/io/JsonObjectDeserializerTest.groovy @@ -18,7 +18,7 @@ package io.jsonwebtoken.impl.io import io.jsonwebtoken.MalformedJwtException import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.io.DeserializationException -import io.jsonwebtoken.io.Reader +import io.jsonwebtoken.io.Deserializer import org.junit.Test import static org.junit.Assert.* @@ -31,17 +31,23 @@ class JsonObjectDeserializerTest { @Test void testStackOverflowError() { def err = new StackOverflowError('foo') - // create a Reader that will throw a StackOverflowError - def reader = new Reader() { + // create one that will throw a StackOverflowError + def deser = new Deserializer() { @Override - Object read(java.io.Reader reader) throws IOException { + Object deserialize(byte[] bytes) throws DeserializationException { + fail() // shouldn't be called in this test + return null + } + + @Override + Object deserialize(InputStream inputStream) throws DeserializationException { throw err } } try { // doesn't matter for this test, just has to be non-null: - def r = new InputStreamReader(new ByteArrayInputStream(Bytes.EMPTY)) - new JsonObjectDeserializer(reader, 'claims').apply(r) + def ins = new ByteArrayInputStream(Bytes.EMPTY) + new JsonObjectDeserializer(deser, 'claims').apply(ins) fail() } catch (DeserializationException e) { String msg = String.format(JsonObjectDeserializer.MALFORMED_COMPLEX_ERROR, 'claims', 'claims', 'foo') @@ -54,17 +60,22 @@ class JsonObjectDeserializerTest { @Test void testDeserializationExceptionMessage() { def ex = new IOException('foo') - // create a Reader that will throw a StackOverflowError - def reader = new Reader() { + // create one that will throw a StackOverflowError + def deser = new Deserializer() { + @Override + Object deserialize(byte[] bytes) throws DeserializationException { + fail() // should not be called in this test + return null + } @Override - Object read(java.io.Reader reader) throws IOException { + Object deserialize(InputStream inputStream) throws DeserializationException { throw ex } } try { // doesn't matter for this test, just has to be non-null: - def r = new InputStreamReader(new ByteArrayInputStream(Bytes.EMPTY)) - new JsonObjectDeserializer(reader, 'claims').apply(r) + def ins = new ByteArrayInputStream(Bytes.EMPTY) + new JsonObjectDeserializer(deser, 'claims').apply(ins) fail() } catch (MalformedJwtException e) { String msg = String.format(JsonObjectDeserializer.MALFORMED_ERROR, 'claims', 'foo') diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/io/TestSerializer.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/io/TestSerializer.groovy new file mode 100644 index 000000000..182f3d493 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/io/TestSerializer.groovy @@ -0,0 +1,48 @@ +/* + * Copyright © 2023 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.impl.io + +import io.jsonwebtoken.io.SerializationException +import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.lang.Strings + +import static org.junit.Assert.fail + +class TestSerializer implements Serializer> { + + Throwable ex + + @Override + byte[] serialize(Map stringMap) throws SerializationException { + fail("serialize(byte[]) should not be invoked.") + return null + } + + @Override + void serialize(Map map, OutputStream out) throws SerializationException { + def json = toJson(map) + if (Strings.hasText(json)) { + out.write(Strings.utf8(json)) + } else { + Throwable t = ex != null ? ex : new UnsupportedOperationException("Override toJson") + throw t + } + } + + protected String toJson(Map m) { + return null + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy index 5c7615269..d5a6545b7 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy @@ -16,8 +16,9 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.impl.io.ConvertingParser +import io.jsonwebtoken.io.AbstractDeserializer import io.jsonwebtoken.io.DeserializationException -import io.jsonwebtoken.io.Reader +import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.MalformedKeyException @@ -61,13 +62,13 @@ class DefaultJwkParserBuilderTest { } @Test - void testJsonReader() { - def reader = createMock(Reader) as Reader + void testDeserializer() { + def deser = createMock(Deserializer) as Deserializer> def m = RFC7516AppendixA3Test.KEK_VALUES // any test key will do - expect(reader.read(anyObject(java.io.Reader.class))).andReturn(m) - replay reader - def jwk = Jwks.parser().jsonReader(reader).build().parse('foo') - verify reader + expect(deser.deserialize((InputStream)anyObject(InputStream))).andReturn(m) + replay deser + def jwk = Jwks.parser().json(deser).build().parse('foo') + verify deser assertTrue jwk instanceof SecretJwk assertEquals m.kty, jwk.kty assertEquals m.k, jwk.k.get() @@ -138,13 +139,13 @@ class DefaultJwkParserBuilderTest { @Test void testDeserializationFailure() { - def reader = new Reader() { + def deser = new AbstractDeserializer() { @Override - Object read(java.io.Reader reader) throws IOException { + protected Object doDeserialize(InputStream inputStream) throws Exception { throw new DeserializationException('test') } } - def parser = new DefaultJwkParserBuilder().jsonReader(reader).build() + def parser = new DefaultJwkParserBuilder().json(deser).build() try { parser.parse('foo') fail() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkSetParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkSetParserBuilderTest.groovy index 3e88b680e..7fbb94768 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkSetParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkSetParserBuilderTest.groovy @@ -15,9 +15,9 @@ */ package io.jsonwebtoken.impl.security +import io.jsonwebtoken.io.AbstractDeserializer import io.jsonwebtoken.io.DeserializationException import io.jsonwebtoken.io.Parser -import io.jsonwebtoken.io.Reader import io.jsonwebtoken.security.JwkSet import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.MalformedKeySetException @@ -66,13 +66,13 @@ class DefaultJwkSetParserBuilderTest { */ @Test void testDeserializeException() { - def reader = new Reader() { + def deser = new AbstractDeserializer() { @Override - Object read(java.io.Reader reader) throws IOException { + protected Object doDeserialize(InputStream inputStream) throws Exception { throw new DeserializationException('foo') } } - parser = new DefaultJwkSetParserBuilder().jsonReader(reader).build() + parser = new DefaultJwkSetParserBuilder().json(deser).build() try { parser.parse('foo') diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy index 0effd0385..8e4f3a61b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy @@ -15,16 +15,16 @@ */ package io.jsonwebtoken.impl.security - -import io.jsonwebtoken.gson.io.GsonReader -import io.jsonwebtoken.gson.io.GsonWriter -import io.jsonwebtoken.io.Reader -import io.jsonwebtoken.io.Writer -import io.jsonwebtoken.jackson.io.JacksonReader -import io.jsonwebtoken.jackson.io.JacksonWriter +import io.jsonwebtoken.gson.io.GsonDeserializer +import io.jsonwebtoken.gson.io.GsonSerializer +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.jackson.io.JacksonDeserializer +import io.jsonwebtoken.jackson.io.JacksonSerializer +import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.lang.Supplier -import io.jsonwebtoken.orgjson.io.OrgJsonReader -import io.jsonwebtoken.orgjson.io.OrgJsonWriter +import io.jsonwebtoken.orgjson.io.OrgJsonDeserializer +import io.jsonwebtoken.orgjson.io.OrgJsonSerializer import io.jsonwebtoken.security.Jwk import io.jsonwebtoken.security.Jwks import org.junit.Test @@ -40,63 +40,63 @@ import static org.junit.Assert.assertTrue */ class JwkSerializationTest { - static String serialize(Writer writer, def value) { - def w = new StringWriter(512) - writer.write(w, value) - return w.toString() + static String serialize(Serializer ser, def value) { + def out = new ByteArrayOutputStream() + ser.serialize(value, out) + return Strings.utf8(out.toByteArray()) } - static Map deserialize(Reader reader, String value) { - StringReader r = new StringReader(value) - return reader.read(r) as Map + static Map deserialize(Deserializer des, String value) { + def ins = new ByteArrayInputStream(Strings.utf8(value)) + return des.deserialize(ins) as Map } @Test void testJacksonSecretJwk() { - testSecretJwk(new JacksonWriter(), new JacksonReader()) + testSecretJwk(new JacksonSerializer(), new JacksonDeserializer()) } @Test void testJacksonPrivateEcJwk() { - testPrivateEcJwk(new JacksonWriter(), new JacksonReader()) + testPrivateEcJwk(new JacksonSerializer(), new JacksonDeserializer()) } @Test void testJacksonPrivateRsaJwk() { - testPrivateRsaJwk(new JacksonWriter(), new JacksonReader()) + testPrivateRsaJwk(new JacksonSerializer(), new JacksonDeserializer()) } @Test void testGsonSecretJwk() { - testSecretJwk(new GsonWriter(), new GsonReader()) + testSecretJwk(new GsonSerializer(), new GsonDeserializer()) } @Test void testGsonPrivateEcJwk() { - testPrivateEcJwk(new GsonWriter(), new GsonReader()) + testPrivateEcJwk(new GsonSerializer(), new GsonDeserializer()) } @Test void testGsonPrivateRsaJwk() { - testPrivateRsaJwk(new GsonWriter(), new GsonReader()) + testPrivateRsaJwk(new GsonSerializer(), new GsonDeserializer()) } @Test void testOrgJsonSecretJwk() { - testSecretJwk(new OrgJsonWriter(), new OrgJsonReader()) + testSecretJwk(new OrgJsonSerializer(), new OrgJsonDeserializer()) } @Test void testOrgJsonPrivateEcJwk() { - testPrivateEcJwk(new OrgJsonWriter(), new OrgJsonReader()) + testPrivateEcJwk(new OrgJsonSerializer(), new OrgJsonDeserializer()) } @Test void testOrgJsonPrivateRsaJwk() { - testPrivateRsaJwk(new OrgJsonWriter(), new OrgJsonReader()) + testPrivateRsaJwk(new OrgJsonSerializer(), new OrgJsonDeserializer()) } - static void testSecretJwk(Writer writer, Reader reader) { + static void testSecretJwk(Serializer ser, Deserializer des) { def key = TestKeys.A128GCM def jwk = Jwks.builder().key(key).id('id').build() @@ -107,14 +107,14 @@ class JwkSerializationTest { assertEquals '{kid=id, kty=oct, k=}', jwk.toString() // java toString //but serialization prints the real value: - String json = serialize(writer, jwk) + String json = serialize(ser, jwk) // assert substrings here because JSON order is not guaranteed: assertTrue json.contains('"kid":"id"') assertTrue json.contains('"kty":"oct"') assertTrue json.contains("\"k\":\"${jwk.k.get()}\"" as String) //now ensure it deserializes back to a JWK: - def map = deserialize(reader, json) + def map = deserialize(des, json) def jwk2 = Jwks.builder().add(map).build() assertTrue jwk.k instanceof Supplier assertEquals jwk, jwk2 @@ -122,7 +122,7 @@ class JwkSerializationTest { assertEquals jwk.k.get(), jwk2.k.get() } - static void testPrivateEcJwk(Writer writer, Reader reader) { + static void testPrivateEcJwk(Serializer ser, Deserializer des) { def jwk = Jwks.builder().ecKeyPair(TestKeys.ES256.pair).id('id').build() assertWrapped(jwk, ['d']) @@ -134,7 +134,7 @@ class JwkSerializationTest { // java toString //but serialization prints the real value: - String json = serialize(writer, jwk) + String json = serialize(ser, jwk) // assert substrings here because JSON order is not guaranteed: assertTrue json.contains('"kid":"id"') assertTrue json.contains('"kty":"EC"') @@ -144,7 +144,7 @@ class JwkSerializationTest { assertTrue json.contains("\"d\":\"${jwk.d.get()}\"" as String) //now ensure it deserializes back to a JWK: - def map = deserialize(reader, json) + def map = deserialize(des, json) def jwk2 = Jwks.builder().add(map).build() assertTrue jwk.d instanceof Supplier assertEquals jwk, jwk2 @@ -171,7 +171,7 @@ class JwkSerializationTest { } } - static void testPrivateRsaJwk(Writer writer, Reader reader) { + static void testPrivateRsaJwk(Serializer ser, Deserializer des) { def jwk = Jwks.builder().rsaKeyPair(TestKeys.RS256.pair).id('id').build() def privateFieldNames = ['d', 'p', 'q', 'dp', 'dq', 'qi'] @@ -184,7 +184,7 @@ class JwkSerializationTest { // java toString //but serialization prints the real value: - String json = serialize(writer, jwk) + String json = serialize(ser, jwk) // assert substrings here because JSON order is not guaranteed: assertTrue json.contains('"kid":"id"') assertTrue json.contains('"kty":"RSA"') @@ -198,7 +198,7 @@ class JwkSerializationTest { assertTrue json.contains("\"qi\":\"${jwk.qi.get()}\"" as String) //now ensure it deserializes back to a JWK: - def map = deserialize(reader, json) + def map = deserialize(des, json) def jwk2 = Jwks.builder().add(map).build() assertEquals(jwk, jwk2, privateFieldNames) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy index 73e45fb40..468a3d77c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy @@ -18,6 +18,7 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.Jwe import io.jsonwebtoken.JweHeader import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.io.TestSerializer import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.security.* import org.junit.Test @@ -297,11 +298,11 @@ class RFC7517AppendixCTest { return RFC_P2S } } - def writer = new io.jsonwebtoken.io.Writer() { + def ser = new TestSerializer() { @Override - void write(Writer out, Object o) throws IOException { - assertTrue o instanceof JweHeader - JweHeader header = (JweHeader) o + protected String toJson(Map m) { + assertTrue m instanceof JweHeader + JweHeader header = (JweHeader) m //assert the 5 values have been set per the RFC: assertEquals 5, header.size() @@ -313,7 +314,7 @@ class RFC7517AppendixCTest { //JSON serialization order isn't guaranteed, so now that we've asserted the values are correct, //return the exact serialization order expected in the RFC test: - out.write(RFC_JWE_PROTECTED_HEADER_JSON) + return RFC_JWE_PROTECTED_HEADER_JSON } } @@ -323,7 +324,7 @@ class RFC7517AppendixCTest { .setPayload(RFC_JWK_JSON) .header().contentType('jwk+json').pbes2Count(RFC_P2C).and() .encryptWith(key, alg, enc) - .jsonWriter(writer) //ensure header created as expected with an assertion serializer + .json(ser) //ensure header created as expected with an assertion serializer .compact() assertEquals RFC_COMPACT_JWE, compact diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy index 887d41b1a..128d0c4e6 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy @@ -18,9 +18,10 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwe import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.io.Streams import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.io.Decoders -import io.jsonwebtoken.io.Reader +import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.* import org.junit.Test @@ -43,7 +44,7 @@ class RFC7518AppendixCTest { } private static final Map fromJson(String s) { - return Services.loadFirst(Reader).read(new StringReader(s)) as Map + return Services.loadFirst(Deserializer).deserialize(Streams.stream(s)) as Map } private static EcPrivateJwk readJwk(String json) { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section4Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section4Test.groovy index 057eb9504..7da54f57a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section4Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section4Test.groovy @@ -15,8 +15,8 @@ */ package io.jsonwebtoken.impl.security - import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.io.TestSerializer import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.lang.Strings @@ -203,21 +203,20 @@ class RFC7520Section4Test { def alg = Jwts.SIG.RS256 // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting - // writer here to check the constructed data, and then, after guaranteeing the same data, return + // Serializer here to check the constructed data, and then, after guaranteeing the same data, return // the order expected by the RFC - def writer = new io.jsonwebtoken.io.Writer>() { + def writer = new TestSerializer() { @Override - void write(Writer out, Map m) throws IOException { + protected String toJson(Map m) { assertEquals 2, m.size() assertEquals alg.getId(), m.get('alg') assertEquals jwk.getId(), m.get('kid') - //everything has been asserted per the RFC - write the exact order as shown in the RFC: - out.write(FIGURE_9) + //everything has been asserted per the RFC - reflect the exact order as shown in the RFC: + return FIGURE_9 } } - String result = Jwts.builder() - .jsonWriter(writer) // assert input, return RFC ordered string + .json(writer) // assert input, return RFC ordered string .header().keyId(jwk.getId()).and() .setPayload(FIGURE_7) .signWith(key, alg) @@ -242,21 +241,21 @@ class RFC7520Section4Test { String kid = 'bilbo.baggins@hobbiton.example' // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting - // writer here to check the constructed data, and then, after guaranteeing the same data, return + // Serializer here to check the constructed data, and then, after guaranteeing the same data, return // the order expected by the RFC - def writer = new io.jsonwebtoken.io.Writer>() { + def ser = new TestSerializer() { @Override - void write(Writer out, Map m) throws IOException { + protected String toJson(Map m) { assertEquals 2, m.size() assertEquals alg.getId(), m.get('alg') assertEquals kid, m.get('kid') //everything has been asserted per the RFC - return the exact order as shown in the RFC: - out.write(FIGURE_16) + return FIGURE_16 } } String result = Jwts.builder() - .jsonWriter(writer) // assert input, return RFC ordered string + .json(ser) // assert input, return RFC ordered string .header().keyId(kid).and() .setPayload(FIGURE_7) .signWith(key, alg) @@ -288,21 +287,21 @@ class RFC7520Section4Test { def alg = Jwts.SIG.ES512 // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting - // writer here to check the constructed data, and then, after guaranteeing the same data, return + // Serializer here to check the constructed data, and then, after guaranteeing the same data, return // the order expected by the RFC - def writer = new io.jsonwebtoken.io.Writer>() { + def ser = new TestSerializer() { @Override - void write(Writer out, Map m) throws IOException { + protected String toJson(Map m) { assertEquals 2, m.size() assertEquals alg.getId(), m.get('alg') assertEquals jwk.getId(), m.get('kid') //everything has been asserted per the RFC - return the exact order as shown in the RFC: - out.write(FIGURE_23) + return FIGURE_23 } } String result = Jwts.builder() - .jsonWriter(writer) // assert input, return RFC ordered string + .json(ser) // assert input, return RFC ordered string .header().keyId(jwk.getId()).and() .setPayload(FIGURE_7) .signWith(key, alg) @@ -333,21 +332,21 @@ class RFC7520Section4Test { def alg = Jwts.SIG.HS256 // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting - // writer here to check the constructed data, and then, after guaranteeing the same data, return + // Serializer here to check the constructed data, and then, after guaranteeing the same data, return // the order expected by the RFC - def writer = new io.jsonwebtoken.io.Writer>() { + def writer = new TestSerializer() { @Override - void write(Writer out, Map m) throws IOException { + protected String toJson(Map m) { assertEquals 2, m.size() assertEquals alg.getId(), m.get('alg') assertEquals jwk.getId(), m.get('kid') //everything has been asserted per the RFC - return the exact order as shown in the RFC: - out.write(FIGURE_30) + return FIGURE_30 } } String result = Jwts.builder() - .jsonWriter(writer) // assert input, return RFC ordered string + .json(writer) // assert input, return RFC ordered string .header().keyId(jwk.getId()).and() .setPayload(FIGURE_7) .signWith(key, alg) @@ -371,21 +370,21 @@ class RFC7520Section4Test { def alg = Jwts.SIG.HS256 // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting - // writer here to check the constructed data, and then, after guaranteeing the same data, return + // Serializer here to check the constructed data, and then, after guaranteeing the same data, return // the order expected by the RFC - def writer = new io.jsonwebtoken.io.Writer>() { + def writer = new TestSerializer() { @Override - void write(Writer out, Map m) throws IOException { + protected String toJson(Map m) { assertEquals 2, m.size() assertEquals alg.getId(), m.get('alg') assertEquals jwk.getId(), m.get('kid') //everything has been asserted per the RFC - return the exact order as shown in the RFC: - out.write(FIGURE_37) + return FIGURE_37 } } String result = Jwts.builder() - .jsonWriter(writer) // assert input, return RFC ordered string + .json(writer) // assert input, return RFC ordered string .header().keyId(jwk.getId()).and() .setPayload(FIGURE_7) .signWith(key, alg) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section5Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section5Test.groovy index ffa293617..b641234cc 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section5Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section5Test.groovy @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.io.TestSerializer import io.jsonwebtoken.impl.lang.CheckedFunction import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders @@ -456,22 +457,20 @@ class RFC7520Section5Test { } // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting - // writer here to check the constructed data, and then, after guaranteeing the same data, return + // Serializer here to check the constructed data, and then, after guaranteeing the same data, return // the order expected by the RFC - def writer = new io.jsonwebtoken.io.Writer>() { + def ser = new TestSerializer() { @Override - void write(Writer out, Map m) throws IOException { + protected String toJson(Map m) { assertEquals 3, m.size() assertEquals alg.getId(), m.get('alg') assertEquals jwk.getId(), m.get('kid') assertEquals enc.getId(), m.get('enc') - //everything has been asserted per the RFC - return the exact order as shown in the RFC: - out.write(FIGURE_77) + return FIGURE_77 } } - String result = Jwts.builder() - .jsonWriter(writer) // assert input, return RFC ordered string + .json(ser) // assert input, return RFC ordered string .header().keyId(jwk.getId()).and() .setPayload(FIGURE_72) .encryptWith(key, alg, enc) @@ -522,20 +521,19 @@ class RFC7520Section5Test { // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting // writer here to check the constructed data, and then, after guaranteeing the same data, return // the order expected by the RFC - def writer = new io.jsonwebtoken.io.Writer>() { + def ser = new TestSerializer() { @Override - void write(Writer out, Map m) throws IOException { + protected String toJson(Map m) { assertEquals 3, m.size() assertEquals alg.getId(), m.get('alg') assertEquals jwk.getId(), m.get('kid') assertEquals enc.getId(), m.get('enc') - //everything has been asserted per the RFC - return the exact order as shown in the RFC: - out.write(FIGURE_88) + return FIGURE_88 } } String result = Jwts.builder() - .jsonWriter(writer) // assert input, return RFC ordered string + .json(ser) // assert input, return RFC ordered string .header().keyId(jwk.getId()).and() .setPayload(FIGURE_72) .encryptWith(key, alg, enc) @@ -581,22 +579,21 @@ class RFC7520Section5Test { // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting // writer here to check the constructed data, and then, after guaranteeing the same data, return // the order expected by the RFC - def writer = new io.jsonwebtoken.io.Writer>() { + def ser = new TestSerializer() { @Override - void write(Writer out, Map m) throws IOException { + protected String toJson(Map m) { assertEquals 5, m.size() assertEquals alg.getId(), m.get('alg') assertEquals FIGURE_99, m.get('p2s') assertEquals p2c, m.get('p2c') assertEquals cty, m.get('cty') assertEquals enc.getId(), m.get('enc') - //everything has been asserted per the RFC - return the exact order as shown in the RFC: - out.write(FIGURE_101) + return FIGURE_101 } } String result = Jwts.builder() - .jsonWriter(writer) // assert input, return RFC ordered string + .json(ser) // assert input, return RFC ordered string .header().contentType(cty).pbes2Count(p2c).and() .setPayload(FIGURE_95) .encryptWith(key, alg, enc) @@ -647,21 +644,20 @@ class RFC7520Section5Test { // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting // writer here to check the constructed data, and then, after guaranteeing the same data, return // the order expected by the RFC - def writer = new io.jsonwebtoken.io.Writer>() { + def ser = new TestSerializer() { @Override - void write(Writer out, Map m) throws IOException { + protected String toJson(Map m) { assertEquals 4, m.size() assertEquals alg.getId(), m.get('alg') assertEquals jwk.getId(), m.get('kid') assertEquals enc.getId(), m.get('enc') assertEquals RFC_EPK.toPublicJwk(), m.get('epk') - //everything has been asserted per the RFC - return the exact order as shown in the RFC: - out.write(FIGURE_113) + return FIGURE_113 } } String result = Jwts.builder() - .jsonWriter(writer) // assert input, return RFC ordered string + .json(ser) // assert input, return RFC ordered string .header().keyId(jwk.getId()).and() .setPayload(FIGURE_72) .encryptWith(encKey, alg, enc)