You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by re...@apache.org on 2020/12/30 02:06:22 UTC

[cxf] branch master updated: CXF-8343: Add JSON-B support

This is an automated email from the ASF dual-hosted git repository.

reta pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/master by this push:
     new 2696ea2  CXF-8343: Add JSON-B support
2696ea2 is described below

commit 2696ea28b67f21b310e400b990da50016b62bd90
Author: reta <dr...@gmail.com>
AuthorDate: Tue Dec 29 21:05:59 2020 -0500

    CXF-8343: Add JSON-B support
---
 rt/rs/extensions/providers/pom.xml                 |  12 +-
 .../jaxrs/provider/jsrjsonb/JsrJsonbProvider.java  | 108 +++++++++
 .../provider/jsrjsonb/JsrJsonbProviderTest.java    | 133 +++++++++++
 systests/jaxrs/pom.xml                             |  17 +-
 .../apache/cxf/systest/jaxrs/provider/Book.java    |   4 +
 .../cxf/systest/jaxrs/provider/BookJsonStore2.java |  77 ++++++
 .../jaxrs/provider/JsrJsonbProviderTest.java       | 259 +++++++++++++++++++++
 7 files changed, 605 insertions(+), 5 deletions(-)

diff --git a/rt/rs/extensions/providers/pom.xml b/rt/rs/extensions/providers/pom.xml
index 3aa2337..ab257ff 100644
--- a/rt/rs/extensions/providers/pom.xml
+++ b/rt/rs/extensions/providers/pom.xml
@@ -119,7 +119,12 @@
             <groupId>org.glassfish</groupId>
             <artifactId>javax.json</artifactId>
             <optional>true</optional>
-        </dependency>     
+        </dependency>
+        <dependency>
+            <groupId>jakarta.json.bind</groupId>
+            <artifactId>jakarta.json.bind-api</artifactId>
+            <optional>true</optional>
+        </dependency>
         <dependency>
             <groupId>org.apache.cxf</groupId>
             <artifactId>cxf-rt-bindings-soap</artifactId>
@@ -147,6 +152,11 @@
             <artifactId>easymock</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.johnzon</groupId>
+            <artifactId>johnzon-jsonb</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     
 </project>
