diff --git a/pom.xml b/pom.xml
index d80270f84..d1e34ec9f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,18 +55,19 @@
1.7.7
+
com.fasterxml.jackson.core
jackson-databind
2.5.3
+ provided
-
+
com.fasterxml.jackson.core
jackson-core
2.5.3
- provided
diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java
index 8b27cab90..24825de3c 100644
--- a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java
+++ b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java
@@ -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 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");
diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNode.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNode.java
new file mode 100644
index 000000000..9dc44579e
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNode.java
@@ -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 {
+
+ /**
+ * 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);
+ }
+
+}
diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNodePostgres.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNodePostgres.java
new file mode 100644
index 000000000..ae12e8fe4
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNodePostgres.java
@@ -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);
+ }
+ }
+}
diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java
index d91def549..2c04d0175 100644
--- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java
+++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java
@@ -19,8 +19,6 @@ import java.util.Map;
@SuppressWarnings("rawtypes")
public class ScalarTypePostgresHstore extends ScalarTypeBase