Skip to content

Instantly share code, notes, and snippets.

@isopropylcyanide
Created June 20, 2018 10:20
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save isopropylcyanide/833e435cb261827e2d5af772ce8264a4 to your computer and use it in GitHub Desktop.
Save isopropylcyanide/833e435cb261827e2d5af772ce8264a4 to your computer and use it in GitHub Desktop.
Nested Property Object Mapper. Using reflection, takes an object and returns a map<string, string> where key is all the properties (nested inclusive) and value is the object value
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.IntStream;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j;
@Log4j
@UtilityClass
public class ObjectMapperUtil {
/**
* The object field mapper. xposes a mapper function that takes in a object of
* any type and generates a map of <property_name, value> Property name should
* be prefixed with parent property name and a "." in case of nesting Value
* should always be of type "String". We first populate an initial map with
* nested values. Then we use a utility function that helps us process the map
* in a certain format
*
* Please note that for testing under Jacoco its recommended to filter out
* synthetic fields as Jacoco plugs in some of its own during reflection
*
* @param element
* the element
* @return the map
*/
public static Map<String, String> objectFieldMapper(Object element) {
Map<String, String> fieldMap = new HashMap<>();
final String root = element.getClass().getSimpleName();
Field[] fields = element.getClass().getDeclaredFields();
Arrays.stream(fields).filter(field -> !field.isSynthetic())
.forEach(field -> getNestedFieldMap(field, root, fieldMap, element));
return fieldMap;
}
/**
* Gets the nested field map given an object. Note that an object may contain
* only user defined types or a list. Every field might have several other
* fields. In order to get it we need to recursively run the function repeatedly
* until we reach a field for which we are sure there won't be any descendants
* e.g
*
* Class -> Student -> Name. Since name is most likely a string, we won't
* recurse further Key thing to handle is, when we do have list as a field. We
* need to handle it specially. If the list is of a primitive type, we add to
* the map, all its items indexed by the position. <br>
* If however, the list is made up of a custom user type, we need to recurse for
* each of the fields in order to populate the map
*
*
* @param field
* the field
* @param root
* the root
* @param fieldMap
* the field map
* @param element
* the element
* @return the nested field map
*/
@SuppressWarnings("unchecked")
private void getNestedFieldMap(Field field, String root, Map<String, String> fieldMap, Object element) {
field.setAccessible(true);
String keyFieldName = StringUtils.capitalize(field.getName());
try {
Object value = field.get(element);
if (null == value) {
fieldMap.put(root + "." + keyFieldName, "null");
return;
}
if (value instanceof String || ClassUtils.isPrimitiveOrWrapper(element.getClass())) {
fieldMap.put(root + "." + keyFieldName, value.toString());
} else if (value instanceof List) {
Field listField = element.getClass().getDeclaredField(field.getName());
listField.setAccessible(true);
Optional<Class<?>> genericTypeClass = getGenericTypeOfFieldByClassName(element.getClass(),
field.getName());
List<Object> valueItems = (List<Object>) listField.get(element);
genericTypeClass.filter(ObjectMapperUtil::isClassPrimitiveAndNotUserDefinedType)
.ifPresent(c -> IntStream.range(0, valueItems.size()).forEach(i -> {
String key = root + "." + keyFieldName + "[" + i + "]";
String keyValue = valueItems.get(i).toString();
fieldMap.put(key, keyValue);
}));
genericTypeClass.filter(ObjectMapperUtil::isClassNotPrimitiveAndIsUserDefinedType)
.ifPresent(c -> IntStream.range(0, valueItems.size()).forEach(i -> {
String newRoot = root + "." + keyFieldName + "[" + i + "]";
Arrays.stream(c.getDeclaredFields()).filter(f -> !f.isSynthetic())
.forEach(f -> getNestedFieldMap(f, newRoot, fieldMap, valueItems.get(i)));
}));
} else {
Arrays.stream(field.getType().getDeclaredFields()).filter(f -> !f.isSynthetic())
.forEach(f -> getNestedFieldMap(f, root + "." + keyFieldName, fieldMap, value));
}
} catch (Exception e) {
log.error(e.getStackTrace());
}
}
/**
* Gets the generic type parameter information of a fieldname present in the
* class. Owing to type erasure, java removes the generics in the underlying
* byte code. <br>
* As a result, List<Integer> is simply a List in the class file. This function
* uses reflection to capture the parameter type information and return it
* wrapped in an optional. <br>
* If an exception occurs it returns an empty optional. If no generics are
* present return the class type of the field
*
* @param clazz
* the clazz
* @param fieldName
* the field name
* @return the generic type of field by class name
*/
public Optional<Class<?>> getGenericTypeOfFieldByClassName(Class<?> clazz, String fieldName) {
Class<?> fieldType = null;
try {
Field field = clazz.getDeclaredField(fieldName);
fieldType = field.getType();
ParameterizedType stringListType = (ParameterizedType) field.getGenericType();
Class<?> stringListClass = (Class<?>) stringListType.getActualTypeArguments()[0];
return Optional.of(stringListClass);
} catch (NoSuchFieldException e) {
} catch (ClassCastException e2) {
return Optional.of(fieldType);
}
return Optional.empty();
}
/**
* Checks if is class primitive and not user defined type. Predominantly, if
* class is a string or any amongst (Boolean, Byte, Character, Short, Integer,
* Long, Double, Float) or their primitives, it returns true
*
* @param clazz
* the clazz
* @return the boolean
*/
public Boolean isClassPrimitiveAndNotUserDefinedType(Class<?> clazz) {
return clazz.equals(String.class) || ClassUtils.isPrimitiveOrWrapper(clazz);
}
/**
* Checks if is class not primitive and is user defined type.
*
* @param clazz
* the clazz
* @return the boolean
*/
public Boolean isClassNotPrimitiveAndIsUserDefinedType(Class<?> clazz) {
return !clazz.equals(String.class) && !ClassUtils.isPrimitiveOrWrapper(clazz);
}
}
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.junit.Test;
@lombok.Data
class SuperUser {
private String pwd;
private List<User> users;
private Record record;
}
@lombok.Data
class Record {
private List<String> rId;
}
@lombok.Data
class User {
private String id;
private Address address;
}
@lombok.Data
class Address {
private String pin;
private String area;
}
public class ObjectMapperUtilTest {
@Test
public void objectFieldMapperTest() {
Address address = new Address();
address.setArea("Area(length=10) ");
address.setPin("my pin");
Map<String, String> addressMap = ObjectMapperUtil.objectFieldMapper(address);
assertEquals(2, addressMap.size());
assertEquals("Area(length=10) ", addressMap.get("Address.Area"));
assertEquals("my pin", addressMap.get("Address.Pin"));
User user = new User();
user.setId("123");
user.setAddress(address);
Map<String, String> userMap = ObjectMapperUtil.objectFieldMapper(user);
assertEquals(3, userMap.size());
assertEquals("Area(length=10) ", userMap.get("User.Address.Area"));
assertEquals("my pin", userMap.get("User.Address.Pin"));
assertEquals("123", userMap.get("User.Id"));
SuperUser superUser = new SuperUser();
Record record = new Record();
record.setRId(new ArrayList<>(Arrays.asList("R1", "R2", "R3")));
List<User> users = new ArrayList<>();
users.add(user);
superUser.setUsers(users);
superUser.setPwd("xxx");
superUser.setRecord(record);
Map<String, String> superUserMap = ObjectMapperUtil.objectFieldMapper(superUser);
assertEquals(7, superUserMap.size());
assertEquals("xxx", superUserMap.get("SuperUser.Pwd"));
assertEquals("my pin", superUserMap.get("SuperUser.Users[0].Address.Pin"));
assertEquals("Area(length=10) ", superUserMap.get("SuperUser.Users[0].Address.Area"));
assertEquals("123", superUserMap.get("SuperUser.Users[0].Id"));
assertEquals("R1", superUserMap.get("SuperUser.Record.RId[0]"));
assertEquals("R2", superUserMap.get("SuperUser.Record.RId[1]"));
assertEquals("R3", superUserMap.get("SuperUser.Record.RId[2]"));
}
@Test
public void getGenericTypeOfListFieldByClassNameTest() {
Class<?> clazz = SuperUser.class;
String fieldName = "users";
Optional<Class<?>> genericUsersClass = ObjectMapperUtil.getGenericTypeOfFieldByClassName(clazz, fieldName);
assertEquals("User", genericUsersClass.map(Class::getSimpleName).get());
fieldName = "record";
Optional<Class<?>> genericRecordClass = ObjectMapperUtil.getGenericTypeOfFieldByClassName(clazz, fieldName);
assertEquals("Record", genericRecordClass.map(Class::getSimpleName).get());
fieldName = "notPresent";
Optional<Class<?>> genericNotPresentClass = ObjectMapperUtil.getGenericTypeOfFieldByClassName(clazz, fieldName);
assertEquals(Optional.empty(), genericNotPresentClass);
}
}
@isopropylcyanide
Copy link
Author

Currently supports objects having a list of properties or a single property which may either be primitive/user defined property. For the purpose of the project, we include String as primitive.
This gist can be easily extended to include collections as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment