| package org.codehaus.jackson.map.ser; |
| |
| import java.lang.reflect.Array; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| import java.util.Collection; |
| import java.util.Map; |
| |
| import org.codehaus.jackson.map.*; |
| import org.codehaus.jackson.map.annotate.JsonSerialize; |
| import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; |
| import org.codehaus.jackson.map.introspect.*; |
| import org.codehaus.jackson.map.util.*; |
| import org.codehaus.jackson.type.JavaType; |
| |
| /** |
| * Helper class for {@link BeanSerializerFactory} that is used to |
| * construct {@link BeanPropertyWriter} instances. Can be sub-classed |
| * to change behavior. |
| */ |
| public class PropertyBuilder |
| { |
| final protected SerializationConfig _config; |
| final protected BasicBeanDescription _beanDesc; |
| final protected JsonSerialize.Inclusion _outputProps; |
| |
| final protected AnnotationIntrospector _annotationIntrospector; |
| |
| /** |
| * If a property has serialization inclusion value of |
| * {@link Inclusion#ALWAYS}, we need to know the default |
| * value of the bean, to know if property value equals default |
| * one. |
| */ |
| protected Object _defaultBean; |
| |
| public PropertyBuilder(SerializationConfig config, BasicBeanDescription beanDesc) |
| { |
| _config = config; |
| _beanDesc = beanDesc; |
| _outputProps = beanDesc.findSerializationInclusion(config.getSerializationInclusion()); |
| _annotationIntrospector = _config.getAnnotationIntrospector(); |
| } |
| |
| /* |
| /********************************************************** |
| /* Public API |
| /********************************************************** |
| */ |
| |
| public Annotations getClassAnnotations() { |
| return _beanDesc.getClassAnnotations(); |
| } |
| |
| /** |
| * @param contentTypeSer Optional explicit type information serializer |
| * to use for contained values (only used for properties that are |
| * of container type) |
| */ |
| protected BeanPropertyWriter buildWriter(String name, JavaType declaredType, |
| JsonSerializer<Object> ser, |
| TypeSerializer typeSer, TypeSerializer contentTypeSer, |
| AnnotatedMember am, boolean defaultUseStaticTyping) |
| { |
| Field f; |
| Method m; |
| if (am instanceof AnnotatedField) { |
| m = null; |
| f = ((AnnotatedField) am).getAnnotated(); |
| } else { |
| m = ((AnnotatedMethod) am).getAnnotated(); |
| f = null; |
| } |
| |
| // do we have annotation that forces type to use (to declared type or its super type)? |
| JavaType serializationType = findSerializationType(am, defaultUseStaticTyping, declaredType); |
| |
| // Container types can have separate type serializers for content (value / element) type |
| if (contentTypeSer != null) { |
| /* 04-Feb-2010, tatu: Let's force static typing for collection, if there is |
| * type information for contents. Should work well (for JAXB case); can be |
| * revisited if this causes problems. |
| */ |
| if (serializationType == null) { |
| // serializationType = TypeFactory.type(am.getGenericType(), _beanDesc.getType()); |
| serializationType = declaredType; |
| } |
| JavaType ct = serializationType.getContentType(); |
| /* 03-Sep-2010, tatu: This is somehow related to [JACKSON-356], but I don't completely |
| * yet understand how pieces fit together. Still, better be explicit than rely on |
| * NPE to indicate an issue... |
| */ |
| if (ct == null) { |
| throw new IllegalStateException("Problem trying to create BeanPropertyWriter for property '" |
| +name+"' (of type "+_beanDesc.getType()+"); serialization type "+serializationType+" has no content"); |
| } |
| serializationType = serializationType.withContentTypeHandler(contentTypeSer); |
| ct = serializationType.getContentType(); |
| } |
| |
| Object valueToSuppress = null; |
| boolean suppressNulls = false; |
| |
| JsonSerialize.Inclusion methodProps = _annotationIntrospector.findSerializationInclusion(am, _outputProps); |
| |
| if (methodProps != null) { |
| switch (methodProps) { |
| case NON_DEFAULT: |
| valueToSuppress = getDefaultValue(name, m, f); |
| if (valueToSuppress == null) { |
| suppressNulls = true; |
| } else { |
| // [JACKSON-531]: Allow comparison of arrays too... |
| if (valueToSuppress.getClass().isArray()) { |
| valueToSuppress = Comparators.getArrayComparator(valueToSuppress); |
| } |
| } |
| break; |
| case NON_EMPTY: |
| // always suppress nulls |
| suppressNulls = true; |
| // but possibly also 'empty' values: |
| valueToSuppress = getEmptyValueChecker(name, declaredType); |
| break; |
| case NON_NULL: |
| suppressNulls = true; |
| // fall through |
| case ALWAYS: // default |
| // we may still want to suppress empty collections, as per [JACKSON-254]: |
| if (declaredType.isContainerType()) { |
| valueToSuppress = getContainerValueChecker(name, declaredType); |
| } |
| break; |
| } |
| } |
| |
| BeanPropertyWriter bpw = new BeanPropertyWriter(am, _beanDesc.getClassAnnotations(), name, declaredType, |
| ser, typeSer, serializationType, m, f, suppressNulls, valueToSuppress); |
| |
| // [JACKSON-132]: Unwrapping |
| Boolean unwrapped = _annotationIntrospector.shouldUnwrapProperty(am); |
| if (unwrapped != null && unwrapped.booleanValue()) { |
| bpw = bpw.unwrappingWriter(); |
| } |
| return bpw; |
| } |
| |
| /* |
| /********************************************************** |
| /* Helper methods; annotation access |
| /********************************************************** |
| */ |
| |
| /** |
| * Method that will try to determine statically defined type of property |
| * being serialized, based on annotations (for overrides), and alternatively |
| * declared type (if static typing for serialization is enabled). |
| * If neither can be used (no annotations, dynamic typing), returns null. |
| */ |
| protected JavaType findSerializationType(Annotated a, boolean useStaticTyping, JavaType declaredType) |
| { |
| // [JACKSON-120]: Check to see if serialization type is fixed |
| Class<?> serClass = _annotationIntrospector.findSerializationType(a); |
| if (serClass != null) { |
| // Must be a super type to be usable |
| Class<?> rawDeclared = declaredType.getRawClass(); |
| if (serClass.isAssignableFrom(rawDeclared)) { |
| declaredType = declaredType.widenBy(serClass); |
| } else { |
| /* 18-Nov-2010, tatu: Related to fixing [JACKSON-416], an issue with such |
| * check is that for deserialization more specific type makes sense; |
| * and for serialization more generic. But alas JAXB uses but a single |
| * annotation to do both... Hence, we must just discard type, as long as |
| * types are related |
| */ |
| if (!rawDeclared.isAssignableFrom(serClass)) { |
| throw new IllegalArgumentException("Illegal concrete-type annotation for method '"+a.getName()+"': class "+serClass.getName()+" not a super-type of (declared) class "+rawDeclared.getName()); |
| } |
| /* 03-Dec-2010, tatu: Actually, ugh, to resolve [JACKSON-415] may further relax this |
| * and actually accept subtypes too for serialization. Bit dangerous in theory |
| * but need to trust user here... |
| */ |
| declaredType = _config.constructSpecializedType(declaredType, serClass); |
| } |
| useStaticTyping = true; |
| } |
| |
| JavaType secondary = BeanSerializerFactory.modifySecondaryTypesByAnnotation(_config, a, declaredType); |
| if (secondary != declaredType) { |
| useStaticTyping = true; |
| declaredType = secondary; |
| } |
| |
| /* [JACKSON-114]: if using static typing, declared type is known |
| * to be the type... |
| */ |
| if (!useStaticTyping) { |
| JsonSerialize.Typing typing = _annotationIntrospector.findSerializationTyping(a); |
| if (typing != null) { |
| useStaticTyping = (typing == JsonSerialize.Typing.STATIC); |
| } |
| } |
| return useStaticTyping ? declaredType : null; |
| } |
| |
| /* |
| /********************************************************** |
| /* Helper methods for default value handling |
| /********************************************************** |
| */ |
| |
| protected Object getDefaultBean() |
| { |
| if (_defaultBean == null) { |
| /* If we can fix access rights, we should; otherwise non-public |
| * classes or default constructor will prevent instantiation |
| */ |
| _defaultBean = _beanDesc.instantiateBean(_config.isEnabled(SerializationConfig.Feature.CAN_OVERRIDE_ACCESS_MODIFIERS)); |
| if (_defaultBean == null) { |
| Class<?> cls = _beanDesc.getClassInfo().getAnnotated(); |
| throw new IllegalArgumentException("Class "+cls.getName()+" has no default constructor; can not instantiate default bean value to support 'properties=JsonSerialize.Inclusion.NON_DEFAULT' annotation"); |
| } |
| } |
| return _defaultBean; |
| } |
| |
| protected Object getDefaultValue(String name, Method m, Field f) |
| { |
| Object defaultBean = getDefaultBean(); |
| try { |
| if (m != null) { |
| return m.invoke(defaultBean); |
| } |
| return f.get(defaultBean); |
| } catch (Exception e) { |
| return _throwWrapped(e, name, defaultBean); |
| } |
| } |
| |
| /** |
| * Helper method called to see if we need a comparator Object to check if values |
| * of a container (Collection, array) property should be suppressed. |
| * This is usually |
| * |
| * @param propertyName Name of property to handle |
| * @param propertyType Declared type of values of the property to handle |
| * @return Object whose <code>equals()</code> method is called to check if given value |
| * is "empty Collection" value to suppress; or null if no such check should be done |
| * (declared type not Collection or array) |
| * |
| * @since 1.9 |
| */ |
| protected Object getContainerValueChecker(String propertyName, JavaType propertyType) |
| { |
| // currently we will only check for certain kinds of empty containers: |
| if (!_config.isEnabled(SerializationConfig.Feature.WRITE_EMPTY_JSON_ARRAYS)) { |
| if (propertyType.isArrayType()) { |
| return new EmptyArrayChecker(); |
| } |
| if (Collection.class.isAssignableFrom(propertyType.getRawClass())) { |
| return new EmptyCollectionChecker(); |
| } |
| } |
| return null; |
| } |
| |
| |
| /** |
| * Helper method called to see if we need a comparator Object to check if values |
| * of specified type are consider empty. |
| * If type has such concept, will build a comparator; otherwise return null, and |
| * in latter case, only null values are considered 'empty'. |
| * |
| * @param propertyName Name of property to handle |
| * @param propertyType Declared type of values of the property to handle |
| * @return Object whose <code>equals()</code> method is called to check if given value |
| * is "empty Collection" value to suppress; or null if no such check should be done |
| * (declared type not Collection or array) |
| * |
| * @since 1.9 |
| */ |
| protected Object getEmptyValueChecker(String propertyName, JavaType propertyType) |
| { |
| Class<?> rawType = propertyType.getRawClass(); |
| if (rawType == String.class) { |
| return new EmptyStringChecker(); |
| } |
| if (propertyType.isArrayType()) { |
| return new EmptyArrayChecker(); |
| } |
| if (Collection.class.isAssignableFrom(rawType)) { |
| return new EmptyCollectionChecker(); |
| } |
| if (Map.class.isAssignableFrom(rawType)) { |
| return new EmptyMapChecker(); |
| } |
| return null; |
| } |
| |
| /* |
| /********************************************************** |
| /* Helper methods for exception handling |
| /********************************************************** |
| */ |
| |
| protected Object _throwWrapped(Exception e, String propName, Object defaultBean) |
| { |
| Throwable t = e; |
| while (t.getCause() != null) { |
| t = t.getCause(); |
| } |
| if (t instanceof Error) throw (Error) t; |
| if (t instanceof RuntimeException) throw (RuntimeException) t; |
| throw new IllegalArgumentException("Failed to get property '"+propName+"' of default "+defaultBean.getClass().getName()+" instance"); |
| } |
| |
| /* |
| /********************************************************** |
| /* Helper classes |
| /********************************************************** |
| */ |
| |
| /** |
| * Helper object used to check if given Collection object is null or empty |
| * |
| * @since 1.9 |
| */ |
| public static class EmptyCollectionChecker |
| { |
| @Override public boolean equals(Object other) { |
| return (other == null) || ((Collection<?>) other).size() == 0; |
| } |
| } |
| |
| /** |
| * Helper object used to check if given Map object is null or empty |
| * |
| * @since 1.9 |
| */ |
| public static class EmptyMapChecker |
| { |
| @Override public boolean equals(Object other) { |
| return (other == null) || ((Map<?,?>) other).size() == 0; |
| } |
| } |
| |
| /** |
| * Helper object used to check if given array object is null or empty |
| * |
| * @since 1.9 |
| */ |
| public static class EmptyArrayChecker |
| { |
| @Override |
| public boolean equals(Object other) { |
| return (other == null) || Array.getLength(other) == 0; |
| } |
| } |
| |
| /** |
| * Helper object used to check if given String object is null or empty |
| * |
| * @since 1.9 |
| */ |
| public static class EmptyStringChecker |
| { |
| @Override |
| public boolean equals(Object other) { |
| return (other == null) || ((String) other).length() == 0; |
| } |
| } |
| } |