diff --git a/rt/rs/extensions/providers/src/main/java/org/apache/cxf/jaxrs/provider/jsrjsonb/JsrJsonbProvider.java b/rt/rs/extensions/providers/src/main/java/org/apache/cxf/jaxrs/provider/jsrjsonb/JsrJsonbProvider.java
new file mode 100644
index 0000000..621d667
--- /dev/null
+++ b/rt/rs/extensions/providers/src/main/java/org/apache/cxf/jaxrs/provider/jsrjsonb/JsrJsonbProvider.java
@@ -0,0 +1,108 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.cxf.jaxrs.provider.jsrjsonb;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.json.JsonException;
+import javax.json.bind.Jsonb;
+import javax.json.bind.JsonbBuilder;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.cxf.jaxrs.utils.ExceptionUtils;
+
+/**
+ * 11.2.7 Java API for JSON Binding (JSR-370)
+ * 
+ * In a product that supports the Java API for JSON Binding (JSON-B) [19], implementations MUST support
+ * entity providers for all Java types supported by JSON-B in combination with the following media
+ * types: application/json, text/json as well as any other media types matching "*"/json or "*"/"*"+json".
+ *
+ * Note that if JSON-B and JSON-P are both supported in the same environment, entity providers for 
+ * JSON-B take precedence over those for JSON-P for all types except JsonValue and its sub-types.
+*/
+@Produces({"application/json", "text/json", "application/*+json" })
+@Consumes({"application/json", "text/json", "application/*+json" })
+@Provider
+public class JsrJsonbProvider implements MessageBodyReader<Object>, MessageBodyWriter<Object> {
+    private final Jsonb jsonb;
+     
+    public JsrJsonbProvider() {
+        this(JsonbBuilder.create()); 
+    }
+    
+    public JsrJsonbProvider(Jsonb jsonb) {
+        this.jsonb = jsonb; 
+    }
+    
+    @Override
+    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        return isSupportedMediaType(mediaType);
+    }
+
+    @Override
+    public void writeTo(Object t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
+            MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) 
+                throws IOException, WebApplicationException {
+        jsonb.toJson(t, type, entityStream);
+    }
+
+    @Override
+    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        return isSupportedMediaType(mediaType);
+    }
+
+    @Override
+    public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
+            MultivaluedMap<String, String> httpHeaders, InputStream entityStream) 
+                throws IOException, WebApplicationException {
+        try {
+            if (genericType == null) {
+                return jsonb.fromJson(entityStream, type);
+            } else {
+                return jsonb.fromJson(entityStream, genericType);
+            }
+        } catch (JsonException ex) {
+            throw ExceptionUtils.toBadRequestException(ex, null);
+        }
+    }
+    
+    protected boolean isSupportedMediaType(MediaType mediaType) {
+        if (mediaType != null) {
+            final String subtype = mediaType.getSubtype();
+            return "json".equalsIgnoreCase(subtype) || subtype.endsWith("+json");
+        } else {
+            // Return 'false' if no media type has been specified
+            return false;
+        }
+    }
+
+}
diff --git a/rt/rs/extensions/providers/src/test/java/org/apache/cxf/jaxrs/provider/jsrjsonb/JsrJsonbProviderTest.java b/rt/rs/extensions/providers/src/test/java/org/apache/cxf/jaxrs/provider/jsrjsonb/JsrJsonbProviderTest.java
new file mode 100644
index 0000000..8515bc5
--- /dev/null
+++ b/rt/rs/extensions/providers/src/test/java/org/apache/cxf/jaxrs/provider/jsrjsonb/JsrJsonbProviderTest.java
@@ -0,0 +1,133 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.cxf.jaxrs.provider.jsrjsonb;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+
+import org.apache.cxf.jaxrs.resources.Book;
+import org.apache.cxf.jaxrs.resources.CollectionsResource;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertThrows;
+
+public class JsrJsonbProviderTest {
+    private JsrJsonbProvider provider;
+    
+    @Before
+    public void setUp() {
+        provider = new JsrJsonbProvider();
+    }
+    
+    @Test
+    public void testReadMalformedJson() throws Exception {
+        final byte[] bytes = "junk".getBytes();
+        final WebApplicationException ex = assertThrows(WebApplicationException.class, () -> read(Book.class, bytes));
+        assertThat(ex.getResponse().getStatus(), equalTo(Response.Status.BAD_REQUEST.getStatusCode()));
+    }
+
+    @Test
+    public void testReadListOfBooks() throws Exception {
+        final String input = "["
+           + "{"
+           + "    \"name\":\"CXF 1\""
+           + "},"
+           + "{"
+           + "    \"name\":\"CXF 2\""
+           + "}"
+           + "]";
+        
+        final Method m = CollectionsResource.class.getMethod("getBooks", new Class[]{});
+        final List<Book> books = read(m.getReturnType(), m.getGenericReturnType(), input.getBytes());
+        
+        assertThat(books.size(), equalTo(2));
+        assertThat(books.get(0).getName(), equalTo("CXF 1"));
+        assertThat(books.get(1).getName(), equalTo("CXF 2"));
+    }
+    
+    @Test
+    public void testWriteBook() throws Exception {
+        final Book book = new Book("CXF 1", 1);
+        final String payload = write(book, Book.class);
+        
+        assertThat(payload, equalTo("{\"id\":1,\"name\":\"CXF 1\",\"state\":\"\"}"));
+    }
+    
+    @Test
+    public void testReadBook() throws Exception {
+        String input = "{"
+            +     "\"id\":1,"
+            +     "\"name\":\"CXF 1\""
+            + "}";
+        
+        final Book book = read(Book.class, input.getBytes());
+        assertThat(book.getId(), equalTo(1L));
+        assertThat(book.getName(), equalTo("CXF 1"));
+    }
+    
+    @Test
+    public void testWriteListOfBooks() throws Exception {
+        final List<Book> books = new ArrayList<>();
+        books.add(new Book("CXF 1", 1));
+        books.add(new Book("CXF 2", 2));
+
+        final Method m = CollectionsResource.class.getMethod("setBooksArray", new Class[]{Book[].class});
+        final String payload = write(books, Book.class, m.getGenericParameterTypes()[0]);
+        
+        assertThat(payload,
+            equalTo("[{\"id\":1,\"name\":\"CXF 1\",\"state\":\"\"},{\"id\":2,\"name\":\"CXF 2\",\"state\":\"\"}]"));
+    }
+    
+    private <T> T read(Class<?> clazz, byte[] bytes) throws IOException {
+        return read(clazz, null, bytes);
+    }
+    
+    @SuppressWarnings("unchecked")
+    private <T> T read(Class<?> clazz, Type genericType, byte[] bytes) throws IOException {
+        try (ByteArrayInputStream in = new ByteArrayInputStream(bytes)) {
+            return (T)provider.readFrom((Class<Object>)clazz, genericType, null, null, null, in);
+        }
+    }
+    
+    private String write(Object value, Class<?> clazz) throws IOException {
+        return write(value, clazz, null);
+    }
+    
+    @SuppressWarnings("unchecked")
+    private String write(Object value, Class<?> clazz, Type genericType) throws IOException {
+        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            provider.writeTo(value, (Class<Object>)clazz, genericType, null, null, null, out);
+            return out.toString(StandardCharsets.UTF_8.name());
+        }
+    }
+}
diff --git a/systests/jaxrs/pom.xml b/systests/jaxrs/pom.xml
index 9a834c9..4bafbe6 100644
--- a/systests/jaxrs/pom.xml
+++ b/systests/jaxrs/pom.xml
@@ -500,9 +500,9 @@
             </exclusions>
         </dependency>
         <dependency>
