/**
* 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);
}
}