kiedysktos
kiedysktos

Reputation: 4100

Copy non-null properties from one object to another using BeanUtils or similar

my aim is to copy fields of one object into another, but only those that aren't null. I don't want to assign it explicitly. A more generic solution would be very useful and easier to maintain i.e. for implementing PATCH in REST API where you allow providing only specific fields.

I saw this similar thread and I'm trying to implement some of the ideas from here: Helper in order to copy non null properties from object to another ? (Java)

But the objects aren't altered in any way after the program execution.

So here are my example classes created for example:

class Person {
    String name;
    int age;
    Pet friend;

    public Person() {
    }

    public Person(String name, int age, Pet friend) {
        this.name = name;
        this.age = age;
        this.friend = friend;
    }

    // getters and setters here
}

class Pet {
    String name;
    int age;

    public Pet(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // getters and setters here
}

Here is my overridden copyProperty method:

import org.apache.commons.beanutils.BeanUtilsBean;
import java.lang.reflect.InvocationTargetException;

public class MyBeansUtil extends BeanUtilsBean {

@Override
public void copyProperty(Object dest, String name, Object value)
        throws IllegalAccessException, InvocationTargetException {
    if(value == null) return;
    super.copyProperty(dest, name, value);
}
}

... and here is the place I'm trying to test it on some examples:

public class SandBox {
    public static void main(String[] args) {
        Person db = new Person("John", 36, new Pet("Lucy", 3));
        Person db2 = new Person("John", 36, new Pet("Lucy", 2));
        Person db3 = new Person("John", 36, new Pet("Lucy", 4));

        Person in = new Person();
        in.age = 17;
        in.name = "Paul";
        in.friend = new Pet(null, 35);

        Person in2 = new Person();
        in2.name = "Damian";

        Person in3 = new Person();
        in3.friend = new Pet("Lup", 25);

        try {
            BeanUtilsBean notNull  =new MyBeansUtil();
            notNull.copyProperties(db, in);
            notNull.copyProperties(db2, in2);
            notNull.copyProperties(db3, in3);

        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

Unfortunately, the original objects db, db1, db2 stay the same as they were. Am I doing something wrong here?

Upvotes: 11

Views: 19218

Answers (5)

Christian Masdeval
Christian Masdeval

Reputation: 31

I have faced a similar problem recently. I was asked to implement a generic solution for implementing PATCH in REST API where you allow providing only specific field.

The project is a Java one with MongoDB.

In the beginning I thought would be possible to solve using the Mongo java driver and the operation $set passing the document with only the fields that should be modified. After extensive researching I realized that it doesn't work this way. If you have nested classes it won't update selectively the inner class but instead replace it. I have tried several options using directly the Mongo java driver and SpringMongoDB java API.

Then I went to the BeanUtils solution as described by the author @kiedysktos.

    public class MyBeansUtil extends BeanUtilsBean {

    @Override
    public void copyProperty(Object dest, String name, Object value)
        throws IllegalAccessException, InvocationTargetException {
        if(value == null) return;
        super.copyProperty(dest, name, value);
    }
    }

It turns out that doing only this it won't work properly as well. Imagine you call your PATCH in the following way

{ "name": "John Doe", "friend": { "age":2 } }

The intent of this call is to update the age of the single pet of John Doe to 2. However the overridden code above will replace the entire Pet structure to

{ "name": null, "age" : 2
} erasing the name information.

My final solutions was to call recursively where I found a nested inner class. This way each one will be copied maintaining the previous information. To do that each class involved should implement a marking interface.

    Person implements NonNullCopy
    Pet implements NonNullCopy

Finally, the code:

class NullAwareBeanUtils extends BeanUtilsBean {
    
    
    @Override
    public void copyProperty(Object dest, String name, Object value)
            throws IllegalAccessException, InvocationTargetException {
        if (value == null)
            return;
        else if(value instanceof NonNullCopy) {
            Class<?> destClazz = value.getClass();
                Class<?> origClazz = dest.getClass();
                String className = destClazz.getSimpleName();
        
                //Recursively invokes copyProperties
                for(Method m : origClazz.getDeclaredMethods()) {
                    if(m.getReturnType().equals(destClazz)) {
                        copyProperties(m.invoke(dest, Collections.EMPTY_LIST.toArray()),value);
                    }                       
                }
                return;
        }

        super.copyProperty(dest, name, value);
    }

       
}


Notice that this solution is generic if the class implements the marking interface.

Upvotes: 2

Abu Talha Siddiqi
Abu Talha Siddiqi

Reputation: 91

Using BeanUtils and java8 we can achieve this:

BeanUtils.copyProperties(Object_source, Object_target, getNullPropertyNames(Object_source));

private String[] getNullPropertyNames(Object source) {
        final BeanWrapper wrappedSource = new BeanWrapperImpl(source);
        return Stream.of(wrappedSource.getPropertyDescriptors()).map(FeatureDescriptor::getName)
                .filter(propertyName -> wrappedSource.getPropertyValue(propertyName) == null).toArray(String[]::new);
    }

Upvotes: 6

KayV
KayV

Reputation: 13855

Using ProprtyUtils, we can achieve this using:

    private void copyNonNullProperties(Object destination,
            Object source) {
        try {
            PropertyUtils.describe(source).entrySet().stream()
                    .filter(source -> source.getValue() != null)
                    .filter(source -> !source.getKey().equals("class"))
                    .forEach(source -> {
                        try {
                            PropertyUtils.setProperty(destination, source.getKey(), source.getValue());
                        } catch (Exception e22) {
                            log.error("Error setting properties : {}", e22.getMessage());
                        }
                    });

        } catch (Exception e1) {
            log.error("Error setting properties : {}", e1.getMessage());
        }

    }

Upvotes: 2

Sudhakar
Sudhakar

Reputation: 3180

You can create your own method to copy properties while ignoring null values.

public static String[] getNullPropertyNames (Object source) {
    final BeanWrapper src = new BeanWrapperImpl(source);
    java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();

    Set<String> emptyNames = new HashSet<String>();
    for(java.beans.PropertyDescriptor pd : pds) {
        Object srcValue = src.getPropertyValue(pd.getName());
        if (srcValue == null) emptyNames.add(pd.getName());
    }
    String[] result = new String[emptyNames.size()];
    return emptyNames.toArray(result);
}

// then use Spring BeanUtils to copy and ignore null
public static void myCopyProperties(Object src, Object target) {
    BeanUtils.copyProperties(src, target, getNullPropertyNames(src))
}

Upvotes: 7

kiedysktos
kiedysktos

Reputation: 4100

I ended up using Spring BeanUtils library. Here is my working method:

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

import java.lang.reflect.Field;
import java.util.Collection;

public class MyBeansUtil<T> {
    public T copyNonNullProperties(T target, T in) {
        if (in == null || target == null || target.getClass() != in.getClass()) return null;

        final BeanWrapper src = new BeanWrapperImpl(in);
        final BeanWrapper trg = new BeanWrapperImpl(target);

        for (final Field property : target.getClass().getDeclaredFields()) {
            Object providedObject = src.getPropertyValue(property.getName());
            if (providedObject != null && !(providedObject instanceof Collection<?>)) {
                trg.setPropertyValue(
                        property.getName(),
                        providedObject);
            }
        }
        return target;
    }
}

It works fine, but notice that it ignores fields that are collections. That's on purpose, I handle them separately.

Upvotes: 10

Related Questions