#299 - ENH: Add support for mapping Jackson JsonNode as JSON content to DB including Postgres JSON and JSON types

This commit is contained in:
rbygrave
2015-05-28 22:54:32 +12:00
parent f5a2cf8ff9
commit 67f427b83e
13 changed files with 895 additions and 4 deletions
@@ -9,6 +9,7 @@ import com.avaje.ebeaninternal.api.ClassUtil;
import com.avaje.ebeaninternal.server.core.BootupClasses;
import com.avaje.ebeaninternal.server.lib.util.StringHelper;
import com.avaje.ebeaninternal.server.type.reflect.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.joda.time.*;
import org.joda.time.LocalDate;
@@ -122,6 +123,29 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
private final boolean objectMapperPresent;
// OPTIONAL ScalarTypes registered if Jackson/JsonNode is in the classpath
/**
* Jackson's JsonNode storage to Clob.
*/
private ScalarType<?> jsonNodeClob;
/**
* Jackson's JsonNode storage to Blob.
*/
private ScalarType<?> jsonNodeBlob;
/**
* Jackson's JsonNode storage to Varchar.
*/
private ScalarType<?> jsonNodeVarchar;
/**
* Jackson's JsonNode storage to Postgres JSON or Clob.
*/
private ScalarType<?> jsonNodeJson;
/**
* Jackson's JsonNode storage to Postgres JSONB or Clob.
*/
private ScalarType<?> jsonNodeJsonb;
/**
* Create the DefaultTypeManager.
*/
@@ -142,6 +166,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
initialiseStandard(jsonDateTime, config);
initialiseJavaTimeTypes(jsonDateTime, config);
initialiseJodaTypes(jsonDateTime);
initialiseJacksonTypes(config);
if (isPostgres(config.getDatabasePlatform())) {
// Postgres has special DB types for JSON/JSONB
@@ -291,6 +316,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
public ScalarType<?> getJsonScalarType(Class<?> type, int dbType) {
if (type.equals(Map.class)) {
// @DbJson Map<String,Object> property
switch (dbType) {
case Types.VARCHAR : return jsonMapVarchar;
case Types.BLOB: return jsonMapBlob;
@@ -302,6 +328,19 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
}
}
if (type.equals(JsonNode.class)) {
// @DbJson JsonNode property (Jackson)
switch (dbType) {
case Types.VARCHAR : return jsonNodeVarchar;
case Types.BLOB: return jsonNodeBlob;
case Types.CLOB : return jsonNodeClob;
case DbType.JSONB: return jsonNodeJsonb;
case DbType.JSON: return jsonNodeJson;
default:
return jsonNodeJson;
}
}
throw new IllegalArgumentException("Type [" + type + "] unsupported for @DbJson mapping");
}
@@ -645,6 +684,38 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
return propParamTypes[1];
}
/**
* Add support for Jackson's JsonNode mapping to Clob, Blob, Varchar, JSON and JSONB.
*/
protected void initialiseJacksonTypes(ServerConfig config) {
if (ClassUtil.isPresent("com.fasterxml.jackson.databind.ObjectMapper", this.getClass())) {
logger.trace("Registering JsonNode type support");
ObjectMapper objectMapper = (ObjectMapper)config.getObjectMapper();
if (objectMapper == null) {
objectMapper = new ObjectMapper();
config.setObjectMapper(objectMapper);
}
jsonNodeClob = new ScalarTypeJsonNode.Clob(objectMapper);
jsonNodeBlob = new ScalarTypeJsonNode.Blob(objectMapper);
jsonNodeVarchar = new ScalarTypeJsonNode.Varchar(objectMapper);
jsonNodeJson = jsonNodeClob; // Default for non-Postgres databases
jsonNodeJsonb = jsonNodeClob; // Default for non-Postgres databases
if (isPostgres(config.getDatabasePlatform())) {
jsonNodeJson = new ScalarTypeJsonNodePostgres.JSON(objectMapper);
jsonNodeJsonb = new ScalarTypeJsonNodePostgres.JSONB(objectMapper);
}
// add as default mapping for JsonNode (when not annotated with @DbJson etc)
typeMap.put(JsonNode.class, jsonNodeJson);
}
}
protected void initialiseJavaTimeTypes(JsonConfig.DateTime mode, ServerConfig config) {
if (ClassUtil.isPresent("java.time.LocalDate", this.getClass())) {
logger.debug("Registering java.time data types");
@@ -0,0 +1,227 @@
package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.text.TextException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.sql.Types;
/**
* Type which maps Jackson's JsonNode to various DB types (Clob, Varchar, Blob) in JSON format.
*/
public abstract class ScalarTypeJsonNode extends ScalarTypeBase<JsonNode> {
/**
* Clob storage based implementation.
*/
public static class Clob extends ScalarTypeJsonNode {
public Clob(ObjectMapper objectMapper) {
super(objectMapper, Types.CLOB);
}
@Override
public JsonNode read(DataReader dataReader) throws SQLException {
Reader reader = dataReader.getClobReader();
if (reader == null) {
return null;
}
try {
JsonNode tree = parse(reader);
reader.close();
return tree;
} catch (IOException e) {
throw new SQLException("Error reading Clob stream from DB", e);
}
}
}
/**
* Varchar storage based implementation.
*/
public static class Varchar extends ScalarTypeJsonNode {
public Varchar(ObjectMapper objectMapper) {
super(objectMapper, Types.VARCHAR);
}
}
/**
* Blob storage based implementation.
*/
public static class Blob extends ScalarTypeJsonNode {
public Blob(ObjectMapper objectMapper) {
super(objectMapper, Types.BLOB);
}
@Override
public JsonNode read(DataReader dataReader) throws SQLException {
InputStream is = dataReader.getBlobInputStream();
if (is == null) {
return null;
}
try {
InputStreamReader reader = new InputStreamReader(is);
JsonNode tree = parse(reader);
reader.close();
return tree;
} catch (IOException e) {
throw new SQLException("Error reading Blob stream from DB", e);
}
}
@Override
public void bind(DataBind dataBind, JsonNode value) throws SQLException {
if (value == null) {
dataBind.setNull(Types.BLOB);
} else {
String rawJson = formatValue(value);
InputStream stream = new ByteArrayInputStream(rawJson.getBytes(StandardCharsets.UTF_8));
dataBind.setBlob(stream);
}
}
}
/**
* Jackson's ObjectMapper used to read / write JsonNode
*/
final ObjectMapper objectMapper;
public ScalarTypeJsonNode(ObjectMapper objectMapper, int jdbcType) {
super(JsonNode.class, false, jdbcType);
this.objectMapper = objectMapper;
}
/**
* Map is a mutable type. Use the isDirty() method to check for dirty state.
*/
@Override
public boolean isMutable() {
return true;
}
/**
* Return true if the value should be considered dirty (and included in an update).
*/
@Override
public boolean isDirty(Object value) {
return true;
}
@Override
public JsonNode read(DataReader dataReader) throws SQLException {
String rawJson = dataReader.getString();
if (rawJson == null) {
return null;
}
return parse(rawJson);
}
@Override
public void bind(DataBind dataBind, JsonNode value) throws SQLException {
if (value == null) {
dataBind.setNull(Types.VARCHAR);
} else {
String rawJson = formatValue(value);
dataBind.setString(rawJson);
}
}
@Override
public Object toJdbcType(Object value) {
return value;
}
@Override
public JsonNode toBeanType(Object value) {
return (JsonNode) value;
}
@Override
public String formatValue(JsonNode jsonNode) {
try {
return objectMapper.writeValueAsString(jsonNode);
} catch (IOException e) {
throw new TextException(e);
}
}
@Override
public JsonNode parse(String value) {
try {
return objectMapper.readValue(value, JsonNode.class);
} catch (IOException e) {
throw new TextException(e);
}
}
public JsonNode parse(Reader reader) {
try {
return objectMapper.readValue(reader, JsonNode.class);
} catch (IOException e) {
throw new TextException(e);
}
}
@Override
public JsonNode convertFromMillis(long dateTime) {
throw new RuntimeException("Should never be called");
}
@Override
public boolean isDateTimeCapable() {
return false;
}
@Override
public JsonNode readData(DataInput dataInput) throws IOException {
if (!dataInput.readBoolean()) {
return null;
} else {
String json = dataInput.readUTF();
return parse(json);
}
}
@Override
public void writeData(DataOutput dataOutput, JsonNode value) throws IOException {
if (value == null) {
dataOutput.writeBoolean(false);
} else {
dataOutput.writeBoolean(true);
String json = format(value);
dataOutput.writeUTF(json);
}
}
@Override
public void jsonWrite(JsonGenerator ctx, String name, JsonNode value) throws IOException {
// write the field name followed by the JsonNode object
if (value == null) {
ctx.writeNullField(name);
} else {
ctx.writeFieldName(name);
objectMapper.writeTree(ctx, value);
}
}
@Override
public JsonNode jsonRead(JsonParser ctx, JsonToken event) throws IOException {
// at this point the BeanProperty has read the START_OBJECT token
// to check for a null value.
return objectMapper.readValue(ctx, JsonNode.class);
}
}
@@ -0,0 +1,59 @@
package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.config.dbplatform.DbType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.postgresql.util.PGobject;
import java.sql.SQLException;
/**
* Support for mapping JsonNode to Postgres DB types JSON and JSONB.
*/
public abstract class ScalarTypeJsonNodePostgres extends ScalarTypeJsonNode {
private static final String POSTGRES_TYPE_JSON = "json";
private static final String POSTGRES_TYPE_JSONB = "jsonb";
final ObjectMapper objectMapper;
final String postgresType;
ScalarTypeJsonNodePostgres(ObjectMapper objectMapper, int jdbcType, String postgresType) {
super(objectMapper, jdbcType);
this.objectMapper = objectMapper;
this.postgresType = postgresType;
}
@Override
public void bind(DataBind dataBind, JsonNode value) throws SQLException {
String rawJson = (value == null) ? null : formatValue(value);
PGobject pgo = new PGobject();
pgo.setType(postgresType);
pgo.setValue(rawJson);
dataBind.setObject(pgo);
}
/**
* ScalarType mapping JsonNode to Postgres JSON database type.
*/
public static class JSON extends ScalarTypeJsonNodePostgres {
public JSON(ObjectMapper objectMapper) {
super(objectMapper, DbType.JSON, POSTGRES_TYPE_JSON);
}
}
/**
* ScalarType mapping JsonNode to Postgres JSONB database type.
*/
public static class JSONB extends ScalarTypeJsonNodePostgres {
public JSONB(ObjectMapper objectMapper) {
super(objectMapper, DbType.JSONB, POSTGRES_TYPE_JSONB);
}
}
}
@@ -19,8 +19,6 @@ import java.util.Map;
@SuppressWarnings("rawtypes")
public class ScalarTypePostgresHstore extends ScalarTypeBase<Map> {
public static final String KEY = "hstore";
public ScalarTypePostgresHstore() {
super(Map.class, false, DbType.HSTORE);
}