Commit 0e7f1249 authored by Matija Obreza's avatar Matija Obreza
Browse files

Copyable interface with default implementation that copies declared fields,...

Copyable interface with default implementation that copies declared fields, inherited public, protected and default fields, ignores Maps, Collections and Arrays
parent a14ab618
......@@ -15,11 +15,103 @@
*/
package org.genesys.blocks.model;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
import org.springframework.util.ReflectionUtils.FieldFilter;
public interface Copyable<T> {
/**
* Make a deep copy of the object, with all new instances
* Make a deep copy of the object, with all new instances. Implementations should use {@link #apply(Object)} to
* apply this values to the new instance.
*
* <pre>
* &#64;Override
* public Budget copy() {
* final Budget copy = new Budget();
* copy.apply(this);
* return copy;
* }
* </pre>
*
* @return a deep copy
*/
T apply(T source);
T copy();
/**
* Apply values from source to this object. It should not handle lists, sets and other complicated things.
*
* The default implementation is based on {@link ReflectionUtils#shallowCopyFieldState(Object, Object)} but it only
* uses accessible fields and ignores Collections.
*
* @param source
* @return
*/
default T apply(T source) {
if (source == null) {
throw new IllegalArgumentException("Source for field copy cannot be null");
}
if (!source.getClass().equals(this.getClass())) {
throw new IllegalArgumentException("Source class [" + source.getClass().getName() + "] must be same as target class [" + this.getClass().getName() + "]");
}
@SuppressWarnings("unchecked")
T dest = (T) this;
ReflectionUtils.doWithFields(source.getClass(), new FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
if (field.getDeclaringClass().equals(dest.getClass()) || Modifier.isProtected(field.getModifiers()) || Modifier.isPublic(field.getModifiers()) || field.getModifiers() == 0) {
ReflectionUtils.makeAccessible(field);
// System.err.println(field + " is made accessible, modifiers = " + field.getModifiers());
Object srcValue = field.get(source);
field.set(dest, srcValue);
} else {
// System.err.println(field + " not accessible, modifiers = " + field.getModifiers());
}
}
}, COPYABLE_FIELDS);
return dest;
}
/**
* Pre-built FieldFilter that matches all non-static, non-final, non-Collection fields.
*/
public static final FieldFilter COPYABLE_FIELDS = new FieldFilter() {
@Override
public boolean matches(Field field) {
return !(Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers())
// or collection
|| Collection.class.isAssignableFrom(field.getType())
// or map
|| Map.class.isAssignableFrom(field.getType())
// or array
|| field.getClass().isArray());
}
};
/**
* Utility method that makes a deep copy of {@link Copyable} elements
*
* @param source
* @param action
* @return
*/
default <R extends Copyable<R>> List<R> copyList(List<R> source, Consumer<? super R> action) {
if (source == null || source.size() == 0) {
return new ArrayList<R>();
}
return source.stream().map(item -> item.copy()).peek(action).collect(Collectors.toList());
}
}
......@@ -32,7 +32,7 @@ public abstract class VersionedModel extends BasicModel implements Activatable {
/** Active by default! */
@Column(nullable = false)
boolean active = true;
protected boolean active = true;
public Integer getVersion() {
return version;
......
/*
* Copyright 2017 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.blocks.model;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
public class CopyableTest {
private static class Parent {
public static int STATIC_INT = 33;
int defaultInt;
private int privateInt;
protected int protectedInt;
public int publicInt;
final int finalInt;
protected Set<String> set = new HashSet<>();
protected List<String> list = new ArrayList<>();
protected Map<String, String> map = new HashMap<>();
Parent() {
finalInt = 42;
}
public int getPrivateInt() {
return privateInt;
}
public void setPrivateInt(int privateInt) {
this.privateInt = privateInt;
}
public void put(String x) {
set.add(x);
list.add(x);
map.put(x, x);
}
}
private static class Child extends Parent implements Copyable<Child> {
private int myInt;
int myDefaultInt;
final int myFinalInt;
private Set<String> mySet = new HashSet<>();
private List<String> myList = new ArrayList<>();
private Map<String, String> myMap = new HashMap<>();
Child() {
myFinalInt = 43;
}
public void put(String x) {
mySet.add(x);
myList.add(x);
myMap.put(x, x);
super.put(x);
}
@Override
public Child copy() {
Child copy = new Child();
copy.apply(this);
return copy;
}
}
@Test
public void testCopy() {
Child source = new Child();
source.defaultInt = 1;
source.protectedInt = 3;
source.publicInt = 4;
source.myInt = 5;
source.setPrivateInt(6);
source.myDefaultInt = 7;
source.put("test1");
source.put("test2");
assertTrue(Map.class.isAssignableFrom(source.map.getClass()));
assertTrue(Collection.class.isAssignableFrom(source.set.getClass()));
assertTrue(Collection.class.isAssignableFrom(source.list.getClass()));
Child copy = source.copy();
assertThat(Parent.STATIC_INT, is(33));
assertThat(copy.finalInt, is(42));
assertThat(copy.defaultInt, is(source.defaultInt));
assertThat(copy.protectedInt, is(source.protectedInt));
assertThat(copy.publicInt, is(source.publicInt));
assertThat(copy.finalInt, is(source.finalInt));
assertThat(copy.myInt, is(source.myInt));
assertThat(copy.myFinalInt, is(43));
assertThat(copy.getPrivateInt(), not(is(source.getPrivateInt())));
assertThat(copy.myDefaultInt, is(source.myDefaultInt));
assertThat(copy.set, hasSize(0));
assertThat(copy.list, hasSize(0));
assertThat(copy.map.keySet(), hasSize(0));
assertThat(copy.mySet, hasSize(0));
assertThat(copy.myList, hasSize(0));
assertThat(copy.myMap.keySet(), hasSize(0));
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment