Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XStreamExcludeEmpty annotation added #109

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,9 @@ public String serializedClass(final Class<?> clazz) {
}
return super.serializedClass(clazz);
}

@Override
public String serializedClass(Object item) {
return serializedClass(item.getClass());
}
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Class<?>, CheckIfEmpty> EMPTY_CHECKER_MAP = new HashMap<Class<?>, CheckIfEmpty>(){{
put(String.class, new CheckIfEmpty<String>() {
@Override
public boolean isEmpty(String value) {
return value == null || value.isEmpty();
}
});
put(Collection.class, new CheckIfEmpty<Collection>() {
@Override
public boolean isEmpty(Collection value) {
return value == null || value.isEmpty();
}
});
put(Map.class, new CheckIfEmpty<Map>() {
@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<T> {
boolean isEmpty(T value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;


Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<Class<?>> 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<Class<?>> types) {
processTypes(types, null);
}

private void processTypes(final Set<Class<?>> types, Object item) {
while (!types.isEmpty()) {
final Iterator<Class<?>> iter = types.iterator();
final Class<?> type = iter.next();
Expand Down Expand Up @@ -219,6 +237,7 @@ private void processTypes(final Set<Class<?>> types) {
processImplicitAnnotation(field);
processOmitFieldAnnotation(field);
processLocalConverterAnnotation(field);
processExcludeEmptyAnnotation(field, item);
}
} finally {
annotatedTypes.add(type);
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
5 changes: 5 additions & 0 deletions xstream/src/java/com/thoughtworks/xstream/mapper/Mapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

Expand Down Expand Up @@ -51,7 +52,7 @@ public void testInBWCanBeMarshalled() throws IOException {
final String xml = xstream.toXML(image);

final ByteArrayOutputStream baosSerialized = new ByteArrayOutputStream();
ImageIO.write(xstream.fromXML(xml), "tiff", baosSerialized);
ImageIO.write(xstream.<RenderedImage>fromXML(xml), "tiff", baosSerialized);

assertArrayEquals(baosOriginal.toByteArray(), baosSerialized.toByteArray());
}
Expand All @@ -71,7 +72,7 @@ public void testInRGBACanBeMarshalled() throws IOException {
final String xml = xstream.toXML(image);

final ByteArrayOutputStream baosSerialized = new ByteArrayOutputStream();
ImageIO.write(xstream.fromXML(xml), "png", baosSerialized);
ImageIO.write(xstream.<RenderedImage>fromXML(xml), "png", baosSerialized);

assertArrayEquals(baosOriginal.toByteArray(), baosSerialized.toByteArray());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ public PackageStrippingMapper(final Mapper wrapped) {
public String serializedClass(final Class<?> type) {
return type.getName().replaceFirst(".*\\.", "");
}

@Override
public String serializedClass(Object item) {
return serializedClass(item.getClass());
}
}

public void testStripsPackagesUponDeserialization() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ public static class Zoo extends StandardObject {
private final Set<Animal> animals;

public Zoo() {
this(new HashSet<>());
this(new HashSet<Animal>());
}

public Zoo(final Set<Animal> set) {
Expand Down Expand Up @@ -469,7 +469,7 @@ public void testWithDifferentDefaultImplementation() {
}

public void testWithSortedSet() {
final Zoo zoo = new Zoo(new TreeSet<>());
final Zoo zoo = new Zoo(new TreeSet<Animal>());
zoo.add(new Animal("Lion"));
zoo.add(new Animal("Ape"));

Expand Down
Original file line number Diff line number Diff line change
@@ -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 = "<person>\n" +
" <name>name</name>\n" +
" <ignoredField>2</ignoredField>\n" +
" <addresses>\n" +
" <string>one</string>\n" +
" <string>two</string>\n" +
" </addresses>\n" +
"</person>";
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<String> addresses = new ArrayList<>(Arrays.asList("one", "two"));
@XStreamExcludeEmpty
private List<String> emptyList = new ArrayList<>();

@XStreamExcludeEmpty
private Map<Integer, String> emptyMap = new HashMap<>();

}
}
Loading