#297 - ENH: Add support for mapping Map<String,Object> as JSON content to DB including Postgres JSON and JSON types.

This commit is contained in:
rbygrave
2015-05-28 00:02:57 +12:00
parent 4e1cda691f
commit f5a2cf8ff9
37 changed files with 1898 additions and 193 deletions
@@ -0,0 +1,44 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Specify a property holding JSON content.
* <p>
* By default the content will be stored in a DB Clob except on Postgres where DB JSON type is used.
* </p>
* <h3>Example:</h3>
* <pre>{@code
*
* // Store as JSON on Postgres or Clob on other databases
* @DbJson
* Map<String,Object> content;
*
* }</pre>
*
* <h3>Example with JSONB storage</h3>
* <pre>{@code
*
* // Store as JSONB on Postgres or Clob on other databases
* @DbJson(storage = DbJsonType.JSONB)
* Map<String,Object> content;
*
* }</pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DbJson {
/**
* Specify the database type used to store the JSON content.
*/
DbJsonType storage() default DbJsonType.JSON;
/**
* For VARCHAR storage specify the column length (defaults to 3000).
*/
int length() default 0;
}
@@ -0,0 +1,39 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Specify a property holding JSON content.
* <p>
* The content will be stored on Postgres using it's JSONB type and as Clob for other databases.
* </p>
* <p>
* This is equivalent to using <code>@DbJson(storage = DbJsonType.JSONB)</code>
* </p>
*
* <h3>Example:</h3>
* <pre>{@code
*
* // Store as JSONB on Postgres or Clob on other databases
* @DbJsonB
* Map<String,Object> content;
*
* }</pre>
*
* <h3>Equivalent to:</h3>
* <pre>{@code
*
* // Store as JSONB on Postgres or Clob on other databases
* @DbJson(storage = DbJsonType.JSONB)
* Map<String,Object> content;
*
* }</pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DbJsonB {
}
@@ -0,0 +1,32 @@
package com.avaje.ebean.annotation;
/**
* Specify the DB storage type used to store JSON content.
*/
public enum DbJsonType {
/**
* Store as JSON on Postgres and for other databases store as CLOB.
*/
JSON,
/**
* Store as JSONB on Postgres and for other databases store as CLOB.
*/
JSONB,
/**
* Store as database VARCHAR.
*/
VARCHAR,
/**
* Store as database CLOB.
*/
CLOB,
/**
* Store as database BLOB.
*/
BLOB
}
@@ -291,6 +291,7 @@ public class ServerConfig {
private int queryCacheMaxSize = 1000;
private int queryCacheMaxIdleTime = 600;
private int queryCacheMaxTimeToLive = 60*60*6;
private Object objectMapper;
/**
* Construct a Server Configuration for programmatically creating an EbeanServer.
@@ -1864,4 +1865,12 @@ public class ServerConfig {
return databasePlatform.isDisallowBatchOnCascade() ? PersistBatch.NONE : persistBatchOnCascade;
}
public Object getObjectMapper() {
return objectMapper;
}
public void setObjectMapper(Object objectMapper) {
this.objectMapper = objectMapper;
}
}
@@ -8,6 +8,36 @@ package com.avaje.ebean.config.dbplatform;
*/
public class DbType {
/**
* Type to map Map content to Postgres HSTORE.
*/
public static final int HSTORE = 5000;
/**
* Type to map JSON content to Clob or Postgres JSON type.
*/
public static final int JSON = 5001;
/**
* Type to map JSON content to Clob or Postgres JSONB type.
*/
public static final int JSONB = 5002;
/**
* Type to map JSON content to VARCHAR.
*/
public static final int JSONVarchar = 5003;
/**
* Type to map JSON content to Clob.
*/
public static final int JSONClob = 5004;
/**
* Type to map JSON content to Blob.
*/
public static final int JSONBlob = 5005;
/**
* The data type name (VARCHAR, INTEGER ...)
*/
@@ -36,6 +36,13 @@ public class DbTypeMap {
put(Types.BLOB, new DbType("blob"));
put(Types.CLOB, new DbType("clob"));
put(DbType.JSON, new DbType("clob")); // Postgres maps this to JSON
put(DbType.JSONB, new DbType("clob")); // Postgres maps this to JSONB
put(DbType.JSONClob, new DbType("clob"));
put(DbType.JSONBlob, new DbType("blob"));
put(DbType.JSONVarchar, new DbType("varchar", 1000));
put(Types.LONGVARBINARY, new DbType("longvarbinary"));
put(Types.LONGVARCHAR, new DbType("lonvarchar"));
put(Types.VARBINARY, new DbType("varbinary", 255));
@@ -13,11 +13,6 @@ import java.sql.Types;
*/
public class PostgresPlatform extends DatabasePlatform {
/**
* Unique jdbc type id defined for hstore type.
*/
public static final int TYPE_HSTORE = 4001;
public PostgresPlatform() {
super();
this.name = "postgres";
@@ -43,8 +38,10 @@ public class PostgresPlatform extends DatabasePlatform {
this.openQuote = "\"";
this.closeQuote = "\"";
dbTypeMap.put(TYPE_HSTORE, new DbType("hstore"));
dbTypeMap.put(DbType.HSTORE, new DbType("hstore"));
dbTypeMap.put(DbType.JSON, new DbType("json"));
dbTypeMap.put(DbType.JSONB, new DbType("jsonb"));
dbTypeMap.put(Types.INTEGER, new DbType("integer", false));
dbTypeMap.put(Types.DOUBLE, new DbType("float"));
dbTypeMap.put(Types.TINYINT, new DbType("smallint"));
@@ -35,14 +35,29 @@ public class EJson {
public static void write(Object object, JsonGenerator jsonGenerator) throws IOException {
EJsonWriter.write(object, jsonGenerator);
}
/**
* Parse the json and return as a Map additionally specifying if the returned map should
* be modify aware meaning that it can detect when it has been modified.
*/
public static Map<String,Object> parseObject(String json, boolean modifyAware) throws IOException {
return EJsonReader.parseObject(json, modifyAware);
}
/**
* Parse the json and return as a Map.
*/
public static Map<String,Object> parseObject(String json) throws IOException {
return EJsonReader.parseObject(json);
}
/**
* Parse the json and return as a Map taking a reader.
*/
public static Map<String,Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
return EJsonReader.parseObject(reader, modifyAware);
}
/**
* Parse the json and return as a Map taking a reader.
*/
@@ -9,6 +9,10 @@ import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import com.avaje.ebeaninternal.server.type.ModifyAwareFlag;
import com.avaje.ebeaninternal.server.type.ModifyAwareList;
import com.avaje.ebeaninternal.server.type.ModifyAwareMap;
import com.avaje.ebeaninternal.server.type.ModifyAwareOwner;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
@@ -16,7 +20,12 @@ import com.fasterxml.jackson.core.JsonToken;
class EJsonReader {
static JsonFactory json = new JsonFactory();
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(String json, boolean modifyAware) throws IOException {
return (Map<String, Object>) parse(json, modifyAware);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(String json) throws IOException {
return (Map<String, Object>) parse(json);
@@ -26,7 +35,12 @@ class EJsonReader {
static Map<String, Object> parseObject(Reader reader) throws IOException {
return (Map<String, Object>) parse(reader);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
return (Map<String, Object>) parse(reader, modifyAware);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(JsonParser parser) throws IOException {
return (Map<String, Object>) parse(parser);
@@ -34,7 +48,7 @@ class EJsonReader {
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException {
return (Map<String, Object>)parse(parser, token);
return (Map<String, Object>)parse(parser, token, false);
}
@SuppressWarnings("unchecked")
@@ -56,40 +70,58 @@ class EJsonReader {
return parse(new StringReader(json));
}
static Object parse(String json, boolean modifyAware) throws IOException {
return parse(new StringReader(json), modifyAware);
}
static Object parse(Reader reader) throws IOException {
return parse(json.createParser(reader));
}
static Object parse(JsonParser parser) throws IOException {
return parse(parser, null);
static Object parse(Reader reader, boolean modifyAware) throws IOException {
return parse(json.createParser(reader), modifyAware);
}
static Object parse(JsonParser parser, JsonToken token) throws IOException {
return new EJsonReader(parser).parseJson(token);
static Object parse(JsonParser parser) throws IOException {
return parse(parser, null, false);
}
static Object parse(JsonParser parser, boolean modifyAware) throws IOException {
return parse(parser, null, modifyAware);
}
static Object parse(JsonParser parser, JsonToken token, boolean modifyAware) throws IOException {
return new EJsonReader(parser, modifyAware).parseJson(token);
}
private final JsonParser parser;
private final boolean modifyAware;
private final ModifyAwareFlag modifyAwareOwner;
private int depth;
private Stack stack;
private Context currentContext;
EJsonReader(JsonParser parser) {
EJsonReader(JsonParser parser, boolean modifyAware) {
this.parser = parser;
this.modifyAware = modifyAware;
this.modifyAwareOwner = (modifyAware) ? new ModifyAwareFlag() : null;
}
private void startArray() {
depth++;
stack.push(currentContext);
currentContext = new ArrayContext();
currentContext = modifyAware ? new ArrayContext(modifyAwareOwner) : new ArrayContext();
}
private void startObject() {
depth++;
stack.push(currentContext);
currentContext = new ObjectContext();
currentContext = modifyAware ? new ObjectContext(modifyAwareOwner) : new ObjectContext();
}
private void endArray() {
@@ -105,6 +137,9 @@ class EJsonReader {
if (!stack.isEmpty()) {
currentContext = stack.pop(currentContext);
}
if (modifyAwareOwner != null) {
modifyAwareOwner.resetMarkedDirty();
}
}
private void setValue(Object value) {
@@ -235,10 +270,18 @@ class EJsonReader {
private static class ObjectContext extends Context {
private final Map<String, Object> map = new LinkedHashMap<String, Object>();
private final Map<String, Object> map;
private String key;
ObjectContext() {
map = new LinkedHashMap<String, Object>();
}
ObjectContext(ModifyAwareOwner owner) {
map = new ModifyAwareMap<String, Object>(owner, new LinkedHashMap<String, Object>());
}
public void popContext(Context temp) {
setValue(temp.getValue());
}
@@ -262,7 +305,15 @@ class EJsonReader {
private static class ArrayContext extends Context {
private final List<Object> values = new ArrayList<Object>();
private final List<Object> values;
ArrayContext() {
values = new ArrayList<Object>();
}
ArrayContext(ModifyAwareOwner owner) {
values = new ModifyAwareList<Object>(owner, new ArrayList<Object>());
}
public void popContext(Context temp) {
values.add(temp.getValue());
@@ -97,7 +97,7 @@ public class DeployBeanPropertyLists {
discDeployProp.setDbColumn(discriminatorColumn);
// create the discriminator BeanProperty and only register it in the propertyMap
BeanProperty dprop = new BeanProperty(owner, desc, discDeployProp);
BeanProperty dprop = new BeanProperty(desc, discDeployProp);
propertyMap.put(dprop.getName(), dprop);
}
@@ -382,6 +382,6 @@ public class DeployBeanPropertyLists {
return new BeanPropertyCompound(owner, desc, (DeployBeanPropertyCompound) deployProp);
}
return new BeanProperty(owner, desc, deployProp);
return new BeanProperty(desc, deployProp);
}
}
@@ -135,6 +135,18 @@ public class AnnotationFields extends AnnotationParser {
util.setLobType(prop);
}
DbJson dbJson = get(prop, DbJson.class);
if (dbJson != null) {
util.setDbJsonType(prop, dbJson);
} else {
if (get(prop, DbJsonB.class) != null) {
util.setDbJsonBType(prop);
}
}
if (get(prop, ColumnHstore.class) != null) {
util.setDbHstore(prop);
}
Formula formula = get(prop, Formula.class);
if (formula != null) {
prop.setSqlFormula(formula.select(), formula.join());
@@ -1,33 +1,22 @@
package com.avaje.ebeaninternal.server.deploy.parse;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import com.avaje.ebean.annotation.ColumnHstore;
import com.avaje.ebean.annotation.DbJson;
import com.avaje.ebean.annotation.DbJsonB;
import com.avaje.ebeaninternal.server.deploy.DetermineManyType;
import com.avaje.ebeaninternal.server.deploy.ManyType;
import com.avaje.ebeaninternal.server.deploy.meta.*;
import com.avaje.ebeaninternal.server.type.CtCompoundType;
import com.avaje.ebeaninternal.server.type.ScalarType;
import com.avaje.ebeaninternal.server.type.TypeManager;
import com.avaje.ebeaninternal.server.type.reflect.CheckImmutableResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.ManyToOne;
import javax.persistence.PersistenceException;
import javax.persistence.Transient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.avaje.ebean.annotation.ColumnHstore;
import com.avaje.ebeaninternal.server.core.Message;
import com.avaje.ebeaninternal.server.deploy.DetermineManyType;
import com.avaje.ebeaninternal.server.deploy.ManyType;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyCompound;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertySimpleCollection;
import com.avaje.ebeaninternal.server.type.CtCompoundType;
import com.avaje.ebeaninternal.server.type.ScalarType;
import com.avaje.ebeaninternal.server.type.ScalarTypePostgresHstore;
import com.avaje.ebeaninternal.server.type.TypeManager;
import com.avaje.ebeaninternal.server.type.reflect.CheckImmutableResponse;
import java.lang.reflect.*;
/**
* Create the properties for a bean.
@@ -95,15 +84,13 @@ public class DeployCreateProperties {
Field field = fields[i];
if (Modifier.isStatic(field.getModifiers())) {
// not interested in static fields
logger.trace("Skipping static field {} in {}", field.getName(), beanType.getName());
} else if (Modifier.isTransient(field.getModifiers())) {
// not interested in transient fields
logger.trace("Skipping transient field " + field.getName() + " in " + beanType.getName());
logger.trace("Skipping transient field {} in {}", field.getName(), beanType.getName());
} else if (ignoreFieldByName(field.getName())) {
// not interested this field (ebean or aspectJ field)
} else {
} else if (!ignoreFieldByName(field.getName())) {
String fieldName = getFieldName(field, beanType);
String initFieldName = initCap(fieldName);
@@ -118,14 +105,10 @@ public class DeployCreateProperties {
prop.setSortOrder((level * 10000 + 100 - i + sortOverride));
DeployBeanProperty replaced = desc.addBeanProperty(prop);
if (replaced != null) {
if (replaced.isTransient()) {
// expected for inheritance...
} else {
String msg = "Huh??? property " + prop.getFullBeanName() + " being defined twice";
msg += " but replaced property was not transient? This is not expected?";
logger.warn(msg);
}
if (replaced != null && !replaced.isTransient()) {
String msg = "Huh??? property " + prop.getFullBeanName() + " being defined twice";
msg += " but replaced property was not transient? This is not expected?";
logger.warn(msg);
}
}
}
@@ -210,33 +193,6 @@ public class DeployCreateProperties {
return null;
}
/**
* Find a public non-static setter method that matches this field (according to bean-spec rules).
*/
private Method findSetter(Field field, String initFieldName, Method[] declaredMethods, boolean scalaObject) {
String methSetName = "set" + initFieldName;
String scalaSetName = field.getName() + "_$eq";
for (int i = 0; i < declaredMethods.length; i++) {
Method m = declaredMethods[i];
if ((scalaObject && m.getName().equals(scalaSetName)) || m.getName().equals(methSetName)) {
Class<?>[] params = m.getParameterTypes();
if (params.length == 1 && field.getType().equals(params[0])) {
if (void.class.equals(m.getReturnType())) {
int modifiers = m.getModifiers();
if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers)) {
return m;
}
}
}
}
}
return null;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private DeployBeanProperty createManyType(DeployBeanDescriptor<?> desc, Class<?> targetType, ManyType manyType) {
@@ -266,16 +222,8 @@ public class DeployCreateProperties {
propertyType = tt;
}
}
Class<?> innerType = propertyType;
String specialTypeKey = getSpecialScalarType(field);
if (specialTypeKey != null) {
ScalarType<?> scalarType = typeManager.getScalarTypeFromKey(specialTypeKey);
if (scalarType == null) {
logger.error("Could not find ScalarType to match key ["+specialTypeKey+"]");
} else {
return new DeployBeanProperty(desc, propertyType, scalarType, null);
}
if (isMappedType(field)) {
return new DeployBeanProperty(desc, propertyType, null, null);
}
// check for Collection type (list, set or map)
@@ -295,16 +243,16 @@ public class DeployCreateProperties {
return createManyType(desc, targetType, manyType);
}
if (innerType.isEnum() || innerType.isPrimitive()) {
if (propertyType.isEnum() || propertyType.isPrimitive()) {
return new DeployBeanProperty(desc, propertyType, null, null);
}
ScalarType<?> scalarType = typeManager.getScalarType(innerType);
ScalarType<?> scalarType = typeManager.getScalarType(propertyType);
if (scalarType != null) {
return new DeployBeanProperty(desc, propertyType, scalarType, null);
}
CtCompoundType<?> compoundType = typeManager.getCompoundType(innerType);
CtCompoundType<?> compoundType = typeManager.getCompoundType(propertyType);
if (compoundType != null) {
return new DeployBeanPropertyCompound(desc, propertyType, compoundType, null);
}
@@ -313,19 +261,19 @@ public class DeployCreateProperties {
return null;
}
try {
CheckImmutableResponse checkImmutable = typeManager.checkImmutable(innerType);
CheckImmutableResponse checkImmutable = typeManager.checkImmutable(propertyType);
if (checkImmutable.isImmutable()) {
if (checkImmutable.isCompoundType()) {
// use reflection to support compound immutable value objects
typeManager.recursiveCreateScalarDataReader(innerType);
compoundType = typeManager.getCompoundType(innerType);
typeManager.recursiveCreateScalarDataReader(propertyType);
compoundType = typeManager.getCompoundType(propertyType);
if (compoundType != null) {
return new DeployBeanPropertyCompound(desc, propertyType, compoundType, null);
}
} else {
// use reflection to support simple immutable value objects
scalarType = typeManager.recursiveCreateScalarTypes(innerType);
scalarType = typeManager.recursiveCreateScalarTypes(propertyType);
return new DeployBeanProperty(desc, propertyType, scalarType, null);
}
}
@@ -338,13 +286,13 @@ public class DeployCreateProperties {
}
}
private String getSpecialScalarType(Field field) {
if (field.getAnnotation(ColumnHstore.class) != null) {
return ScalarTypePostgresHstore.KEY;
}
return null;
/**
* Return true if the field has one of the special mappings.
*/
private boolean isMappedType(Field field) {
return (field.getAnnotation(DbJson.class) != null)
|| (field.getAnnotation(DbJsonB.class) != null)
|| (field.getAnnotation(ColumnHstore.class) != null);
}
private boolean isTransientField(Field field) {
@@ -5,6 +5,8 @@ import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.PersistenceException;
import com.avaje.ebean.annotation.DbJson;
import com.avaje.ebean.annotation.DbJsonType;
import com.avaje.ebean.config.EncryptDeploy;
import com.avaje.ebean.config.EncryptDeployManager;
import com.avaje.ebean.config.EncryptKeyManager;
@@ -13,6 +15,7 @@ import com.avaje.ebean.config.NamingConvention;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.config.TableName;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
import com.avaje.ebean.config.dbplatform.DbType;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyCompound;
import com.avaje.ebeaninternal.server.type.DataEncryptSupport;
@@ -41,6 +44,8 @@ public class DeployUtil {
*/
private static final int dbBLOBType = Types.BLOB;
private static final int DEFAULT_JSON_VARCHAR_LENGTH = 3000;
private final NamingConvention namingConvention;
private final TypeManager typeManager;
@@ -184,6 +189,72 @@ public class DeployUtil {
}
}
/**
* Map to Postgres HSTORE type.
*/
public void setDbHstore(DeployBeanProperty prop) {
ScalarType<?> scalarType = typeManager.getScalarType(DbType.HSTORE);
if (scalarType == null) {
// this should never occur actually
throw new RuntimeException("No ScalarType found for HSTORE on [" + prop.getFullBeanName() + "]");
}
prop.setDbType(DbType.HSTORE);
prop.setScalarType(scalarType);
}
/**
* This property is marked as a Lob object.
*/
public void setDbJsonType(DeployBeanProperty prop, DbJson dbJsonType) {
int dbType = getDbJsonStorage(dbJsonType.storage());
setDbJsonType(prop, dbType, dbJsonType.length());
}
public void setDbJsonBType(DeployBeanProperty prop) {
setDbJsonType(prop, DbType.JSONB, 0);
}
private void setDbJsonType(DeployBeanProperty prop, int dbType, int dbLength) {
Class<?> type = prop.getPropertyType();
ScalarType<?> scalarType = typeManager.getJsonScalarType(type, dbType);
if (scalarType == null) {
// this should never occur actually
throw new RuntimeException("No ScalarType for JSON type [" + type + "] [" + dbType + "]");
}
prop.setDbType(dbType);
prop.setScalarType(scalarType);
if (dbType == Types.VARCHAR) {
// determine the db column size
int columnLength = (dbLength > 0) ? dbLength : DEFAULT_JSON_VARCHAR_LENGTH;
prop.setDbLength(columnLength);
}
}
/**
* Return the JDBC type for the JSON storage type.
*/
private int getDbJsonStorage(DbJsonType dbJsonType) {
switch (dbJsonType) {
case JSON:
return DbType.JSON;
case JSONB:
return DbType.JSONB;
case VARCHAR:
return Types.VARCHAR;
case CLOB:
return Types.CLOB;
case BLOB:
return Types.BLOB;
default:
return DbType.JSON;
}
}
/**
* This property is marked as a Lob object.
*/
@@ -1,6 +1,7 @@
package com.avaje.ebeaninternal.server.type;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.sql.Array;
import java.sql.SQLException;
@@ -19,10 +20,14 @@ public interface DataReader {
byte[] getBlobBytes() throws SQLException;
InputStream getBlobInputStream() throws SQLException;
String getStringFromStream() throws SQLException;
String getStringClob() throws SQLException;
Reader getClobReader() throws SQLException;
String getString() throws SQLException;
Boolean getBoolean() throws SQLException;
@@ -1,5 +1,22 @@
package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.annotation.EnumMapping;
import com.avaje.ebean.annotation.EnumValue;
import com.avaje.ebean.config.*;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
import com.avaje.ebean.config.dbplatform.DbType;
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.ObjectMapper;
import org.joda.time.*;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
@@ -13,42 +30,12 @@ import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Currency;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import java.time.Duration;
import java.time.Instant;
import java.time.Period;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import com.avaje.ebean.config.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.joda.time.DateMidnight;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.avaje.ebean.annotation.EnumMapping;
import com.avaje.ebean.annotation.EnumValue;
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.CheckImmutable;
import com.avaje.ebeaninternal.server.type.reflect.CheckImmutableResponse;
import com.avaje.ebeaninternal.server.type.reflect.ImmutableMeta;
import com.avaje.ebeaninternal.server.type.reflect.ImmutableMetaFactory;
import com.avaje.ebeaninternal.server.type.reflect.KnownImmutable;
import com.avaje.ebeaninternal.server.type.reflect.ReflectionBasedCompoundType;
import com.avaje.ebeaninternal.server.type.reflect.ReflectionBasedCompoundTypeProperty;
import com.avaje.ebeaninternal.server.type.reflect.ReflectionBasedTypeBuilder;
/**
* Default implementation of TypeManager.
* <p>
@@ -65,10 +52,16 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
private final ConcurrentHashMap<Integer, ScalarType<?>> nativeMap;
private final ConcurrentHashMap<String, ScalarType<?>> customTypeMap;
private final DefaultTypeFactory extraTypeFactory;
private final ScalarTypeJsonMap JSON_MAP_CLOB = new ScalarTypeJsonMap.Clob();
private final ScalarTypeJsonMap jsonMapClob = JSON_MAP_CLOB;
private final ScalarTypeJsonMap jsonMapBlob = new ScalarTypeJsonMap.Blob();
private final ScalarTypeJsonMap jsonMapVarchar = new ScalarTypeJsonMap.Varchar();
private final ScalarTypeJsonMap jsonMapJson;
private final ScalarTypeJsonMap jsonMapJsonb;
private final ScalarTypeFile fileType = new ScalarTypeFile();
private final ScalarType<?> charType = new ScalarTypeChar();
@@ -134,9 +127,6 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
*/
public DefaultTypeManager(ServerConfig config, BootupClasses bootupClasses) {
int clobType = config.getDatabasePlatform().getClobDbType();
int blobType = config.getDatabasePlatform().getBlobDbType();
this.jsonDateTime = config.getJsonDateTime();
this.checkImmutable = new CheckImmutable(this);
this.reflectScalarBuilder = new ReflectionBasedTypeBuilder(this);
@@ -144,18 +134,24 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
this.compoundTypeMap = new ConcurrentHashMap<Class<?>, CtCompoundType<?>>();
this.typeMap = new ConcurrentHashMap<Class<?>, ScalarType<?>>();
this.nativeMap = new ConcurrentHashMap<Integer, ScalarType<?>>();
this.customTypeMap = new ConcurrentHashMap<String, ScalarType<?>>();
this.customTypeMap.put(ScalarTypePostgresHstore.KEY, new ScalarTypePostgresHstore());
this.objectMapperPresent = ClassUtil.isPresent("com.fasterxml.jackson.databind.ObjectMapper", this.getClass());
this.extraTypeFactory = new DefaultTypeFactory(config);
initialiseStandard(jsonDateTime, clobType, blobType, config.isUuidStoreAsBinary());
initialiseStandard(jsonDateTime, config);
initialiseJavaTimeTypes(jsonDateTime, config);
initialiseJodaTypes(jsonDateTime);
if (isPostgres(config.getDatabasePlatform())) {
// Postgres has special DB types for JSON/JSONB
this.jsonMapJson = new ScalarTypeJsonMapPostgres.JSON();
this.jsonMapJsonb = new ScalarTypeJsonMapPostgres.JSONB();
} else {
this.jsonMapJson = JSON_MAP_CLOB;
this.jsonMapJsonb = JSON_MAP_CLOB;
}
if (bootupClasses != null) {
initialiseCustomScalarTypes(jsonDateTime, bootupClasses, config);
initialiseScalarConverters(bootupClasses);
@@ -163,12 +159,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
}
}
/**
* Lookup a special or custom scalar type by key.
*/
@Override
public ScalarType<?> getScalarTypeFromKey(String specialTypeKey) {
return customTypeMap.get(specialTypeKey);
private boolean isPostgres(DatabasePlatform databasePlatform) {
return databasePlatform.getName().toLowerCase().startsWith("postgre");
}
public boolean isKnownImmutable(Class<?> cls) {
@@ -295,6 +287,24 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
return reader;
}
@Override
public ScalarType<?> getJsonScalarType(Class<?> type, int dbType) {
if (type.equals(Map.class)) {
switch (dbType) {
case Types.VARCHAR : return jsonMapVarchar;
case Types.BLOB: return jsonMapBlob;
case Types.CLOB : return jsonMapClob;
case DbType.JSONB: return jsonMapJsonb;
case DbType.JSON: return jsonMapJson;
default:
return jsonMapJson;
}
}
throw new IllegalArgumentException("Type [" + type + "] unsupported for @DbJson mapping");
}
/**
* Return a ScalarType for a given class.
* <p>
@@ -370,8 +380,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
return value;
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private boolean isIntegerType(String s) {
try {
Integer.parseInt(s);
return true;
@@ -685,7 +695,14 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
* Register all the standard types supported. This is the standard JDBC types
* plus some other common types such as java.util.Date and java.util.Calendar.
*/
protected void initialiseStandard(JsonConfig.DateTime mode, int platformClobType, int platformBlobType, boolean binaryUUID) {
protected void initialiseStandard(JsonConfig.DateTime mode, ServerConfig config) {
boolean binaryUUID = config.isUuidStoreAsBinary();
DatabasePlatform databasePlatform = config.getDatabasePlatform();
int platformClobType = databasePlatform.getClobDbType();
int platformBlobType = databasePlatform.getBlobDbType();
nativeMap.put(DbType.HSTORE, new ScalarTypePostgresHstore());
ScalarType<?> utilDateType = extraTypeFactory.createUtilDate(mode);
typeMap.put(java.util.Date.class, utilDateType);
@@ -0,0 +1,26 @@
package com.avaje.ebeaninternal.server.type;
/**
* Detects when content has been modified and as such needs to be persisted (included in an update).
*/
public class ModifyAwareFlag implements ModifyAwareOwner {
boolean dirty;
@Override
public boolean isMarkedDirty() {
if (!dirty) return false;
dirty = false;
return true;
}
@Override
public void markAsModified() {
dirty = true;
}
@Override
public void resetMarkedDirty() {
dirty = false;
}
}
@@ -0,0 +1,166 @@
package com.avaje.ebeaninternal.server.type;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
/**
* Modify aware wrapper of a list.
*/
public class ModifyAwareList<E> implements List<E>, ModifyAwareOwner {
final List<E> list;
final ModifyAwareOwner owner;
public ModifyAwareList(List<E> list) {
this.list = list;
this.owner = new ModifyAwareFlag();
}
public ModifyAwareList(ModifyAwareOwner owner, List<E> list) {
this.list = list;
this.owner = owner;
}
@Override
public boolean isMarkedDirty() {
return owner.isMarkedDirty();
}
@Override
public void markAsModified() {
owner.markAsModified();
}
@Override
public void resetMarkedDirty() {
owner.resetMarkedDirty();
}
@Override
public int size() {
return list.size();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public boolean contains(Object o) {
return list.contains(o);
}
@Override
public Iterator<E> iterator() {
return new ModifyAwareIterator<E>(owner, list.iterator());
}
@Override
public Object[] toArray() {
return list.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}
@Override
public boolean add(E e) {
owner.markAsModified();
return list.add(e);
}
@Override
public boolean remove(Object o) {
owner.markAsModified();
return list.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
owner.markAsModified();
return list.addAll(c);
}
@Override
public boolean addAll(int index, Collection<? extends E> c) {
owner.markAsModified();
return list.addAll(index, c);
}
@Override
public boolean removeAll(Collection<?> c) {
owner.markAsModified();
return list.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
owner.markAsModified();
return list.retainAll(c);
}
@Override
public void clear() {
owner.markAsModified();
list.clear();
}
@Override
public E get(int index) {
return list.get(index);
}
@Override
public E set(int index, E element) {
owner.markAsModified();
return list.set(index, element);
}
@Override
public void add(int index, E element) {
owner.markAsModified();
list.add(index, element);
}
@Override
public E remove(int index) {
owner.markAsModified();
return list.remove(index);
}
@Override
public int indexOf(Object o) {
return list.indexOf(o);
}
@Override
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
@Override
public ListIterator<E> listIterator() {
return new ModifyAwareListIterator<E>(owner, list.listIterator());
}
@Override
public ListIterator<E> listIterator(int index) {
return new ModifyAwareListIterator<E>(owner, list.listIterator(index));
}
@Override
public List<E> subList(int fromIndex, int toIndex) {
return new ModifyAwareList<E>(owner,list.subList(fromIndex, toIndex));
}
}
@@ -0,0 +1,66 @@
package com.avaje.ebeaninternal.server.type;
import java.util.ListIterator;
/**
* Modify aware wrapper of a ListIterator.
*/
public class ModifyAwareListIterator<E> implements ListIterator<E> {
final ModifyAwareOwner owner;
final ListIterator<E> iterator;
public ModifyAwareListIterator(ModifyAwareOwner owner, ListIterator<E> iterator) {
this.owner = owner;
this.iterator = iterator;
}
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public E next() {
return iterator.next();
}
@Override
public boolean hasPrevious() {
return iterator.hasPrevious();
}
@Override
public E previous() {
return iterator.previous();
}
@Override
public int nextIndex() {
return iterator.nextIndex();
}
@Override
public int previousIndex() {
return iterator.previousIndex();
}
@Override
public void remove() {
owner.markAsModified();
iterator.remove();
}
@Override
public void set(E e) {
owner.markAsModified();
iterator.set(e);
}
@Override
public void add(E e) {
owner.markAsModified();
iterator.add(e);
}
}
@@ -9,18 +9,21 @@ import java.util.Set;
*/
public class ModifyAwareMap<K, V> implements Map<K, V>, ModifyAwareOwner {
/**
* Dirty flag set when the map has been modified.
*/
private boolean dirty;
final ModifyAwareOwner owner;
/**
* The underlying map.
*/
private Map<K, V> map;
final Map<K, V> map;
public ModifyAwareMap(Map<K, V> underyling) {
this.map = underyling;
this.owner = new ModifyAwareFlag();
}
public ModifyAwareMap(ModifyAwareOwner owner, Map<K, V> underyling) {
this.owner = owner;
this.map = underyling;
}
public String toString() {
@@ -29,12 +32,17 @@ public class ModifyAwareMap<K, V> implements Map<K, V>, ModifyAwareOwner {
@Override
public boolean isMarkedDirty() {
return dirty;
return owner.isMarkedDirty();
}
@Override
public void markAsModified() {
dirty = true;
owner.markAsModified();
}
@Override
public void resetMarkedDirty() {
owner.resetMarkedDirty();
}
@Override
@@ -7,6 +7,7 @@ public interface ModifyAwareOwner {
/**
* Return true if the value is considered dirty.
* Note that this resets the dirty status back to clean.
*/
boolean isMarkedDirty();
@@ -14,4 +15,10 @@ public interface ModifyAwareOwner {
* Marks the object as modified.
*/
void markAsModified();
/**
* Reset the dirty state to clean.
*/
void resetMarkedDirty();
}
@@ -168,6 +168,15 @@ public class RsetDataReader implements DataReader {
return readStringLob(reader);
}
@Override
public Reader getClobReader() throws SQLException {
Clob clob = rset.getClob(pos());
if (clob == null) {
return null;
}
return clob.getCharacterStream();
}
public String getStringClob() throws SQLException {
Clob clob = rset.getClob(pos());
@@ -212,6 +221,14 @@ public class RsetDataReader implements DataReader {
return getBinaryLob(in);
}
public InputStream getBlobInputStream() throws SQLException {
Blob blob = rset.getBlob(pos());
if (blob == null) {
return null;
}
return blob.getBinaryStream();
}
protected byte[] getBinaryLob(InputStream in) throws SQLException {
try {
@@ -0,0 +1,214 @@
package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.text.TextException;
import com.avaje.ebean.text.json.EJson;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Map;
/**
* Type which maps Map<String,Object> to various DB types (Clob, Varchar, Blob) in JSON format.
*/
public abstract class ScalarTypeJsonMap extends ScalarTypeBase<Map> {
public static class Clob extends ScalarTypeJsonMap {
public Clob() {
super(Types.CLOB);
}
@Override
public Map read(DataReader dataReader) throws SQLException {
Reader reader = dataReader.getClobReader();
if (reader == null) {
return null;
}
try {
Map map = parse(reader);
reader.close();
return map;
} catch (IOException e) {
throw new SQLException("Error reading Clob stream from DB", e);
}
}
}
public static class Varchar extends ScalarTypeJsonMap {
public Varchar() {
super(Types.VARCHAR);
}
}
public static class Blob extends ScalarTypeJsonMap {
public Blob() {
super(Types.BLOB);
}
@Override
public Map read(DataReader dataReader) throws SQLException {
InputStream is = dataReader.getBlobInputStream();
if (is == null) {
return null;
}
try {
InputStreamReader reader = new InputStreamReader(is);
Map map = parse(reader);
reader.close();
return map;
} catch (IOException e) {
throw new SQLException("Error reading Blob stream from DB", e);
}
}
@Override
public void bind(DataBind b, Map value) throws SQLException {
if (value == null) {
b.setNull(Types.BLOB);
} else {
String rawJson = formatValue(value);
InputStream stream = new ByteArrayInputStream(rawJson.getBytes(StandardCharsets.UTF_8));
b.setBlob(stream);
}
}
}
public ScalarTypeJsonMap(int jdbcType) {
super(Map.class, false, jdbcType);
}
/**
* 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 !(value instanceof ModifyAwareOwner) || ((ModifyAwareOwner) value).isMarkedDirty();
}
@Override
public Map read(DataReader dataReader) throws SQLException {
String rawJson = dataReader.getString();
if (rawJson == null) {
return null;
}
return parse(rawJson);
}
@Override
public void bind(DataBind b, Map value) throws SQLException {
if (value == null) {
b.setNull(Types.VARCHAR);
} else {
String rawJson = formatValue(value);
b.setString(rawJson);
}
}
@Override
public Object toJdbcType(Object value) {
return value;
}
@Override
public Map toBeanType(Object value) {
return (Map) value;
}
@Override
public String formatValue(Map v) {
try {
return EJson.write(v);
} catch (IOException e) {
throw new TextException(e);
}
}
@Override
public Map parse(String value) {
try {
// return a modify aware map
return EJson.parseObject(value, true);
} catch (IOException e) {
throw new TextException(e);
}
}
public Map parse(Reader reader) {
try {
// return a modify aware map
return EJson.parseObject(reader, true);
} catch (IOException e) {
throw new TextException(e);
}
}
@Override
public Map convertFromMillis(long dateTime) {
throw new RuntimeException("Should never be called");
}
@Override
public boolean isDateTimeCapable() {
return false;
}
@Override
public Map readData(DataInput dataInput) throws IOException {
if (!dataInput.readBoolean()) {
return null;
} else {
String json = dataInput.readUTF();
return parse(json);
}
}
@Override
public void writeData(DataOutput dataOutput, Map v) throws IOException {
if (v == null) {
dataOutput.writeBoolean(false);
} else {
dataOutput.writeBoolean(true);
String json = format(v);
dataOutput.writeUTF(json);
}
}
@Override
public void jsonWrite(JsonGenerator ctx, String name, Map value) throws IOException {
// write the field name followed by the Map/JSON Object
if (value == null) {
ctx.writeNullField(name);
} else {
ctx.writeFieldName(name);
EJson.write(value, ctx);
}
}
@Override
public Map jsonRead(JsonParser ctx, JsonToken event) throws IOException {
// at this point the BeanProperty has read the START_OBJECT token
// to check for a null value. Pass the START_OBJECT token through to
// the EJson parsing so that it knows the first token has been read
return EJson.parseObject(ctx, event);
}
}
@@ -0,0 +1,55 @@
package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.config.dbplatform.DbType;
import org.postgresql.util.PGobject;
import java.sql.SQLException;
import java.util.Map;
/**
* Support for the Postgres DB types JSON and JSONB.
*/
public abstract class ScalarTypeJsonMapPostgres extends ScalarTypeJsonMap {
private static final String POSTGRES_TYPE_JSON = "json";
private static final String POSTGRES_TYPE_JSONB = "jsonb";
final String postgresType;
ScalarTypeJsonMapPostgres(int jdbcType, String postgresType) {
super(jdbcType);
this.postgresType = postgresType;
}
@Override
public void bind(DataBind b, Map value) throws SQLException {
String rawJson = (value == null) ? null : formatValue(value);
PGobject pgo = new PGobject();
pgo.setType(postgresType);
pgo.setValue(rawJson);
b.setObject(pgo);
}
/**
* ScalarType mapping java Map type to Postgres JSON database type.
*/
public static class JSON extends ScalarTypeJsonMapPostgres {
public JSON() {
super(DbType.JSON, POSTGRES_TYPE_JSON);
}
}
/**
* ScalarType mapping java Map type to Postgres JSONB database type.
*/
public static class JSONB extends ScalarTypeJsonMapPostgres {
public JSONB() {
super(DbType.JSONB, POSTGRES_TYPE_JSONB);
}
}
}
@@ -1,18 +1,18 @@
package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.config.dbplatform.DbType;
import com.avaje.ebean.text.TextException;
import com.avaje.ebean.text.json.EJson;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Map;
import com.avaje.ebean.config.dbplatform.PostgresPlatform;
import com.avaje.ebean.text.json.EJson;
import com.avaje.ebean.text.TextException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
/**
* Postgres Hstore type which maps Map<String,String> to a single 'HStore column' in the DB.
*/
@@ -21,10 +21,8 @@ public class ScalarTypePostgresHstore extends ScalarTypeBase<Map> {
public static final String KEY = "hstore";
public static final int HSTORE_TYPE = PostgresPlatform.TYPE_HSTORE;
public ScalarTypePostgresHstore() {
super(Map.class, false, HSTORE_TYPE);
super(Map.class, false, DbType.HSTORE);
}
@Override
@@ -58,7 +58,10 @@ public interface TypeManager {
ScalarType<?> createEnumScalarType(Class<?> enumType);
/**
* Find a scalarType using a custom type key. Used for Hstore and similar special types.
* Return the ScalarType used to handle JSON content.
* <p>
* Note that type expected to be JsonNode or Map.
* </p>
*/
ScalarType<?> getScalarTypeFromKey(String specialTypeKey);
ScalarType<?> getJsonScalarType(Class<?> type, int dbType);
}