mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
#299 - ENH: Add support for mapping Jackson JsonNode as JSON content to DB including Postgres JSON and JSON types
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user