-                <groupId>org.bouncycastle</groupId>
-                <artifactId>bcprov-jdk15on</artifactId>
-                <scope>test</scope>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15on</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
            <groupId>org.yaml</groupId>
@@ -554,7 +554,16 @@
                 </exclusion>
             </exclusions>
         </dependency>
-        
+        <dependency>
+            <groupId>jakarta.json.bind</groupId>
+            <artifactId>jakarta.json.bind-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.johnzon</groupId>
+            <artifactId>johnzon-jsonb</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
diff --git a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/provider/Book.java b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/provider/Book.java
index fcc074b..38ec370 100644
--- a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/provider/Book.java
+++ b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/provider/Book.java
@@ -59,4 +59,8 @@ public class Book {
     public Collection<BookChapter> getChapters() {
         return chapters.values();
     }
+    
+    public void setChapters(Collection<BookChapter> value) {
+        value.forEach(c -> chapters.put(c.getId(), c));
+    }
 }
\ No newline at end of file
diff --git a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/provider/BookJsonStore2.java b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/provider/BookJsonStore2.java
new file mode 100644
index 0000000..5579792
--- /dev/null
+++ b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/provider/BookJsonStore2.java
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.cxf.systest.jaxrs.provider;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+@Path("/bookstore2/")
+public class BookJsonStore2 {
+    private Map< Long, Book > books = new HashMap<>();
+
+    @GET
+    @Path("/books/{bookId}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Book getBook(@PathParam("bookId") Long id) {
+        final Book book = books.get(id);
+
+        if (book == null) {
+            return null;
+        }
+
+        return book;
+    }
+
+    @GET
+    @Path("/books")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Collection<Book> getBooks() {
+        return books.values();
+    }
+
+    @POST
+    @Path("/books")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Book addBook(@Context final UriInfo uriInfo, Book book) {
+        books.put(book.getId(), book);
+        return book;
+    }
+
+    @DELETE
+    @Path("/books")
+    public Response deleteAll() {
+        books.clear();
+        return Response.ok().build();
+    }
+}
diff --git a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/provider/JsrJsonbProviderTest.java b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/provider/JsrJsonbProviderTest.java
new file mode 100644
index 0000000..ee19064
--- /dev/null
+++ b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/provider/JsrJsonbProviderTest.java
@@ -0,0 +1,259 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.cxf.systest.jaxrs.provider;
+
+import java.util.Arrays;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
+import org.apache.cxf.jaxrs.model.AbstractResourceInfo;
+import org.apache.cxf.jaxrs.provider.jsrjsonb.JsrJsonbProvider;
+import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase;
+import org.apache.cxf.testutil.common.AbstractBusTestServerBase;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class JsrJsonbProviderTest extends AbstractBusClientServerTestBase {
+    public static final String PORT = allocatePort(JsrJsonbProviderTest.class);
+
+    @Ignore
+    public static class Server extends AbstractBusTestServerBase {
+        protected void run() {
+            final JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
+            sf.setResourceClasses(BookJsonStore.class, BookJsonStore2.class);
+            sf.setResourceProvider(BookJsonStore.class,
+                new SingletonResourceProvider(new BookJsonStore()));
+            sf.setResourceProvider(BookJsonStore2.class,
+                new SingletonResourceProvider(new BookJsonStore2()));
+            sf.setProvider(new JsrJsonbProvider());
+            sf.setAddress("http://localhost:" + PORT + "/");
+            sf.create();
+        }
+
+        public static void main(String[] args) {
+            try {
+                Server s = new Server();
+                s.start();
+            } catch (Exception ex) {
+                ex.printStackTrace();
+                System.exit(-1);
+            } finally {
+                System.out.println("done!");
+            }
+        }
+    }
+
+    @BeforeClass
+    public static void startServers() throws Exception {
+        AbstractResourceInfo.clearAllMaps();
+        //keep out of process due to stack traces testing failures
+        assertTrue("server did not launch correctly", launchServer(Server.class, true));
+        createStaticBus();
+    }
+
+    @Before
+    public void setUp() {
+        final Response r = createWebClient("/bookstore/books").delete();
+        assertEquals(Status.OK.getStatusCode(), r.getStatus());
+    }
+
+    @Test
+    public void testNoResultsAreReturned() throws Exception {
+        final Response r = createWebClient("/bookstore/books/155").get();
+        assertEquals(Status.NO_CONTENT.getStatusCode(), r.getStatus());
+    }
+
+    @Test
+    public void testPostSimpleJsonObject() {
+        final Response r = createWebClient("/bookstore/books")
+            .header("Content-Type", MediaType.APPLICATION_JSON)
+            .post(
+                Json
+                    .createObjectBuilder()
+                    .add("id", 1)
+                    .add("name", "Book 1")
+                    .build()
+            );
+        assertEquals(Status.CREATED.getStatusCode(), r.getStatus());
+    }
+
+    @Test
+    public void testPostComplexJsonObject() {
+        final Response r = createWebClient("/bookstore/books")
+            .header("Content-Type", MediaType.APPLICATION_JSON)
+            .post(
+                Json
+                    .createObjectBuilder()
+                    .add("id", 1)
+                    .add("name", "Book 1")
+                    .add("chapters",
+                        Json.createArrayBuilder()
+                            .add(
+                                Json.createObjectBuilder()
+                                    .add("id", 1)
+                                    .add("title", "Chapter 1")
+                            )
+                            .add(
+                                Json.createObjectBuilder()
+                                    .add("id", 2)
+                                    .add("title", "Chapter 2")
+                            )
+                    )
+                    .build()
+            );
+        assertEquals(Status.CREATED.getStatusCode(), r.getStatus());
+    }
+
+    @Test
+    public void testPostAndGetSimpleJsonObject() {
+        testPostSimpleJsonObject();
+
+        final Response r = createWebClient("/bookstore/books/1").get();
+        assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+        JsonObject obj = r.readEntity(JsonObject.class);
+        assertThat(obj.getInt("id"), equalTo(1));
+        assertThat(obj.getString("name"), equalTo("Book 1"));
+        assertThat(obj.get("chapters"), nullValue());
+    }
+
+    @Test
+    public void testPostAndGetComplexJsonObject() {
+        testPostComplexJsonObject();
+
+        final Response r = createWebClient("/bookstore/books/1").get();
+        assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+        JsonObject obj = r.readEntity(JsonObject.class);
+        assertThat(obj.getInt("id"), equalTo(1));
+        assertThat(obj.getString("name"), equalTo("Book 1"));
+        assertThat(obj.get("chapters"), instanceOf(JsonArray.class));
+
+        final JsonArray chapters = (JsonArray)obj.get("chapters");
+        assertThat(chapters.size(), equalTo(2));
+        assertThat(((JsonObject)chapters.get(0)).getInt("id"), equalTo(1));
+        assertThat(((JsonObject)chapters.get(0)).getString("title"), equalTo("Chapter 1"));
+        assertThat(((JsonObject)chapters.get(1)).getInt("id"), equalTo(2));
+        assertThat(((JsonObject)chapters.get(1)).getString("title"), equalTo("Chapter 2"));
+    }
+
+    @Test
+    public void testPostAndGetJsonBooks() {
+        testPostSimpleJsonObject();
+
+        final Response r = createWebClient("/bookstore/books").get();
+        assertEquals(Status.OK.getStatusCode(), r.getStatus());
+
+        final JsonArray obj = r.readEntity(JsonArray.class);
+        assertThat(obj.size(), equalTo(1));
+        assertThat(obj.get(0), instanceOf(JsonObject.class));
+
+        assertThat(((JsonObject)obj.get(0)).getInt("id"), equalTo(1));
+        assertThat(((JsonObject)obj.get(0)).getString("name"), equalTo("Book 1"));
+    }
+
+    @Test
+    public void testPostBadJsonObject() {
+        final Response r = createWebClient("/bookstore/books")
+            .header("Content-Type", MediaType.APPLICATION_JSON)
+            .post("blabla");
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), r.getStatus());
+    }
+    
+    @Test
+    public void testPostBook() {
+        final Book response = createWebClient("/bookstore2/books")
+            .header("Content-Type", MediaType.APPLICATION_JSON)
+            .post(new Book("Book 1", 1L))
+            .readEntity(Book.class);
+        
+        assertThat(response.getId(), equalTo(1L));
+        assertThat(response.getName(), equalTo("Book 1"));
+    }
+
+    @Test
+    public void testPostBookWithChapters() {
+        final Book book = new Book("Book 1", 1L);
+        book.addChapter(1L, "Chapter 1");
+        book.addChapter(2L, "Chapter 2");
+        
+        final Book response = createWebClient("/bookstore2/books")
+            .header("Content-Type", MediaType.APPLICATION_JSON)
+            .post(book)
+            .readEntity(Book.class);
+        assertThat(response.getId(), equalTo(1L));
+        assertThat(response.getName(), equalTo("Book 1"));
+        assertThat(response.getChapters().size(), equalTo(2));
+    }
+
+    @Test
+    public void testPostAndGetBook() {
+        testPostBook();
+
+        final Book book = createWebClient("/bookstore2/books/1").get(Book.class);
+        assertThat(book.getId(), equalTo(1L));
+        assertThat(book.getName(), equalTo("Book 1"));
+        assertThat(book.getChapters(), hasItems());
+    }
+    
+    @Test
+    public void testPostAndGetBooks() {
+        testPostBookWithChapters();
+
+        final Book[] books = createWebClient("/bookstore2/books").get(Book[].class);
+        assertThat(books.length, equalTo(1));
+        assertThat(books[0].getId(), equalTo(1L));
+        assertThat(books[0].getName(), equalTo("Book 1"));
+        assertThat(books[0].getChapters().size(), equalTo(2));
+    }
+    
+    @Test
+    public void testGetBook() {
+        final Book book = createWebClient("/bookstore2/books/100").get(Book.class);
+        assertThat(book, nullValue());
+    }
+
+    private static WebClient createWebClient(final String url) {
+        return WebClient
+            .create("http://localhost:" + PORT + url,
+                Arrays.< Object >asList(new JsrJsonbProvider()))
+            .accept(MediaType.APPLICATION_JSON);
+    }
+
+}