Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
54 changes: 54 additions & 0 deletions modules/jooby-jackson3/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?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/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.jooby</groupId>
<artifactId>modules</artifactId>
<version>4.0.16-SNAPSHOT</version>
</parent>
<artifactId>jooby-jackson3</artifactId>
<name>jooby-jackson3</name>

<dependencies>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby</artifactId>
<version>${jooby.version}</version>
</dependency>

<!-- jackson -->
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.agent</artifactId>
<classifier>runtime</classifier>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>tools.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<scope>test</scope>
</dependency>

</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.jackson3;

import edu.umd.cs.findbugs.annotations.NonNull;
import io.jooby.*;
import io.jooby.output.Output;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.databind.JacksonModule;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.type.TypeFactory;

import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

/**
* JSON module using Jackson3: https://jooby.io/modules/jackson3.
*
* <p>Usage:
*
* <pre>{@code
* {
*
* install(new Jackson3Module());
*
* get("/", ctx -> {
* MyObject myObject = ...;
* // send json
* return myObject;
* });
*
* post("/", ctx -> {
* // read json
* MyObject myObject = ctx.body(MyObject.class);
* // send json
* return myObject;
* });
* }
* }</pre>
* <p>
* For body decoding the client must specify the <code>Content-Type</code> header set to <code>
* application/json</code>.
*
* <p>You can retrieve the {@link ObjectMapper} via require call:
*
* <pre>{@code
* {
*
* ObjectMapper mapper = require(ObjectMapper.class);
*
* }
* }</pre>
*
* @author edgar, kliushnichenko
* @since 4.1.0
*/
public class Jackson3Module implements Extension, MessageDecoder, MessageEncoder {
private final MediaType mediaType;

private final ObjectMapper mapper;

private final TypeFactory typeFactory;

private final Set<Class<? extends JacksonModule>> modules = new HashSet<>();

private static final Map<String, MediaType> defaultTypes = new HashMap<>();

static {
defaultTypes.put("XmlMapper", MediaType.xml);
}

/**
* Creates a Jackson module.
*
* @param mapper Object mapper to use.
* @param contentType Content type.
*/
public Jackson3Module(@NonNull ObjectMapper mapper, @NonNull MediaType contentType) {
this.mapper = mapper;
this.typeFactory = mapper.getTypeFactory();
this.mediaType = contentType;
}

/**
* Creates a Jackson module.
*
* @param mapper Object mapper to use.
*/
public Jackson3Module(@NonNull ObjectMapper mapper) {
this(mapper, defaultTypes.getOrDefault(mapper.getClass().getSimpleName(), MediaType.json));
}

/**
* Creates a Jackson module using the default object mapper from {@link #create(JacksonModule...)}.
*/
public Jackson3Module() {
this(create());
}

/**
* Add a Jackson module to the object mapper. This method require a dependency injection framework
* which is responsible for provisioning a module instance.
*
* @param module Module type.
* @return This module.
*/
public Jackson3Module module(Class<? extends JacksonModule> module) {
modules.add(module);
return this;
}

@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public void install(@NonNull Jooby application) {
application.decoder(mediaType, this);
application.encoder(mediaType, this);

ServiceRegistry services = application.getServices();
Class mapperType = mapper.getClass();
services.put(mapperType, mapper);
services.put(ObjectMapper.class, mapper);

// Parsing exception as 400
application.errorCode(StreamReadException.class, StatusCode.BAD_REQUEST);

application.onStarting(() -> onStarting(application, services, mapperType));
}

@SuppressWarnings({"rawtypes", "unchecked"})
private void onStarting(Jooby application, ServiceRegistry services, Class mapperType) {
if (!modules.isEmpty()) {
var builder = mapper.rebuild();
for (Class<? extends JacksonModule> type : modules) {
JacksonModule module = application.require(type);
builder.addModule(module);
}
var newMapper = builder.build();
services.put(mapperType, newMapper);
services.put(ObjectMapper.class, newMapper);
}
}

@Override
public Output encode(@NonNull Context ctx, @NonNull Object value) {
var factory = ctx.getOutputFactory();
ctx.setDefaultResponseType(mediaType);
// let jackson uses his own cache, so wrap the bytes
return factory.wrap(mapper.writeValueAsBytes(value));
}

@Override
public Object decode(Context ctx, Type type) throws Exception {
Body body = ctx.body();
if (body.isInMemory()) {
if (type == JsonNode.class) {
return mapper.readTree(body.bytes());
}
return mapper.readValue(body.bytes(), typeFactory.constructType(type));
} else {
try (InputStream stream = body.stream()) {
if (type == JsonNode.class) {
return mapper.readTree(stream);
}
return mapper.readValue(stream, typeFactory.constructType(type));
}
}
}

/**
* Default object mapper.
*
* @param modules Extra/additional modules to install.
* @return Object mapper instance.
*/
public static ObjectMapper create(JacksonModule... modules) {
JsonMapper.Builder builder = JsonMapper.builder();

Stream.of(modules).forEach(builder::addModule);

return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault
package io.jooby.jackson3;
14 changes: 14 additions & 0 deletions modules/jooby-jackson3/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
/** Jackson module. */
module io.jooby.jackson3 {
exports io.jooby.jackson3;

requires io.jooby;
requires static com.github.spotbugs.annotations;
requires typesafe.config;
requires tools.jackson.databind;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.jackson3;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.Test;

import tools.jackson.databind.ObjectMapper;
import tools.jackson.dataformat.xml.XmlMapper;
import io.jooby.Body;
import io.jooby.Context;
import io.jooby.MediaType;
import io.jooby.output.OutputFactory;
import io.jooby.output.OutputOptions;

public class Jackson3JsonModuleTest {

@Test
public void renderJson() {
Context ctx = mock(Context.class);
when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(OutputOptions.small()));

Jackson3Module jackson = new Jackson3Module(new ObjectMapper());

var buffer = jackson.encode(ctx, mapOf("k", "v"));
assertEquals("{\"k\":\"v\"}", StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString());

verify(ctx).setDefaultResponseType(MediaType.json);
}

@Test
public void parseJson() throws Exception {
byte[] bytes = "{\"k\":\"v\"}".getBytes(StandardCharsets.UTF_8);
Body body = mock(Body.class);
when(body.isInMemory()).thenReturn(true);
when(body.bytes()).thenReturn(bytes);

Context ctx = mock(Context.class);
when(ctx.body()).thenReturn(body);

Jackson3Module jackson = new Jackson3Module(new ObjectMapper());

Map<String, String> result = (Map<String, String>) jackson.decode(ctx, Map.class);
assertEquals(mapOf("k", "v"), result);
}

@Test
public void renderXml() {
Context ctx = mock(Context.class);
when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(OutputOptions.small()));

Jackson3Module jackson = new Jackson3Module(new XmlMapper());

var buffer = jackson.encode(ctx, mapOf("k", "v"));
assertEquals(
"<HashMap><k>v</k></HashMap>",
StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString());

verify(ctx).setDefaultResponseType(MediaType.xml);
}

@Test
public void parseXml() throws Exception {
byte[] bytes = "<HashMap><k>v</k></HashMap>".getBytes(StandardCharsets.UTF_8);
Body body = mock(Body.class);
when(body.isInMemory()).thenReturn(true);
when(body.bytes()).thenReturn(bytes);

Context ctx = mock(Context.class);
when(ctx.body()).thenReturn(body);

Jackson3Module jackson = new Jackson3Module(new XmlMapper());

Map<String, String> result = (Map<String, String>) jackson.decode(ctx, Map.class);
assertEquals(mapOf("k", "v"), result);
}

private Map<String, String> mapOf(String... values) {
Map<String, String> hash = new HashMap<>();
for (int i = 0; i < values.length; i += 2) {
hash.put(values[i], values[i + 1]);
}
return hash;
}
}
1 change: 1 addition & 0 deletions modules/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<module>jooby-caffeine</module>

<module>jooby-jackson</module>
<module>jooby-jackson3</module>
<module>jooby-gson</module>
<module>jooby-avaje-jsonb</module>
<module>jooby-yasson</module>
Expand Down
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<jstachio.version>1.3.7</jstachio.version>
<pebble.version>4.1.1</pebble.version>
<jackson.version>2.21.0</jackson.version>
<jackson3.version>3.0.4</jackson3.version>
<gson.version>2.13.2</gson.version>
<jakarta.json.bind-api.version>3.0.1</jakarta.json.bind-api.version>
<yasson.version>3.0.4</yasson.version>
Expand Down Expand Up @@ -254,6 +255,13 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>tools.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson3.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Jooby -->
<dependency>
<groupId>io.jooby</groupId>
Expand Down