#318 - JSON - toBean error at Transient (with support for #327, #328, #329)

This commit is contained in:
Robin Bygrave
2015-07-19 21:41:20 +12:00
parent a00980c61f
commit 160e4d7dd1
25 changed files with 633 additions and 1358 deletions
@@ -15,6 +15,8 @@ public class JsonReadOptions {
protected Map<String, JsonReadBeanVisitor<?>> visitorMap;
protected Object objectMapper;
/**
* Default constructor.
*/
@@ -44,4 +46,17 @@ public class JsonReadOptions {
return this;
}
/**
* Return the Jackson ObjectMapper to use (if not wanted to use the objectMapper set on the ServerConfig).
*/
public Object getObjectMapper() {
return objectMapper;
}
/**
* Set the Jackson ObjectMapper to use (if not wanted to use the objectMapper set on the ServerConfig).
*/
public void setObjectMapper(Object objectMapper) {
this.objectMapper = objectMapper;
}
}
@@ -49,6 +49,21 @@ public class ClassUtil {
return classLoader;
}
/**
* Return true if Jackson annotations like JsonIgnore are present.
*/
public static boolean isJacksonAnnotationsPresent() {
return isPresent("com.fasterxml.jackson.annotation.JsonIgnore", null);
}
/**
* Return true if Jackson ObjectMapper is present.
*/
public static boolean isJacksonObjectMapperPresent() {
return isPresent("com.fasterxml.jackson.databind.ObjectMapper", null);
}
/**
* Return true if the given class is present.
*/
@@ -22,6 +22,8 @@ import com.avaje.ebeaninternal.server.text.json.WriteJson;
import com.avaje.ebeaninternal.server.type.DataBind;
import com.avaje.ebeaninternal.server.type.ScalarType;
import com.fasterxml.jackson.core.JsonToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
import java.io.DataInput;
@@ -39,6 +41,8 @@ import java.util.Map;
*/
public class BeanProperty implements ElPropertyValue {
private static final Logger logger = LoggerFactory.getLogger(BeanProperty.class);
/**
* Flag to mark this at part of the unique id.
*/
@@ -299,8 +303,8 @@ public class BeanProperty implements ElPropertyValue {
this.elPlaceHolder = tableAliasIntern(descriptor, deploy.getElPlaceHolder(et), false, null);
this.elPlaceHolderEncrypted = tableAliasIntern(descriptor, deploy.getElPlaceHolder(et), dbEncrypted, dbColumn);
this.jsonSerialize = deploy.isExposeSerialize();
this.jsonDeserialize = deploy.isExposeDeserialize();
this.jsonSerialize = deploy.isJsonSerialize();
this.jsonDeserialize = deploy.isJsonDeserialize();
}
private String tableAliasIntern(BeanDescriptor<?> descriptor, String s, boolean dbEncrypted, String dbColumn) {
@@ -1072,22 +1076,40 @@ public class BeanProperty implements ElPropertyValue {
if (value == null) {
writeJson.writeNull(name);
} else {
scalarType.jsonWrite(writeJson.gen(), name, value);
if (scalarType != null) {
scalarType.jsonWrite(writeJson.gen(), name, value);
} else {
writeJson.writeValueUsingObjectMapper(name, value);
}
}
}
public void jsonRead(ReadJson ctx, EntityBean bean) throws IOException {
if (!jsonDeserialize) {
return;
}
JsonToken event = ctx.nextToken();
if (JsonToken.VALUE_NULL == event) {
setValue(bean, null);
if (jsonDeserialize) {
setValue(bean, null);
}
} else {
// expect to read non-null json value
Object objValue = scalarType.jsonRead(ctx.getParser(), event);
setValue(bean, objValue);
Object objValue;
if (scalarType != null) {
objValue = scalarType.jsonRead(ctx.getParser(), event);
} else {
try {
objValue = ctx.readValueUsingObjectMapper(propertyType);
} catch (IOException e) {
// change in behavior for #318
objValue = null;
String msg = "Error trying to use Jackson ObjectMapper to read transient property "
+ getFullBeanName() +" - consider marking this property with @JsonIgnore";
logger.error(msg, e);
}
}
if (jsonDeserialize) {
setValue(bean, objValue);
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -873,7 +873,11 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
if (help != null) {
help.jsonWrite(ctx, name, value, include != null);
} else {
ctx.toJson(name, (Collection<?>) value);
if (isTransient) {
ctx.writeValueUsingObjectMapper(name, value);
} else {
ctx.toJson(name, (Collection<?>) value);
}
}
ctx.popParentBeanMany();
}
@@ -1,23 +1,42 @@
package com.avaje.ebeaninternal.server.deploy;
import java.io.IOException;
import com.avaje.ebean.bean.BeanCollection;
import com.avaje.ebean.bean.BeanCollectionAdd;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebeaninternal.api.ClassUtil;
import com.avaje.ebeaninternal.server.text.json.ReadJson;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import java.io.IOException;
/**
* Help BeanPropertyAssocMany with JSON processing.
*/
public class BeanPropertyAssocManyJsonHelp {
/**
* The associated many property.
*/
private final BeanPropertyAssocMany<?> many;
/**
* Helper used to read json for transient 'many' properties.
*/
private final BeanPropertyAssocManyJsonTransient jsonTransient;
/**
* Construct for the owning many property.
*/
public BeanPropertyAssocManyJsonHelp(BeanPropertyAssocMany<?> many) {
this.many = many;
this.jsonTransient = !ClassUtil.isJacksonObjectMapperPresent() ? null : new BeanPropertyAssocManyJsonTransient();
}
/**
* Read the JSON for this property.
*/
public void jsonRead(ReadJson readJson, EntityBean parentBean) throws IOException {
if (!this.many.jsonDeserialize) {
@@ -33,6 +52,11 @@ public class BeanPropertyAssocManyJsonHelp {
throw new JsonParseException("Unexpected token " + event + " - expecting start_array ", parser.getCurrentLocation());
}
if (many.isTransient()) {
jsonReadTransientUsingObjectMapper(readJson, parentBean);
return;
}
BeanCollection<?> collection = many.createEmpty(parentBean);
BeanCollectionAdd add = many.getBeanCollectionAdd(collection, null);
do {
@@ -51,4 +75,15 @@ public class BeanPropertyAssocManyJsonHelp {
many.setValue(parentBean, collection);
}
/**
* Read a Transient property using Jackson ObjectMapper.
*/
private void jsonReadTransientUsingObjectMapper(ReadJson readJson, EntityBean parentBean) throws IOException {
if (jsonTransient == null) {
throw new IllegalStateException("Jackson ObjectMapper is required to read this Transient property "+many.getFullBeanName());
}
jsonTransient.jsonReadUsingObjectMapper(many, readJson, parentBean);
}
}
@@ -0,0 +1,44 @@
package com.avaje.ebeaninternal.server.deploy;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebeaninternal.server.text.json.ReadJson;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import java.io.IOException;
import java.util.LinkedHashMap;
/**
* Helper used to read transient many properties using Jackson ObjectMapper.
*/
public class BeanPropertyAssocManyJsonTransient {
/**
* Use Jackson ObjectMapper to read the transient 'many' property.
*/
public void jsonReadUsingObjectMapper(BeanPropertyAssocMany<?> many, ReadJson readJson, EntityBean parentBean) throws IOException {
ObjectMapper mapper = readJson.getObjectMapper();
ManyType manyType = many.getManyType();
Object value;
if (manyType.isMap()) {
// read map using Jackson object mapper with unknown key type
TypeFactory typeFactory = mapper.getTypeFactory();
JavaType target = typeFactory.constructType(many.getTargetType());
MapType jacksonType = typeFactory.constructMapType(LinkedHashMap.class, typeFactory.unknownType(), target);
value = mapper.readValue(readJson.getParser(), jacksonType);
} else {
// read list or set using Jackson object mapper
CollectionType jacksonType = mapper.getTypeFactory().constructCollectionType(manyType.getCollectionType(), many.getTargetType());
value = mapper.readValue(readJson.getParser(), jacksonType);
}
many.setValue(parentBean, value);
}
}
@@ -2,58 +2,76 @@ package com.avaje.ebeaninternal.server.deploy;
import com.avaje.ebeaninternal.api.SpiQuery;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* Represents the type of a OneToMany or ManyToMany property.
*/
public class ManyType {
public static final ManyType JAVA_LIST = new ManyType(Underlying.LIST);
public static final ManyType JAVA_SET = new ManyType(Underlying.SET);
public static final ManyType JAVA_MAP = new ManyType(Underlying.MAP);
public enum Underlying {
LIST,
SET,
MAP
}
private final SpiQuery.Type queryType;
private final Underlying underlying;
public static final ManyType JAVA_LIST = new ManyType(Underlying.LIST);
public static final ManyType JAVA_SET = new ManyType(Underlying.SET);
public static final ManyType JAVA_MAP = new ManyType(Underlying.MAP);
public ManyType(Underlying underlying) {
this.underlying = underlying;
switch (underlying) {
case LIST:
queryType = SpiQuery.Type.LIST;
break;
case SET:
queryType = SpiQuery.Type.SET;
break;
public enum Underlying {
default:
queryType = SpiQuery.Type.MAP;
break;
}
}
LIST(List.class),
SET(Set.class),
MAP(null);
public boolean isMap() {
return Underlying.MAP.equals(underlying);
Class<? extends Collection> type;
Underlying(Class<? extends Collection> type) {
this.type = type;
}
/**
* Return the matching Query type.
*/
public SpiQuery.Type getQueryType() {
return queryType;
}
private final SpiQuery.Type queryType;
private final Underlying underlying;
public ManyType(Underlying underlying) {
this.underlying = underlying;
switch (underlying) {
case LIST:
queryType = SpiQuery.Type.LIST;
break;
case SET:
queryType = SpiQuery.Type.SET;
break;
default:
queryType = SpiQuery.Type.MAP;
break;
}
/**
* Return the underlying type.
*/
public Underlying getUnderlying() {
return underlying;
}
}
public boolean isMap() {
return Underlying.MAP.equals(underlying);
}
/**
* Return the matching Query type.
*/
public SpiQuery.Type getQueryType() {
return queryType;
}
/**
* Return the underlying type.
*/
public Underlying getUnderlying() {
return underlying;
}
/**
* Returns List.class or Set.class and null for Map.
* Not intended to be called for maps.
*/
public Class<? extends Collection> getCollectionType() {
return underlying.type;
}
}
@@ -101,8 +101,8 @@ public class DeployBeanProperty {
private boolean localEncrypted;
private boolean exposeSerialize = true;
private boolean exposeDeserialize = true;
private boolean jsonSerialize = true;
private boolean jsonDeserialize = true;
private boolean dbEncrypted;
private DbEncryptFunction dbEncryptFunction;
@@ -292,20 +292,20 @@ public class DeployBeanProperty {
return dbLength;
}
public boolean isExposeSerialize() {
return exposeSerialize;
public boolean isJsonSerialize() {
return jsonSerialize;
}
public void setExposeSerialize(boolean exposeSerialize) {
this.exposeSerialize = exposeSerialize;
public void setJsonSerialize(boolean jsonSerialize) {
this.jsonSerialize = jsonSerialize;
}
public boolean isExposeDeserialize() {
return exposeDeserialize;
public boolean isJsonDeserialize() {
return jsonDeserialize;
}
public void setExposeDeserialize(boolean exposeDeserialize) {
this.exposeDeserialize = exposeDeserialize;
public void setJsonDeserialize(boolean jsonDeserialize) {
this.jsonDeserialize = jsonDeserialize;
}
/**
@@ -1,11 +1,23 @@
package com.avaje.ebeaninternal.server.deploy.parse;
import com.avaje.ebean.annotation.*;
import com.avaje.ebean.annotation.ColumnHstore;
import com.avaje.ebean.annotation.CreatedTimestamp;
import com.avaje.ebean.annotation.DbHstore;
import com.avaje.ebean.annotation.DbJson;
import com.avaje.ebean.annotation.DbJsonB;
import com.avaje.ebean.annotation.EmbeddedColumns;
import com.avaje.ebean.annotation.Encrypted;
import com.avaje.ebean.annotation.Expose;
import com.avaje.ebean.annotation.Formula;
import com.avaje.ebean.annotation.Index;
import com.avaje.ebean.annotation.JsonIgnore;
import com.avaje.ebean.annotation.UpdatedTimestamp;
import com.avaje.ebean.config.EncryptDeploy;
import com.avaje.ebean.config.EncryptDeploy.Mode;
import com.avaje.ebean.config.dbplatform.DbEncrypt;
import com.avaje.ebean.config.dbplatform.DbEncryptFunction;
import com.avaje.ebean.config.dbplatform.IdType;
import com.avaje.ebeaninternal.api.ClassUtil;
import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedPropertyFactory;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssoc;
@@ -13,7 +25,12 @@ import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyCompound;
import com.avaje.ebeaninternal.server.idgen.UuidIdGenerator;
import com.avaje.ebeaninternal.server.lib.util.StringHelper;
import com.avaje.ebeaninternal.server.type.*;
import com.avaje.ebeaninternal.server.type.CtCompoundType;
import com.avaje.ebeaninternal.server.type.DataEncryptSupport;
import com.avaje.ebeaninternal.server.type.ScalarType;
import com.avaje.ebeaninternal.server.type.ScalarTypeBytesBase;
import com.avaje.ebeaninternal.server.type.ScalarTypeBytesEncrypted;
import com.avaje.ebeaninternal.server.type.ScalarTypeEncryptedWrapper;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@@ -27,6 +44,11 @@ import java.util.UUID;
*/
public class AnnotationFields extends AnnotationParser {
/**
* If present read Jackson JsonIgnore.
*/
private boolean jacksonAnnotationsPresent = ClassUtil.isJacksonAnnotationsPresent();
/**
* By default we lazy load Lob properties.
*/
@@ -101,10 +123,25 @@ public class AnnotationFields extends AnnotationParser {
if (column != null) {
readColumn(column, prop);
}
if (jacksonAnnotationsPresent) {
com.fasterxml.jackson.annotation.JsonIgnore jsonIgnore = get(prop, com.fasterxml.jackson.annotation.JsonIgnore.class);
if (jsonIgnore != null) {
prop.setJsonSerialize(!jsonIgnore.value());
prop.setJsonDeserialize(!jsonIgnore.value());
}
}
Expose expose = get(prop, Expose.class);
if (expose != null) {
prop.setExposeSerialize(expose.serialize());
prop.setExposeDeserialize(expose.deserialize());
prop.setJsonSerialize(expose.serialize());
prop.setJsonDeserialize(expose.deserialize());
}
JsonIgnore jsonIgnore = get(prop, JsonIgnore.class);
if (jsonIgnore != null) {
prop.setJsonSerialize(jsonIgnore.serialize());
prop.setJsonDeserialize(jsonIgnore.deserialize());
}
if (prop.getDbColumn() == null) {
@@ -259,7 +259,8 @@ public class DeployCreateProperties {
}
if (isTransientField(field)) {
return null;
// return with no ScalarType (still support JSON features)
return new DeployBeanProperty(desc, propertyType, null, null);
}
try {
CheckImmutableResponse checkImmutable = typeManager.checkImmutable(propertyType);
@@ -173,19 +173,27 @@ public class DeployUtil {
// Note that Temporal types already have dbType
// set via annotations
Class<?> propType = property.getPropertyType();
ScalarType<?> scalarType = typeManager.getScalarType(propType, property.getDbType());
if (scalarType != null) {
return scalarType;
}
try {
ScalarType<?> scalarType = typeManager.getScalarType(propType, property.getDbType());
if (scalarType != null) {
return scalarType;
}
String msg = property.getFullBeanName() + " has no ScalarType - type[" + propType.getName() + "]";
if (!property.isTransient()) {
throw new PersistenceException(msg);
String msg = property.getFullBeanName() + " has no ScalarType - type[" + propType.getName() + "]";
if (!property.isTransient()) {
throw new PersistenceException(msg);
} else {
// this is ok...
logger.trace("... transient property " + msg);
return null;
} else {
// this is ok...
logger.trace("... transient property " + msg);
return null;
}
} catch (IllegalArgumentException e) {
if (property.isTransient()) {
// expected for transient properties with unknown/non-mapped types
return null;
}
throw e;
}
}
@@ -24,9 +24,12 @@ public class DJsonContext implements JsonContext {
private final JsonFactory jsonFactory;
private final Object defaultObjectMapper;
public DJsonContext(SpiEbeanServer server, JsonFactory jsonFactory) {
this.server = server;
this.jsonFactory = (jsonFactory != null) ? jsonFactory : new JsonFactory();
this.defaultObjectMapper = this.server.getServerConfig().getObjectMapper();
}
public boolean isSupportedType(Type genericType) {
@@ -72,7 +75,7 @@ public class DJsonContext implements JsonContext {
public <T> T toBean(Class<T> cls, JsonParser parser, JsonReadOptions options) throws JsonIOException {
ReadJson readJson = new ReadJson(parser, options);
ReadJson readJson = new ReadJson(parser, options, determineObjectMapper(options));
try {
BeanDescriptor<T> d = getDescriptor(cls);
return d.jsonRead(readJson, null);
@@ -104,7 +107,7 @@ public class DJsonContext implements JsonContext {
public <T> List<T> toList(Class<T> cls, JsonParser src, JsonReadOptions options) throws JsonIOException {
ReadJson readJson = new ReadJson(src, options);
ReadJson readJson = new ReadJson(src, options, determineObjectMapper(options));
try {
BeanDescriptor<T> d = getDescriptor(cls);
@@ -271,7 +274,7 @@ public class DJsonContext implements JsonContext {
private WriteJson createWriteJson(JsonGenerator gen, JsonWriteOptions options) {
PathProperties pathProps = (options == null) ? null : options.getPathProperties();
return new WriteJson(server, gen, pathProps);
return new WriteJson(server, gen, pathProps, determineObjectMapper(options));
}
private <T> void toJsonFromCollection(Collection<T> collection, String key, JsonGenerator gen, JsonWriteOptions options) throws IOException {
@@ -320,11 +323,36 @@ public class DJsonContext implements JsonContext {
gen.writeEndObject();
}
private <T> BeanDescriptor<T> getDescriptor(Class<T> cls) {
BeanDescriptor<T> d = server.getBeanDescriptor(cls);
/**
* Return the BeanDescriptor for the given bean type.
*/
private <T> BeanDescriptor<T> getDescriptor(Class<T> beanType) {
BeanDescriptor<T> d = server.getBeanDescriptor(beanType);
if (d == null) {
throw new RuntimeException("No BeanDescriptor found for " + cls);
throw new RuntimeException("No BeanDescriptor found for " + beanType);
}
return d;
}
/**
* Determine the object mapper to use for a JSON read request.
*/
private Object determineObjectMapper(JsonReadOptions options) {
if (options == null) {
return defaultObjectMapper;
}
Object mapper = options.getObjectMapper();
return (mapper != null) ? mapper : defaultObjectMapper;
}
/**
* Determine the object mapper to use for a JSON write request.
*/
private Object determineObjectMapper(JsonWriteOptions options) {
if (options == null) {
return defaultObjectMapper;
}
Object mapper = options.getObjectMapper();
return (mapper != null) ? mapper : defaultObjectMapper;
}
}
@@ -4,6 +4,7 @@ import com.avaje.ebean.text.json.JsonReadBeanVisitor;
import com.avaje.ebean.text.json.JsonReadOptions;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Map;
@@ -28,17 +29,33 @@ public class ReadJson {
*/
final Map<String, JsonReadBeanVisitor<?>> visitorMap;
final Object objectMapper;
/**
* Construct with parser and readOptions.
*/
public ReadJson(JsonParser parser, JsonReadOptions readOptions) {
public ReadJson(JsonParser parser, JsonReadOptions readOptions, Object objectMapper) {
this.parser = parser;
this.objectMapper = objectMapper;
// only create visitorMap, pathStack if needed ...
this.visitorMap = (readOptions == null) ? null : readOptions.getVisitorMap();
this.pathStack = (visitorMap == null) ? null : new PathStack();
}
/**
* Return the objectMapper used for this request.
*/
public ObjectMapper getObjectMapper() {
if (objectMapper == null) {
throw new IllegalStateException(
"Jackson ObjectMapper required but has not set. The ObjectMapper can be set on"
+" either the ServerConfig or on JsonReadOptions.");
}
return (ObjectMapper)objectMapper;
}
/**
* Return the JsonParser.
*/
@@ -84,4 +101,13 @@ public class ReadJson {
}
}
}
/**
* Read the property value using Jackson ObjectMapper.
* <p/>
* Typically this is used to read Transient properties where the type is unknown to Ebean.
*/
public Object readValueUsingObjectMapper(Class<?> propertyType) throws IOException {
return getObjectMapper().readValue(parser, propertyType);
}
}
@@ -7,6 +7,7 @@ import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.util.ArrayStack;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Collection;
@@ -24,10 +25,13 @@ public class WriteJson {
private final ArrayStack<Object> parentBeans = new ArrayStack<Object>();
public WriteJson(SpiEbeanServer server, JsonGenerator generator, PathProperties pathProperties){
private final Object objectMapper;
public WriteJson(SpiEbeanServer server, JsonGenerator generator, PathProperties pathProperties, Object objectMapper){
this.server = server;
this.generator = generator;
this.pathProperties = pathProperties;
this.objectMapper = objectMapper;
}
public JsonGenerator gen() {
@@ -73,6 +77,20 @@ public class WriteJson {
return new WriteBean(desc, explicitAllProps, currentIncludeProps, bean);
}
public void writeValueUsingObjectMapper(String name, Object value) throws IOException {
generator.writeFieldName(name);
objectMapper().writeValue(generator, value);
}
private ObjectMapper objectMapper() {
if (objectMapper == null) {
throw new IllegalStateException(
"Jackson ObjectMapper required but not set. Expected to be set on either"
+" serverConfig");
}
return (ObjectMapper)objectMapper;
}
public static class WriteBean {
final boolean explicitAllProps;
@@ -159,7 +159,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
this.typeMap = new ConcurrentHashMap<Class<?>, ScalarType<?>>();
this.nativeMap = new ConcurrentHashMap<Integer, ScalarType<?>>();
this.objectMapperPresent = ClassUtil.isPresent("com.fasterxml.jackson.databind.ObjectMapper", this.getClass());
this.objectMapperPresent = ClassUtil.isJacksonObjectMapperPresent();
this.extraTypeFactory = new DefaultTypeFactory(config);
@@ -384,7 +384,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
return (ScalarType<T>) extraTypeFactory.createCalendar(jsonDateTime, jdbcType);
}
throw new RuntimeException("Unmatched ScalarType for " + type + " jdbcType:" + jdbcType);
throw new IllegalArgumentException("Unmatched ScalarType for " + type + " jdbcType:" + jdbcType);
}
/**