From 11b134d71170f4000615db141b8ce6b01fe82071 Mon Sep 17 00:00:00 2001 From: Ruslan Sibgatullin Date: Fri, 2 Mar 2018 00:37:07 +0300 Subject: [PATCH] XStreamExcludeEmpty annotation added --- .../hibernate/mapper/HibernateMapper.java | 5 ++ .../annotations/XStreamExcludeEmpty.java | 19 ++++++ .../xstream/core/TreeMarshaller.java | 2 +- .../xstream/core/util/EmptyFieldChecker.java | 66 +++++++++++++++++++ .../xstream/mapper/AnnotationMapper.java | 33 +++++++++- .../xstream/mapper/ArrayMapper.java | 5 ++ .../xstream/mapper/CGLIBMapper.java | 5 ++ .../xstream/mapper/ClassAliasingMapper.java | 5 ++ .../mapper/DefaultImplementationsMapper.java | 5 ++ .../xstream/mapper/DefaultMapper.java | 5 ++ .../xstream/mapper/DynamicProxyMapper.java | 5 ++ .../thoughtworks/xstream/mapper/Mapper.java | 5 ++ .../xstream/mapper/MapperWrapper.java | 5 ++ .../acceptance/CustomMapperTest.java | 6 ++ .../annotations/ExcludeEmptyTest.java | 56 ++++++++++++++++ .../collections/CollectionConverterTest.java | 40 +++++++++++ 16 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 xstream/src/java/com/thoughtworks/xstream/annotations/XStreamExcludeEmpty.java create mode 100644 xstream/src/java/com/thoughtworks/xstream/core/util/EmptyFieldChecker.java create mode 100644 xstream/src/test/com/thoughtworks/acceptance/annotations/ExcludeEmptyTest.java create mode 100644 xstream/src/test/com/thoughtworks/xstream/converters/collections/CollectionConverterTest.java diff --git a/xstream-hibernate/src/java/com/thoughtworks/xstream/hibernate/mapper/HibernateMapper.java b/xstream-hibernate/src/java/com/thoughtworks/xstream/hibernate/mapper/HibernateMapper.java index d5d25d10d..6455f8c8d 100644 --- a/xstream-hibernate/src/java/com/thoughtworks/xstream/hibernate/mapper/HibernateMapper.java +++ b/xstream-hibernate/src/java/com/thoughtworks/xstream/hibernate/mapper/HibernateMapper.java @@ -75,4 +75,9 @@ public String serializedClass(final Class clazz) { } return super.serializedClass(clazz); } + + @Override + public String serializedClass(Object item) { + return serializedClass(item.getClass()); + } } diff --git a/xstream/src/java/com/thoughtworks/xstream/annotations/XStreamExcludeEmpty.java b/xstream/src/java/com/thoughtworks/xstream/annotations/XStreamExcludeEmpty.java new file mode 100644 index 000000000..c80b2274c --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/annotations/XStreamExcludeEmpty.java @@ -0,0 +1,19 @@ +package com.thoughtworks.xstream.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation for marking {@link String}, all {@link java.util.Collection} and {@link java.util.Map} types + * as excluded if the object value is null or the object is empty. + * For emptiness check corresponded method from each of supported types is used + * + * @author Ruslan Sibgatullin + * @since 1.5.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface XStreamExcludeEmpty { +} diff --git a/xstream/src/java/com/thoughtworks/xstream/core/TreeMarshaller.java b/xstream/src/java/com/thoughtworks/xstream/core/TreeMarshaller.java index 682796db5..768ce92ee 100644 --- a/xstream/src/java/com/thoughtworks/xstream/core/TreeMarshaller.java +++ b/xstream/src/java/com/thoughtworks/xstream/core/TreeMarshaller.java @@ -77,7 +77,7 @@ public void start(final Object item, final DataHolder dataHolder) { writer.startNode(mapper.serializedClass(null)); writer.endNode(); } else { - writer.startNode(mapper.serializedClass(item.getClass()), item.getClass()); + writer.startNode(mapper.serializedClass(item), item.getClass()); convertAnother(item); writer.endNode(); } diff --git a/xstream/src/java/com/thoughtworks/xstream/core/util/EmptyFieldChecker.java b/xstream/src/java/com/thoughtworks/xstream/core/util/EmptyFieldChecker.java new file mode 100644 index 000000000..1008f0350 --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/core/util/EmptyFieldChecker.java @@ -0,0 +1,66 @@ +package com.thoughtworks.xstream.core.util; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import com.thoughtworks.xstream.InitializationException; +import com.thoughtworks.xstream.mapper.ElementIgnoringMapper; + + +/** + * Utility functions for {@link com.thoughtworks.xstream.annotations.XStreamExcludeEmpty} annotation + * Primary usage is to check if corresponded field should be omitted + * + * @author Ruslan Sibgatullin + */ +public final class EmptyFieldChecker { + + private static final transient Map, CheckIfEmpty> EMPTY_CHECKER_MAP = new HashMap, CheckIfEmpty>(){{ + put(String.class, new CheckIfEmpty() { + @Override + public boolean isEmpty(String value) { + return value == null || value.isEmpty(); + } + }); + put(Collection.class, new CheckIfEmpty() { + @Override + public boolean isEmpty(Collection value) { + return value == null || value.isEmpty(); + } + }); + put(Map.class, new CheckIfEmpty() { + @Override + public boolean isEmpty(Map value) { + return value == null || value.isEmpty(); + } + }); + }}; + + public static void checkAndOmitIfEmpty(ElementIgnoringMapper elementIgnoringMapper, final Field field, Object item) { + for (Class assignableClass : EMPTY_CHECKER_MAP.keySet()) { + omitIfEmpty(elementIgnoringMapper, field, item, assignableClass); + } + } + + private static void omitIfEmpty(ElementIgnoringMapper elementIgnoringMapper, Field field, Object item, Class assignableClass) { + if (assignableClass.isAssignableFrom(field.getType())) { + try { + field.setAccessible(true); + final Object value = field.get(item); + + //noinspection unchecked + if (EMPTY_CHECKER_MAP.get(assignableClass).isEmpty(value)) { + elementIgnoringMapper.omitField(field.getDeclaringClass(), field.getName()); + } + } catch (IllegalAccessException e) { + throw new InitializationException("Field " + field.getName() + " cannot be accessed", e); + } + } + } + + private interface CheckIfEmpty { + boolean isEmpty(T value); + } +} diff --git a/xstream/src/java/com/thoughtworks/xstream/mapper/AnnotationMapper.java b/xstream/src/java/com/thoughtworks/xstream/mapper/AnnotationMapper.java index 1eff98895..327c6951f 100644 --- a/xstream/src/java/com/thoughtworks/xstream/mapper/AnnotationMapper.java +++ b/xstream/src/java/com/thoughtworks/xstream/mapper/AnnotationMapper.java @@ -37,6 +37,7 @@ import com.thoughtworks.xstream.annotations.XStreamAsAttribute; import com.thoughtworks.xstream.annotations.XStreamConverter; import com.thoughtworks.xstream.annotations.XStreamConverters; +import com.thoughtworks.xstream.annotations.XStreamExcludeEmpty; import com.thoughtworks.xstream.annotations.XStreamImplicit; import com.thoughtworks.xstream.annotations.XStreamInclude; import com.thoughtworks.xstream.annotations.XStreamOmitField; @@ -50,6 +51,7 @@ import com.thoughtworks.xstream.core.ClassLoaderReference; import com.thoughtworks.xstream.core.JVM; import com.thoughtworks.xstream.core.util.DependencyInjectionFactory; +import com.thoughtworks.xstream.core.util.EmptyFieldChecker; import com.thoughtworks.xstream.core.util.TypedNull; @@ -127,6 +129,14 @@ public String serializedClass(final Class type) { return super.serializedClass(type); } + @Override + public String serializedClass(final Object item) { + if (!locked) { + processAnnotation(item.getClass(), item); + } + return super.serializedClass(item.getClass()); + } + @Override public Class defaultImplementationOf(final Class type) { if (!locked) { @@ -166,17 +176,25 @@ public void processAnnotations(final Class... initialTypes) { processTypes(types); } - private void processAnnotation(final Class initialType) { + private void processAnnotation(final Class initialType, Object item) { if (initialType == null) { return; } final Set> types = new UnprocessedTypesSet(); types.add(initialType); - processTypes(types); + processTypes(types, item); + } + + private void processAnnotation(final Class initialType) { + processAnnotation(initialType, null); } private void processTypes(final Set> types) { + processTypes(types, null); + } + + private void processTypes(final Set> types, Object item) { while (!types.isEmpty()) { final Iterator> iter = types.iterator(); final Class type = iter.next(); @@ -219,6 +237,7 @@ private void processTypes(final Set> types) { processImplicitAnnotation(field); processOmitFieldAnnotation(field); processLocalConverterAnnotation(field); + processExcludeEmptyAnnotation(field, item); } } finally { annotatedTypes.add(type); @@ -413,6 +432,16 @@ private void processLocalConverterAnnotation(final Field field) { } } + private void processExcludeEmptyAnnotation(final Field field, Object item) { + final XStreamExcludeEmpty annotation = field.getAnnotation(XStreamExcludeEmpty.class); + if (annotation != null && item != null) { + if (elementIgnoringMapper == null) { + throw new InitializationException("No " + ElementIgnoringMapper.class.getName() + " available"); + } + EmptyFieldChecker.checkAndOmitIfEmpty(elementIgnoringMapper, field, item); + } + } + private Converter cacheConverter(final XStreamConverter annotation, final Class targetType) { Converter result = null; final Object[] args; diff --git a/xstream/src/java/com/thoughtworks/xstream/mapper/ArrayMapper.java b/xstream/src/java/com/thoughtworks/xstream/mapper/ArrayMapper.java index d66851c9d..fd0f3693a 100644 --- a/xstream/src/java/com/thoughtworks/xstream/mapper/ArrayMapper.java +++ b/xstream/src/java/com/thoughtworks/xstream/mapper/ArrayMapper.java @@ -53,6 +53,11 @@ public String serializedClass(Class type) { } } + @Override + public String serializedClass(Object item) { + return serializedClass(item.getClass()); + } + @Override public Class realClass(String elementName) { int dimensions = 0; diff --git a/xstream/src/java/com/thoughtworks/xstream/mapper/CGLIBMapper.java b/xstream/src/java/com/thoughtworks/xstream/mapper/CGLIBMapper.java index 78f89e6e0..6d03c8405 100644 --- a/xstream/src/java/com/thoughtworks/xstream/mapper/CGLIBMapper.java +++ b/xstream/src/java/com/thoughtworks/xstream/mapper/CGLIBMapper.java @@ -51,6 +51,11 @@ public String serializedClass(final Class type) { && Enhancer.isEnhanced(type) ? alias : serializedName; } + @Override + public String serializedClass(Object item) { + return serializedClass(item.getClass()); + } + @Override public Class realClass(final String elementName) { return elementName.equals(alias) ? Marker.class : super.realClass(elementName); diff --git a/xstream/src/java/com/thoughtworks/xstream/mapper/ClassAliasingMapper.java b/xstream/src/java/com/thoughtworks/xstream/mapper/ClassAliasingMapper.java index 4559c3d55..aad953aca 100644 --- a/xstream/src/java/com/thoughtworks/xstream/mapper/ClassAliasingMapper.java +++ b/xstream/src/java/com/thoughtworks/xstream/mapper/ClassAliasingMapper.java @@ -58,6 +58,11 @@ public String serializedClass(final Class type) { } } + @Override + public String serializedClass(Object item) { + return serializedClass(item.getClass()); + } + @Override public Class realClass(String elementName) { final String mappedName = nameToType.get(elementName); diff --git a/xstream/src/java/com/thoughtworks/xstream/mapper/DefaultImplementationsMapper.java b/xstream/src/java/com/thoughtworks/xstream/mapper/DefaultImplementationsMapper.java index 15e3c2c85..e42564894 100644 --- a/xstream/src/java/com/thoughtworks/xstream/mapper/DefaultImplementationsMapper.java +++ b/xstream/src/java/com/thoughtworks/xstream/mapper/DefaultImplementationsMapper.java @@ -62,6 +62,11 @@ public String serializedClass(final Class type) { return baseType == null ? super.serializedClass(type) : super.serializedClass(baseType); } + @Override + public String serializedClass(Object item) { + return serializedClass(item.getClass()); + } + @Override public Class defaultImplementationOf(final Class type) { if (typeToImpl.containsKey(type)) { diff --git a/xstream/src/java/com/thoughtworks/xstream/mapper/DefaultMapper.java b/xstream/src/java/com/thoughtworks/xstream/mapper/DefaultMapper.java index f50a7268f..c1c72f9f3 100644 --- a/xstream/src/java/com/thoughtworks/xstream/mapper/DefaultMapper.java +++ b/xstream/src/java/com/thoughtworks/xstream/mapper/DefaultMapper.java @@ -63,6 +63,11 @@ public String serializedClass(final Class type) { return type.getName(); } + @Override + public String serializedClass(Object item) { + return serializedClass(item.getClass()); + } + @Override public Class realClass(final String elementName) { final Class resultingClass = Primitives.primitiveType(elementName); diff --git a/xstream/src/java/com/thoughtworks/xstream/mapper/DynamicProxyMapper.java b/xstream/src/java/com/thoughtworks/xstream/mapper/DynamicProxyMapper.java index 4e93128a5..a84a2274a 100644 --- a/xstream/src/java/com/thoughtworks/xstream/mapper/DynamicProxyMapper.java +++ b/xstream/src/java/com/thoughtworks/xstream/mapper/DynamicProxyMapper.java @@ -52,6 +52,11 @@ public String serializedClass(final Class type) { } } + @Override + public String serializedClass(Object item) { + return serializedClass(item.getClass()); + } + @Override public Class realClass(final String elementName) { if (elementName.equals(alias)) { diff --git a/xstream/src/java/com/thoughtworks/xstream/mapper/Mapper.java b/xstream/src/java/com/thoughtworks/xstream/mapper/Mapper.java index 5a9df995b..ca310b10a 100644 --- a/xstream/src/java/com/thoughtworks/xstream/mapper/Mapper.java +++ b/xstream/src/java/com/thoughtworks/xstream/mapper/Mapper.java @@ -26,6 +26,11 @@ class Null {} */ String serializedClass(Class type); + /** + * How an item and underlying class should be represented in its serialized form. + */ + String serializedClass(Object item); + /** * How a serialized class representation should be mapped back to a real class. */ diff --git a/xstream/src/java/com/thoughtworks/xstream/mapper/MapperWrapper.java b/xstream/src/java/com/thoughtworks/xstream/mapper/MapperWrapper.java index b83ff9913..97430d802 100644 --- a/xstream/src/java/com/thoughtworks/xstream/mapper/MapperWrapper.java +++ b/xstream/src/java/com/thoughtworks/xstream/mapper/MapperWrapper.java @@ -121,6 +121,11 @@ public String serializedClass(final Class type) { return serializedClassMapper.serializedClass(type); } + @Override + public String serializedClass(Object item) { + return serializedClassMapper.serializedClass(item); + } + @Override public Class realClass(final String elementName) { return realClassMapper.realClass(elementName); diff --git a/xstream/src/test/com/thoughtworks/acceptance/CustomMapperTest.java b/xstream/src/test/com/thoughtworks/acceptance/CustomMapperTest.java index 840be612f..ef75ff63b 100644 --- a/xstream/src/test/com/thoughtworks/acceptance/CustomMapperTest.java +++ b/xstream/src/test/com/thoughtworks/acceptance/CustomMapperTest.java @@ -104,9 +104,15 @@ public PackageStrippingMapper(Mapper wrapped) { super(wrapped); } + @Override public String serializedClass(Class type) { return type.getName().replaceFirst(".*\\.", ""); } + + @Override + public String serializedClass(Object item) { + return serializedClass(item.getClass()); + } } public void testStripsPackagesUponDeserialization() { diff --git a/xstream/src/test/com/thoughtworks/acceptance/annotations/ExcludeEmptyTest.java b/xstream/src/test/com/thoughtworks/acceptance/annotations/ExcludeEmptyTest.java new file mode 100644 index 000000000..55d074b65 --- /dev/null +++ b/xstream/src/test/com/thoughtworks/acceptance/annotations/ExcludeEmptyTest.java @@ -0,0 +1,56 @@ +package com.thoughtworks.acceptance.annotations; + +import com.thoughtworks.acceptance.AbstractAcceptanceTest; +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamExcludeEmpty; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Ruslan Sibgatullin + */ +public class ExcludeEmptyTest extends AbstractAcceptanceTest { + + @Override + protected XStream createXStream() { + XStream xstream = super.createXStream(); + xstream.autodetectAnnotations(true); + return xstream; + } + + public void testAnnotationForClassWithExcludeEmptyFields() { + String xml = "\n" + + " name\n" + + " 2\n" + + " \n" + + " one\n" + + " two\n" + + " \n" + + ""; + assertBothWays(new Person(), xml); + } + + @XStreamAlias("person") + private static class Person { + @XStreamExcludeEmpty + private String name = "name"; + @XStreamExcludeEmpty + private String emptyField = ""; + @XStreamExcludeEmpty + private int ignoredField = 2; + + @XStreamExcludeEmpty + private List addresses = new ArrayList<>(Arrays.asList("one", "two")); + @XStreamExcludeEmpty + private List emptyList = new ArrayList<>(); + + @XStreamExcludeEmpty + private Map emptyMap = new HashMap<>(); + + } +} diff --git a/xstream/src/test/com/thoughtworks/xstream/converters/collections/CollectionConverterTest.java b/xstream/src/test/com/thoughtworks/xstream/converters/collections/CollectionConverterTest.java new file mode 100644 index 000000000..83da0566c --- /dev/null +++ b/xstream/src/test/com/thoughtworks/xstream/converters/collections/CollectionConverterTest.java @@ -0,0 +1,40 @@ +package com.thoughtworks.xstream.converters.collections; + +import com.thoughtworks.acceptance.AbstractAcceptanceTest; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +/** + * @author Ruslan Sibgatullin + */ +public class CollectionConverterTest extends AbstractAcceptanceTest { + + public void testMarshallArrayListOfStrings() { + Collection input = new ArrayList<>(Arrays.asList("one", "two", "three")); + + String expected = "\n" + + " one\n" + + " two\n" + + " three\n" + + ""; + assertBothWays(input, expected); + } + + public void testMarshallEmptyList() { + Collection input = Collections.emptyList(); + + String expected = ""; + assertBothWays(input, expected); + } + + public void testMarshallEmptyArray() { + Collection input = new ArrayList<>(); + + String expected = ""; + assertBothWays(input, expected); + } + +}