/** * 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.jooby.json; import static java.util.Objects.requireNonNull; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; import java.util.TimeZone; import java.util.function.Consumer; import javax.inject.Inject; import org.jooby.Env; import org.jooby.Jooby; import org.jooby.MediaType; import org.jooby.Parser; import org.jooby.Renderer; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import com.google.inject.Binder; import com.google.inject.Key; import com.google.inject.multibindings.Multibinder; import com.google.inject.name.Names; import com.typesafe.config.Config; /** * <h1>jackson</h1> * * JSON support from the excellent <a href="https://github.com/FasterXML/jackson">Jackson</a> * library. * * This module provides a JSON {@link Parser} and {@link Renderer}, but also an * {@link ObjectMapper}. * * <h2>usage</h2> * * <pre> * { * use(new Jackson()); * * // sending * get("/my-api", req {@literal ->} new MyObject()); * * // receiving a json body * post("/my-api", req {@literal ->} { * MyObject obj = req.body(MyObject.class); * return obj; * }); * * // receiving a json param from a multipart or form url encoded * post("/my-api", req {@literal ->} { * MyObject obj = req.param("my-object").to(MyObject.class); * return obj; * }); * } * </pre> * * <h2>advanced configuration</h2> * <p> * If you need a special setting or configuration for your {@link ObjectMapper}: * </p> * * <pre> * { * use(new Jackson().configure(mapper {@literal ->} { * // setup your custom object mapper * }); * } * </pre> * * or provide an {@link ObjectMapper} instance: * * <pre> * { * ObjectMapper mapper = ....; * use(new Jackson(mapper)); * } * </pre> * * It is possible to wire Jackson modules too: * * <pre> * { * * use(new Jackson() * .module(MyJacksonModuleWiredByGuice.class) * ); * } * </pre> * * This is useful when your jackson module require some dependencies. * * @author edgar * @since 0.6.0 */ public class Jackson implements Jooby.Module { private static class PostConfigurer { @Inject public PostConfigurer(final ObjectMapper mapper, final Set<Module> jacksonModules) { mapper.registerModules(jacksonModules); } } private final Optional<ObjectMapper> mapper; private MediaType type = MediaType.json; private Consumer<ObjectMapper> configurer; private List<Consumer<Multibinder<Module>>> modules = new ArrayList<>(); private boolean raw; /** * Creates a new {@link Jackson} module and use the provided {@link ObjectMapper} instance. * * @param mapper {@link ObjectMapper} to apply. */ public Jackson(final ObjectMapper mapper) { this.mapper = Optional.of(requireNonNull(mapper, "The mapper is required.")); } /** * Creates a new {@link Jackson} module. */ public Jackson() { this.mapper = Optional.empty(); } /** * Set the json type supported by this module, default is: <code>application/json</code>. * * @param type Media type. * @return This module. */ public Jackson type(final MediaType type) { this.type = type; return this; } /** * Set the json type supported by this module, default is: <code>application/json</code>. * * @param type Media type. * @return This module. */ public Jackson type(final String type) { return type(MediaType.valueOf(type)); } /** * Apply advanced configuration over the provided {@link ObjectMapper}. * * @param configurer A configurer callback. * @return This module. */ public Jackson doWith(final Consumer<ObjectMapper> configurer) { this.configurer = requireNonNull(configurer, "ObjectMapper configurer is required."); return this; } /** * Register the provided module. * * @param module A module instance. * @return This module. */ public Jackson module(final Module module) { requireNonNull(module, "Jackson Module is required."); modules.add(binder -> binder.addBinding().toInstance(module)); return this; } /** * Register the provided {@link Module}. The module will be instantiated by Guice. * * @param module Module type. * @return This module. */ public Jackson module(final Class<? extends Module> module) { requireNonNull(module, "Jackson Module is required."); modules.add(binder -> binder.addBinding().to(module)); return this; } /** * Add support raw string json responses: * * <pre>{@code * { * get("/raw", () -> { * return "{\"raw\": \"json\"}"; * }); * } * }</pre> * * @return This module. */ public Jackson raw() { raw = true; return this; } @Override public void configure(final Env env, final Config config, final Binder binder) { // provided or default mapper. ObjectMapper mapper = this.mapper.orElseGet(() -> { ObjectMapper m = new ObjectMapper(); Locale locale = env.locale(); // Jackson clone the date format in order to make dateFormat thread-safe m.setDateFormat(new SimpleDateFormat(config.getString("application.dateFormat"), locale)); m.setLocale(locale); m.setTimeZone(TimeZone.getTimeZone(config.getString("application.tz"))); // default modules: m.registerModule(new Jdk8Module()); m.registerModule(new JavaTimeModule()); m.registerModule(new ParameterNamesModule()); return m; }); if (configurer != null) { configurer.accept(mapper); } // bind mapper binder.bind(ObjectMapper.class).toInstance(mapper); // Jackson Modules from Guice Multibinder<Module> mbinder = Multibinder.newSetBinder(binder, Module.class); modules.forEach(m -> m.accept(mbinder)); // Jackson Configurer (like a post construct) binder.bind(PostConfigurer.class).asEagerSingleton(); // json parser & renderer JacksonParser parser = new JacksonParser(mapper, type); JacksonRenderer renderer = raw ? new JacksonRawRenderer(mapper, type) : new JacksonRenderer(mapper, type); Multibinder.newSetBinder(binder, Renderer.class) .addBinding() .toInstance(renderer); Multibinder.newSetBinder(binder, Parser.class) .addBinding() .toInstance(parser); // direct access? binder.bind(Key.get(Renderer.class, Names.named(renderer.toString()))).toInstance(renderer); binder.bind(Key.get(Parser.class, Names.named(parser.toString()))).toInstance(parser); } }