Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#719] JSON-B extension. #720

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1269,11 +1269,27 @@ They are checked in order, and the first one found is used:
Android applications _unless_ you want to use POJOs as claims. The `org.json` library supports simple
Object-to-JSON marshaling, but it *does not* support JSON-to-Object unmarshalling.

4. JSON-B: This will automatically be used if you specify `io.jsonwebtoken:jjwt-jsonb` as a project runtime dependency.
JSON-B also supports POJOs as claims with full marshaling/unmarshaling as necessary.

**NOTE**: `JSON-B` is just a specification and does not bring an implementation.

* In Java-SE environments you will need to add three more dependencies:
1. `jakarta.json:jakarta.json-api`
2. `jakarta.json.bind:jakarta.json.bind-api`
3. A JSON-B compliant implementation like [Eclipse Yasson](https://github.com/eclipse-ee4j/yasson) or [Apache Johnzon](https://johnzon.apache.org/johnzon-jsonb/index.html).
* In Java/Jakarta EE environments, you might need to enable the following features:
1. json-p / json-api
2. json-b / json-bind

**If you want to use POJOs as claim values, use either the `io.jsonwebtoken:jjwt-jackson` or
`io.jsonwebtoken:jjwt-gson` dependency** (or implement your own Serializer and Deserializer if desired). **But beware**,
bdemers marked this conversation as resolved.
Show resolved Hide resolved
Jackson will force a sizable (> 1 MB) dependency to an Android application thus increasing the app download size for
mobile users.

If you want to use POJOs and a JSON-B compliant specification _**or**_ you want to use JJWT on a JakartaEE compliant application server, use
`io.jsonwebtoken:jjwt-jsonb`.

bmarwell marked this conversation as resolved.
Show resolved Hide resolved
<a name="json-custom"></a>
### Custom JSON Processor

Expand Down
1 change: 1 addition & 0 deletions extensions/jsonb/bnd.bnd
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fragment-Host: io.jsonwebtoken.jjwt-api
47 changes: 47 additions & 0 deletions extensions/jsonb/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-root</artifactId>
<version>0.11.3-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<artifactId>jjwt-jsonb</artifactId>
<name>JJWT :: Extensions :: JSON-B</name>
<packaging>jar</packaging>

<properties>
<jjwt.root>${basedir}/../..</jjwt.root>
<!-- JSON-B uses static methods in interfaces. -->
<jdk.version>8</jdk.version>
</properties>

<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.json.bind</groupId>
<artifactId>jakarta.json.bind-api</artifactId>
<scope>provided</scope>
</dependency>

<!-- Test dependency -->
<dependency>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-jsonb</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (C) 2014 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.jsonb.io;

import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;

import javax.json.bind.Jsonb;
import javax.json.bind.JsonbException;
import java.nio.charset.StandardCharsets;

import static java.util.Objects.requireNonNull;

/**
* @since 0.10.0
bmarwell marked this conversation as resolved.
Show resolved Hide resolved
*/
public class JsonbDeserializer<T> implements Deserializer<T> {

private final Class<T> returnType;
private final Jsonb jsonb;

@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
public JsonbDeserializer() {
this(JsonbSerializer.DEFAULT_JSONB);
}

@SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom ObjectMapper
public JsonbDeserializer(Jsonb jsonb) {
this(jsonb, (Class<T>) Object.class);
}

private JsonbDeserializer(Jsonb jsonb, Class<T> returnType) {
requireNonNull(jsonb, "ObjectMapper cannot be null.");
requireNonNull(returnType, "Return type cannot be null.");
this.jsonb = jsonb;
this.returnType = returnType;
}

@Override
public T deserialize(byte[] bytes) throws DeserializationException {
try {
return readValue(bytes);
} catch (JsonbException jsonbException) {
String msg = "Unable to deserialize bytes into a " + returnType.getName() + " instance: " + jsonbException.getMessage();
throw new DeserializationException(msg, jsonbException);
}
}

protected T readValue(byte[] bytes) {
return jsonb.fromJson(new String(bytes, StandardCharsets.UTF_8), returnType);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2014 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.jsonb.io;

import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.lang.Assert;

import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.JsonbException;
import java.nio.charset.StandardCharsets;

import static java.util.Objects.requireNonNull;

/**
* @since 0.10.0
bmarwell marked this conversation as resolved.
Show resolved Hide resolved
*/
public class JsonbSerializer<T> implements Serializer<T> {

static final Jsonb DEFAULT_JSONB = JsonbBuilder.create();

private final Jsonb jsonb;

@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
public JsonbSerializer() {
this(DEFAULT_JSONB);
}

@SuppressWarnings("WeakerAccess") //intended for end-users to use when providing a custom ObjectMapper
public JsonbSerializer(Jsonb jsonb) {
requireNonNull(jsonb, "Jsonb cannot be null.");
this.jsonb = jsonb;
}

@Override
public byte[] serialize(T t) throws SerializationException {
Assert.notNull(t, "Object to serialize cannot be null.");
try {
return writeValueAsBytes(t);
} catch (JsonbException jsonbException) {
String msg = "Unable to serialize object: " + jsonbException.getMessage();
throw new SerializationException(msg, jsonbException);
}
}

@SuppressWarnings("WeakerAccess") //for testing
protected byte[] writeValueAsBytes(T t) {
final Object obj;

if (t instanceof byte[]) {
obj = Encoders.BASE64.encode((byte[]) t);
} else if (t instanceof char[]) {
obj = new String((char[]) t);
} else {
obj = t;
}

return this.jsonb.toJson(obj).getBytes(StandardCharsets.UTF_8);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.jsonwebtoken.jsonb.io.JsonbDeserializer
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.jsonwebtoken.jsonb.io.JsonbSerializer
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.jsonwebtoken.jsonb.io

import io.jsonwebtoken.io.DeserializationException
import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.lang.Strings
import org.junit.Test

import javax.json.bind.JsonbBuilder

import static org.easymock.EasyMock.*
import static org.hamcrest.CoreMatchers.instanceOf
import static org.hamcrest.MatcherAssert.assertThat
import static org.junit.Assert.*

class JsonbDeserializerTest {

@Test
void loadService() {
def deserializer = ServiceLoader.load(Deserializer).iterator().next()
assertThat(deserializer, instanceOf(JsonbDeserializer))
}


@Test
void testDefaultConstructor() {
def deserializer = new JsonbDeserializer()
assertNotNull deserializer.jsonb
}

@Test
void testObjectMapperConstructor() {
def customJsonb = JsonbBuilder.create()
def deserializer = new JsonbDeserializer(customJsonb)
assertSame customJsonb, deserializer.jsonb
}

@Test(expected = NullPointerException)
void testObjectMapperConstructorWithNullArgument() {
new JsonbDeserializer<>(null)
}

@Test
void testDeserialize() {
byte[] serialized = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
def expected = [hello: '世界']
def result = new JsonbDeserializer().deserialize(serialized)
assertEquals expected, result
}

@Test
void testDeserializeFailsWithJsonProcessingException() {

def ex = createMock javax.json.bind.JsonbException

expect(ex.getMessage()).andReturn('foo')

def deserializer = new JsonbDeserializer() {
@Override
protected Object readValue(byte[] bytes) throws javax.json.bind.JsonbException {
throw ex
}
}

replay ex

try {
deserializer.deserialize('{"hello":"世界"}'.getBytes(Strings.UTF_8))
fail()
} catch (DeserializationException se) {
assertEquals 'Unable to deserialize bytes into a java.lang.Object instance: foo', se.getMessage()
assertSame ex, se.getCause()
}

verify ex
}
}
Loading