All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ratpack.jackson.Jackson Maven / Gradle / Ivy

There is a newer version: 2.0.0-rc-1
Show newest version
/*
 * Copyright 2013 the original author or authors.
 *
 * 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 ratpack.jackson;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.common.reflect.TypeToken;
import io.netty.buffer.Unpooled;
import org.reactivestreams.Publisher;
import ratpack.api.Nullable;
import ratpack.func.Function;
import ratpack.http.ResponseChunks;
import ratpack.http.internal.HttpHeaderConstants;
import ratpack.jackson.internal.DefaultJsonParseOpts;
import ratpack.jackson.internal.DefaultJsonRender;
import ratpack.parse.Parse;
import ratpack.registry.Registry;
import ratpack.stream.Streams;
import ratpack.stream.WriteStream;

import java.io.IOException;
import java.io.OutputStream;

/**
 * Provides key integration points with the Jackson support for dealing with JSON.
 *
 * 

Rendering as JSON

*

* The methods that return a {@link JsonRender} are to be used with the {@link ratpack.handling.Context#render(Object)} method for serializing objects to the response as JSON. *

{@code
 * import ratpack.test.embed.EmbeddedApp;
 * import ratpack.jackson.Jackson;
 * import ratpack.http.client.ReceivedResponse;
 * import com.fasterxml.jackson.databind.ObjectMapper;
 *
 * import static ratpack.jackson.Jackson.json;
 * import static org.junit.Assert.*;
 *
 * public class Example {
 *
 *   public static class Person {
 *     private final String name;
 *     public Person(String name) {
 *       this.name = name;
 *     }
 *     public String getName() {
 *       return name;
 *     }
 *   }
 *
 *   public static void main(String... args) throws Exception {
 *     EmbeddedApp.of(s -> s
 *       .handlers(chain ->
 *         chain.get(ctx -> ctx.render(json(new Person("John"))))
 *       )
 *     ).test(httpClient -> {
 *       ReceivedResponse response = httpClient.get();
 *       assertEquals("{\"name\":\"John\"}", response.getBody().getText());
 *       assertEquals("application/json", response.getBody().getContentType().getType());
 *     });
 *   }
 * }
 * }
* *

Streaming JSON and events

*

* There are several options for streaming JSON data and events. *

* The {@link #chunkedJsonList(Registry, Publisher)} method can be used for rendering a very large JSON stream/list without buffering the entire list in memory. *

* It is also easy to render {@link ratpack.sse.ServerSentEvents server sent events}, which can be useful for real time applications and infinite data streams. *

{@code
 * import ratpack.test.embed.EmbeddedApp;
 * import ratpack.jackson.Jackson;
 * import ratpack.stream.Streams;
 * import ratpack.http.client.ReceivedResponse;
 * import com.fasterxml.jackson.databind.ObjectMapper;
 * import org.reactivestreams.Publisher;
 *
 * import java.util.Arrays;
 *
 * import static ratpack.jackson.Jackson.toJson;
 * import static ratpack.sse.ServerSentEvents.serverSentEvents;
 * import static java.util.stream.Collectors.joining;
 * import static org.junit.Assert.*;
 *
 * public class Example {
 *
 *   public static class Person {
 *     private final String name;
 *     public Person(String name) {
 *       this.name = name;
 *     }
 *     public String getName() {
 *       return name;
 *     }
 *   }
 *
 *   public static void main(String... args) throws Exception {
 *     EmbeddedApp.of(s -> s
 *       .handlers(chain -> chain
 *         .get("stream", ctx -> {
 *           Publisher people = Streams.publish(Arrays.asList(
 *             new Person("a"),
 *             new Person("b"),
 *             new Person("c")
 *           ));
 *
 *           ctx.render(serverSentEvents(people, e -> e.data(toJson(ctx))));
 *         })
 *       )
 *     ).test(httpClient -> {
 *       ReceivedResponse response = httpClient.get("stream");
 *       assertEquals("text/event-stream;charset=UTF-8", response.getHeaders().get("Content-Type"));
 *
 *       String expectedOutput = Arrays.asList("a", "b", "c")
 *         .stream()
 *         .map(i -> "data: {\"name\":\"" + i + "\"}\n")
 *         .collect(joining("\n"))
 *         + "\n";
 *
 *       assertEquals(expectedOutput, response.getBody().getText());
 *     });
 *   }
 * }
 * }
*

* A similar approach could be used directly with the {@link ratpack.http.Response#sendStream(Publisher)} method for a custom “protocol”. * *

Parsing JSON requests

*

* The methods that return a {@link Parse} are to be used with the {@link ratpack.handling.Context#parse(ratpack.parse.Parse)} method for deserializing request bodies containing JSON. *

{@code
 * import ratpack.test.embed.EmbeddedApp;
 * import ratpack.jackson.Jackson;
 * import ratpack.http.client.ReceivedResponse;
 * import com.fasterxml.jackson.databind.JsonNode;
 * import com.fasterxml.jackson.databind.ObjectMapper;
 * import com.fasterxml.jackson.annotation.JsonProperty;
 * import com.google.common.reflect.TypeToken;
 *
 * import java.util.List;
 *
 * import static ratpack.util.Types.listOf;
 * import static ratpack.jackson.Jackson.jsonNode;
 * import static ratpack.jackson.Jackson.fromJson;
 * import static org.junit.Assert.*;
 *
 * public class Example {
 *
 *   public static class Person {
 *     private final String name;
 *     public Person(@JsonProperty("name") String name) {
 *       this.name = name;
 *     }
 *     public String getName() {
 *       return name;
 *     }
 *   }
 *
 *   public static void main(String... args) throws Exception {
 *     EmbeddedApp.of(s -> s
 *       .handlers(chain -> chain
 *         .post("asNode", ctx -> {
 *           ctx.render(ctx.parse(jsonNode()).map(n -> n.get("name").asText()));
 *         })
 *         .post("asPerson", ctx -> {
 *           ctx.render(ctx.parse(fromJson(Person.class)).map(p -> p.getName()));
 *         })
 *         .post("asPersonList", ctx -> {
 *           ctx.render(ctx.parse(fromJson(listOf(Person.class))).map(p -> p.get(0).getName()));
 *         })
 *       )
 *     ).test(httpClient -> {
 *       ReceivedResponse response = httpClient.requestSpec(s ->
 *         s.body(b -> b.type("application/json").text("{\"name\":\"John\"}"))
 *       ).post("asNode");
 *       assertEquals("John", response.getBody().getText());
 *
 *       response = httpClient.requestSpec(s ->
 *         s.body(b -> b.type("application/json").text("{\"name\":\"John\"}"))
 *       ).post("asPerson");
 *       assertEquals("John", response.getBody().getText());
 *
 *       response = httpClient.requestSpec(s ->
 *         s.body(b -> b.type("application/json").text("[{\"name\":\"John\"}]"))
 *       ).post("asPersonList");
 *       assertEquals("John", response.getBody().getText());
 *     });
 *   }
 * }
 * }
*

* A {@link ratpack.parse.NoOptParserSupport} parser is also rendered for the {@code "application/json"} content type. * This allows the use of the {@link ratpack.handling.Context#parse(java.lang.Class)} and {@link ratpack.handling.Context#parse(com.google.common.reflect.TypeToken)} methods. *

{@code
 * import ratpack.test.embed.EmbeddedApp;
 * import ratpack.jackson.Jackson;
 * import ratpack.http.client.ReceivedResponse;
 * import com.fasterxml.jackson.annotation.JsonProperty;
 * import com.fasterxml.jackson.databind.ObjectMapper;
 * import com.google.common.reflect.TypeToken;
 *
 * import java.util.List;
 *
 * import static ratpack.util.Types.listOf;
 * import static org.junit.Assert.*;
 *
 * public class Example {
 *
 *   public static class Person {
 *     private final String name;
 *     public Person(@JsonProperty("name") String name) {
 *       this.name = name;
 *     }
 *     public String getName() {
 *       return name;
 *     }
 *   }
 *
 *   public static void main(String... args) throws Exception {
 *     EmbeddedApp.of(s -> s
 *       .handlers(chain -> chain
 *         .post("asPerson", ctx -> {
 *           ctx.parse(Person.class).then(person -> ctx.render(person.getName()));
 *         })
 *         .post("asPersonList", ctx -> {
 *           ctx.parse(listOf(Person.class)).then(person -> ctx.render(person.get(0).getName()));
 *         })
 *       )
 *     ).test(httpClient -> {
 *       ReceivedResponse response = httpClient.requestSpec(s ->
 *         s.body(b -> b.type("application/json").text("{\"name\":\"John\"}"))
 *       ).post("asPerson");
 *       assertEquals("John", response.getBody().getText());
 *
 *       response = httpClient.requestSpec(s ->
 *         s.body(b -> b.type("application/json").text("[{\"name\":\"John\"}]"))
 *       ).post("asPersonList");
 *       assertEquals("John", response.getBody().getText());
 *     });
 *   }
 * }
 * }
* *

Configuring Jackson

*

* The Jackson API is based around the {@link ObjectMapper}. * Ratpack adds a default instance to the base registry automatically. * To configure Jackson behaviour, override this instance. *

{@code
 * import ratpack.test.embed.EmbeddedApp;
 * import ratpack.http.client.ReceivedResponse;
 * import com.fasterxml.jackson.databind.ObjectMapper;
 * import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 *
 * import java.util.Optional;
 *
 * import static ratpack.jackson.Jackson.json;
 * import static org.junit.Assert.*;
 *
 * public class Example {
 *
 *   public static class Person {
 *     private final String name;
 *     public Person(String name) {
 *       this.name = name;
 *     }
 *     public String getName() {
 *       return name;
 *     }
 *   }
 *
 *   public static void main(String... args) throws Exception {
 *     EmbeddedApp.of(s -> s
 *       .registryOf(r -> r
 *         .add(new ObjectMapper().registerModule(new Jdk8Module()))
 *       )
 *       .handlers(chain ->
 *         chain.get(ctx -> {
 *           Optional personOptional = Optional.of(new Person("John"));
 *           ctx.render(json(personOptional));
 *         })
 *       )
 *     ).test(httpClient -> {
 *       ReceivedResponse response = httpClient.get();
 *       assertEquals("{\"name\":\"John\"}", response.getBody().getText());
 *       assertEquals("application/json", response.getBody().getContentType().getType());
 *     });
 *   }
 * }
 * }
*/ public abstract class Jackson { private Jackson() { } /** * Creates a {@link ratpack.handling.Context#render renderable object} to render the given object as JSON. *

* The given object will be converted to JSON using an {@link ObjectWriter} obtained from the context registry. *

* See the rendering section for usage examples. * * @param object the object to render as JSON * @return a renderable wrapper for the given object */ public static JsonRender json(Object object) { return new DefaultJsonRender(object, null, null); } /** * Creates a {@link ratpack.handling.Context#render renderable object} to render the given object as JSON. *

* The given object will be converted to JSON using the given {@link ObjectWriter}. * If it is {@code null}, an {@code ObjectWriter} will be obtained from the context registry. *

* See the rendering section for usage examples. * * @param object the object to render as JSON * @param objectWriter the object writer to use to serialize the object to JSON * @return a renderable wrapper for the given object */ public static JsonRender json(Object object, @Nullable ObjectWriter objectWriter) { return new DefaultJsonRender(object, objectWriter); } /** * Creates a {@link ratpack.handling.Context#render renderable object} to render the given object as JSON. *

* The given object will be converted to JSON using an {@link ObjectWriter} obtained from the context registry * with the specified view {@code Class} used to determine which fields are included. * If it is null the default view rendering of the {@link ObjectWriter} will be used. *

* See the rendering section for usage examples. * * @param object the object to render as JSON * @param viewClass the view to use when rendering * @return a renderable wrapper for the given object */ public static JsonRender json(Object object, @Nullable Class viewClass) { return new DefaultJsonRender(object, viewClass); } /** * Creates a {@link ratpack.handling.Context#render renderable object} to render the given object as JSON. *

* The given object will be converted to JSON using the given {@link ObjectWriter} * with the specified view {@code Class} used to determine which fields are included. * If the {@link ObjectWriter} is {@code null}, an {@code ObjectWriter} will be obtained from the context registry. * If the view {@code Class} is null the default view rendering of the {@link ObjectWriter} will be used. *

* See the rendering section for usage examples. * * @param object the object to render as JSON * @param objectWriter the object writer to use to serialize the object to JSON * @param viewClass the view to use when rendering * @return a renderable wrapper for the given object */ public static JsonRender json(Object object, @Nullable ObjectWriter objectWriter, @Nullable Class viewClass) { return new DefaultJsonRender(object, objectWriter, viewClass); } /** * Creates a {@link ratpack.handling.Context#parse parseable object} to parse a request body into a {@link JsonNode}. *

* The corresponding parser for this type requires the request content type to be {@code "application/json"}. *

* The request body will be parsed using an {@link ObjectMapper} obtained from the context registry. *

* See the parsing section for usage examples. * * @return a parse object */ public static Parse jsonNode() { return jsonNode(null); } /** * Creates a {@link ratpack.handling.Context#parse parseable object} to parse a request body into a {@link JsonNode}. *

* The corresponding parser for this type requires the request content type to be {@code "application/json"}. *

* The request body will be parsed using the given {@link ObjectMapper}. * If it is {@code null}, a mapper will be obtained from the context registry. *

* See the parsing section for usage examples. * * @param objectMapper the object mapper to use to parse the JSON * @return a parse object */ public static Parse jsonNode(@Nullable ObjectMapper objectMapper) { return fromJson(JsonNode.class, objectMapper); } /** * Creates a {@link ratpack.handling.Context#parse parseable object} to parse a request body into the given type. *

* The corresponding parser for this type requires the request content type to be {@code "application/json"}. *

* The request body will be parsed using an {@link ObjectMapper} obtained from the context registry. *

* See the parsing section for usage examples. * * @param type the type of object to deserialize the JSON into * @param the type of object to deserialize the JSON into * @return a parse object */ public static Parse fromJson(Class type) { return fromJson(type, null); } /** * Creates a {@link ratpack.handling.Context#parse parseable object} to parse a request body into the given type. *

* The corresponding parser for this type requires the request content type to be {@code "application/json"}. *

* The request body will be parsed using an {@link ObjectMapper} obtained from the context registry. *

* See the parsing section for usage examples. * * @param type the type of object to deserialize the JSON into * @param the type of object to deserialize the JSON into * @return a parse object */ public static Parse fromJson(TypeToken type) { return fromJson(type, null); } /** * Creates a {@link ratpack.handling.Context#parse parseable object} to parse a request body into the given type. *

* The corresponding parser for this type requires the request content type to be {@code "application/json"}. *

* The request body will be parsed using the given {@link ObjectMapper}. * If it is {@code null}, a mapper will be obtained from the context registry. *

* See the parsing section for usage examples. * * @param type the type of object to deserialize the JSON into * @param objectMapper the object mapper to use to convert the JSON into a Java object * @param the type of object to deserialize the JSON into * @return a parse object */ public static Parse fromJson(Class type, @Nullable ObjectMapper objectMapper) { return Parse.of(type, new DefaultJsonParseOpts(objectMapper)); } /** * Creates a {@link ratpack.handling.Context#parse parseable object} to parse a request body into the given type. *

* The corresponding parser for this type requires the request content type to be {@code "application/json"}. *

* The request body will be parsed using the given {@link ObjectMapper}. * If it is {@code null}, a mapper will be obtained from the context registry. *

* See the parsing section for usage examples. * * @param type the type of object to deserialize the JSON into * @param objectMapper the object mapper to use to convert the JSON into a Java object * @param the type of object to deserialize the JSON into * @return a parse object */ public static Parse fromJson(TypeToken type, @Nullable ObjectMapper objectMapper) { return Parse.of(type, new DefaultJsonParseOpts(objectMapper)); } /** * Renders a data stream as a JSON list, directly streaming the JSON. *

* This method differs from rendering a list of items using {@link #json(Object) json(someList)} in that data is * written to the response as chunks, and is streamed. *

* If stream can be very large without using considerable memory as the JSON is streamed incrementally in chunks. * This does mean that if on object-to-JSON conversion fails midway through the stream, then the output JSON will be malformed due to being incomplete. * If the publisher emits an error, the response will be terminated and no more JSON will be sent. *

{@code
   * import ratpack.test.embed.EmbeddedApp;
   * import ratpack.jackson.Jackson;
   * import ratpack.http.client.ReceivedResponse;
   * import ratpack.stream.Streams;
   * import com.fasterxml.jackson.databind.ObjectMapper;
   * import org.reactivestreams.Publisher;
   *
   * import java.util.Arrays;
   *
   * import static ratpack.jackson.Jackson.chunkedJsonList;
   * import static org.junit.Assert.*;
   *
   * public class Example {
   *   public static void main(String... args) throws Exception {
   *     EmbeddedApp.of(s -> s
   *       .handlers(chain ->
   *         chain.get(ctx -> {
   *           Publisher ints = Streams.publish(Arrays.asList(1, 2, 3));
   *           ctx.render(chunkedJsonList(ctx, ints));
   *         })
   *       )
   *     ).test(httpClient -> {
   *       ReceivedResponse response = httpClient.get();
   *       assertEquals("[1,2,3]", response.getBody().getText()); // body was streamed in chunks
   *       assertEquals("application/json", response.getBody().getContentType().getType());
   *     });
   *   }
   * }
   * }
*

* Items of the stream will be converted to JSON by an {@link ObjectMapper} obtained from the given registry. *

* This method uses {@link Streams#streamMap(Publisher, ratpack.func.Function)} to consume the given stream. * * @param registry the registry to obtain the object mapper from * @param stream the stream to render * @param the type of item in the stream * @return a renderable object * @see Streams#streamMap(Publisher, ratpack.func.Function) */ public static ResponseChunks chunkedJsonList(Registry registry, Publisher stream) { return chunkedJsonList(getObjectWriter(registry), stream); } public static ObjectWriter getObjectWriter(Registry registry) { return registry.maybeGet(ObjectWriter.class) .orElseGet(() -> registry.get(ObjectMapper.class).writer()); } /** * Renders a data stream as a JSON list, directly streaming the JSON. *

* Identical to {@link #chunkedJsonList(Registry, Publisher)}, except uses the given object writer instead of obtaining one from the registry. * * @param objectWriter the object write to use to convert stream items to their JSON representation * @param stream the stream to render * @param the type of item in the stream * @return a renderable object * @see #chunkedJsonList(Registry, Publisher) */ public static ResponseChunks chunkedJsonList(ObjectWriter objectWriter, Publisher stream) { return ResponseChunks.bufferChunks(HttpHeaderConstants.JSON, Streams.streamMap(stream, out -> { JsonGenerator generator = objectWriter.getFactory().createGenerator(new OutputStream() { @Override public void write(int b) throws IOException { throw new UnsupportedOperationException(); } @Override public void write(@SuppressWarnings("NullableProblems") byte[] b, int off, int len) throws IOException { out.item(Unpooled.copiedBuffer(b, off, len)); } }); generator.writeStartArray(); return new WriteStream() { @Override public void item(T item) { try { generator.writeObject(item); } catch (Exception e) { out.error(e); } } @Override public void error(Throwable throwable) { out.error(throwable); } @Override public void complete() { try { generator.writeEndArray(); generator.close(); out.complete(); } catch (IOException e) { out.error(e); } } }; })); } /** * Creates a mapping function that returns the JSON representation as a string of the input object. *

* An {@link ObjectWriter} instance is obtained from the given registry eagerly. * The returned function uses the {@link ObjectWriter#writeValueAsString(Object)} method to convert the input object to JSON. *

{@code
   * import ratpack.exec.Promise;
   * import ratpack.test.embed.EmbeddedApp;
   * import ratpack.jackson.Jackson;
   * import ratpack.http.client.ReceivedResponse;
   * import com.fasterxml.jackson.databind.ObjectMapper;
   *
   * import java.util.Arrays;
   *
   * import static ratpack.jackson.Jackson.toJson;
   * import static java.util.Collections.singletonMap;
   * import static org.junit.Assert.*;
   *
   * public class Example {
   *   public static void main(String... args) throws Exception {
   *     EmbeddedApp.of(s -> s
   *       .handlers(chain -> chain
   *         .get(ctx ->
   *           Promise.value(singletonMap("foo", "bar"))
   *             .map(toJson(ctx))
   *             .then(ctx::render)
   *         )
   *       )
   *     ).test(httpClient -> {
   *       assertEquals("{\"foo\":\"bar\"}", httpClient.getText());
   *     });
   *   }
   * }
   * }
*

* Note that in the above example, it would have been better to just render the result of the blocking call. * Doing so would be more convenient and also set the correct {@code "Content-Type"} header. * This method can be useful when sending the JSON somewhere else than directly to the response, or when {@link Streams#map(Publisher, Function) mapping streams}. * * @param registry the registry to obtain the {@link ObjectWriter} from * @param the type of object to convert to JSON * @return a function that converts objects to their JSON string representation */ public static Function toJson(Registry registry) { return getObjectWriter(registry)::writeValueAsString; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy