/*
* Copyright 2013 David Tinker
*
* 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 io.qdb.server.controller;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.NumberSerializers;
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.google.inject.Inject;
import humanize.Humanize;
import io.qdb.server.databind.DateTimeParser;
import io.qdb.server.databind.DurationParser;
import io.qdb.server.databind.IntegerParser;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Marshaling of objects to/from JSON using Jackson for the REST API.
*/
@Singleton
public class JsonService {
private final ObjectMapper mapper;
private final ObjectMapper mapperMsgHeader;
private final ObjectMapper mapperBorg;
private final ObjectMapper mapperBorgMsgHeader;
private final JsonDeserializer<Long> longJsonDeserializer = new JsonDeserializer<Long>() {
@Override
public Long deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
if (jp.getCurrentToken() == JsonToken.VALUE_NUMBER_INT) return jp.getLongValue();
return IntegerParser.INSTANCE.parseLong(jp.getText().trim());
}
};
private final JsonDeserializer<Integer> integerJsonDeserializer = new JsonDeserializer<Integer>() {
@Override
public Integer deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
if (jp.getCurrentToken() == JsonToken.VALUE_NUMBER_INT) return jp.getIntValue();
return IntegerParser.INSTANCE.parseInt(jp.getText().trim());
}
};
private final JsonDeserializer<Date> dateDeserializer = new JsonDeserializer<Date>() {
@Override
public Date deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
if (jp.getCurrentToken() == JsonToken.VALUE_NUMBER_INT) return new Date(jp.getLongValue());
String s = jp.getText().trim();
try {
return DateTimeParser.INSTANCE.parse(s);
} catch (ParseException e) {
throw new IllegalArgumentException("Invalid date: [" + s + "]");
}
}
};
BorgOrHumanSerializer<Integer> integerSerializer = new BorgOrHumanSerializer<Integer>(Integer.TYPE,
new HumanNumberSerializer<Integer>(Integer.TYPE),
new NumberSerializers.IntegerSerializer());
BorgOrHumanSerializer<Long> longSerializer = new BorgOrHumanSerializer<Long>(Long.TYPE,
new HumanNumberSerializer<Long>(Long.TYPE),
new NumberSerializers.LongSerializer());
@Inject
@SuppressWarnings("deprecation")
public JsonService(@Named("prettyPrint") boolean prettyPrint) {
mapper = createMapper(prettyPrint, false, false);
mapperMsgHeader = createMapper(false, false, true);
mapperBorg = createMapper(prettyPrint, true, true);
mapperBorgMsgHeader = prettyPrint ? createMapper(false, true, true) : mapperBorg;
}
private ObjectMapper createMapper(boolean prettyPrint, boolean datesAsTimestamps, boolean borg) {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
mapper.configure(SerializationFeature.INDENT_OUTPUT, prettyPrint);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, datesAsTimestamps);
SimpleModule module = new SimpleModule();
module.addDeserializer(Date.class, dateDeserializer);
module.addDeserializer(Integer.class, integerJsonDeserializer);
module.addDeserializer(Integer.TYPE, integerJsonDeserializer);
module.addDeserializer(Long.class, longJsonDeserializer);
module.addDeserializer(Long.TYPE, longJsonDeserializer);
if (!borg) {
module.addSerializer(Integer.TYPE, integerSerializer);
module.addSerializer(Integer.class, integerSerializer);
module.addSerializer(Long.TYPE, longSerializer);
module.addSerializer(Long.class, longSerializer);
}
if (!datesAsTimestamps) module.addSerializer(Date.class, new ISO8601DateSerializer());
mapper.registerModule(module);
return mapper;
}
/**
* Convert o to JSON.
*/
public byte[] toJson(Object o, boolean borg) throws IOException {
return (borg ? mapperBorg : mapper).writeValueAsBytes(o);
}
/**
* Convert o to JSON with no indenting and no humanization of sizes.
*/
public byte[] toJsonMsgHeader(Object o, boolean borg) throws IOException {
return (borg ? mapperBorgMsgHeader : mapperMsgHeader).writeValueAsBytes(o);
}
/**
* Converts content to an instance of a particular type. Throws IllegalArgumentException if JSON is invalid.
*/
public <T> T fromJson(InputStream ins, Class<T> klass) throws IOException {
try {
return mapper.readValue(ins, klass);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
/**
* Chooses between human and borg serializers based on the name of the property being serialized.
*/
private static class BorgOrHumanSerializer<T> extends StdSerializer<T> implements ContextualSerializer {
private final JsonSerializer<T> human;
private final JsonSerializer<T> borg;
public BorgOrHumanSerializer(Class<T> t, JsonSerializer<T> human, JsonSerializer<T> borg) {
super(t);
this.human = human;
this.borg = borg;
}
@Override
public JsonSerializer<T> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
if (property != null) {
String name = property.getName();
if (name.endsWith("emory") || name.endsWith("ize") || name.endsWith("ytes")) {
return human;
}
}
return borg;
}
@Override
public void serialize(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
borg.serialize(value, jgen, provider);
}
}
/**
* Serializers numbers as strings in human form e.g. 1 GB.
*/
private static class HumanNumberSerializer<T extends Number> extends StdScalarSerializer<T> {
public HumanNumberSerializer(Class<T> t) {
super(t);
}
@Override
public void serialize(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(Humanize.binaryPrefix(value));
}
}
private static class ISO8601DateSerializer extends JsonSerializer<Date> {
private SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); // ISO 8601
@Override
public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(df.format(value));
}
}
}