Object in Object to HashMap with reflection - java

Project:
I am currently using a tool called Netuno to develop an api, so to do the export of objects to json it is necessary to do the transformation of the object to HashMap, for this a method was developed in a parent class to export the object.
The method:
public Map<String, Object> export() {
Object obj = this;
Map<String, Object> map = new HashMap<>();
for (Field field : obj.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
map.put(field.getName(), field.get(obj));
} catch (Exception e) {
//todo e
}
}
return map;
}
This works, but only for simple objects.
My problem:
If the object has complex objects inside my method it has no ability to export them to HashMap either.
Example structure:
public abstract class Master {
public Map < String, Object >
export () {
Object obj = this;
Map < String, Object > map = new HashMap < > ();
for (Field field: obj.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
map.put(field.getName(), field.get(obj));
} catch (Exception e) {
//todo e
}
}
return map;
}
}
public class Foo extends Master {
private int a;
private int b;
private String c;
private Bar bar;
//...
}
public class Bar extends Master {
private int q;
private int w;
private String e;
//...
}
I use it this way:
return new Bar(/*data*/).export();
Output:
{
"a": 2,
"b": 5,
"c": "abc",
"bar": "myproject.myPackage.Bar#XXXXX"
}
Expected Output:
{
"a": 2,
"b": 5,
"c": "abc",
"bar": {
"q": 10,
"w": 15,
"e": "it works"
}
}

You need to decide if a value should me put plain in the map or recursively. Therefore you can use a simple method like this:
private boolean isSimpleType(Class<?> type) {
return type.isPrimitive() ||
Boolean.class == type ||
Character.class == type ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Enum.class.isAssignableFrom(type);
}
This method returns true for all primitive types or the Wrapper objects, Strings and Enums. You can simply adjust that methods to fit your needs. Look here for more details on how to determine if a class is simple.
Now you can use to transform an object:
public Map<String, Object> convert(Object object) {
Map<String, Object> map = new HashMap<>();
for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
if (isSimpleType(field.getType())) {
map.put(field.getName(), field.get(object));
} else {
map.put(field.getName(), convert(field.get(object)));
}
} catch (Exception e) {
e.printStackTrace();
}
}
return map;
}
You also can adjust this to do fir your export method.
The result for your example will be this:
{a=2, b=5, bar={q=10, e=it works, w=15}, c=abc}
Beside that I would strongly recommend using an library (like Jackson), which already does stuff like this by perfection. Here is the same solution using Jackson:
Map result = new ObjectMapper().convertValue(object, Map.class);
Or if you need type safety:
ObjectMapper mapper = new ObjectMapper();
MapType mapType = mapper.getTypeFactory().constructMapType(Map.class, String.class, Object.class);
Map<String, Object> result = mapper.convertValue(value, mapType);

Related

Java 8 to remove string value from each field where "string" value comes of the Custom Object

I went through link: Is it possible in Java to check if objects fields are null and then add default value to all those attributes? and implemented the same solution as below -
Note: I am using Swagger/Open API Specs (using springdoc-openapi-ui) and while making POST request all string fields are having default value as "string" which I really wanted to set it to null or space.
Any quick pointer ?
public static Object getObject(Object obj) {
for (Field f : obj.getClass().getFields()) {
f.setAccessible(true);
try {
if (f.get(obj) == "string") {
f.set(obj, null);
}
} catch (IllegalArgumentException | IllegalAccessException e) {
log.error("Error While Setting default values for String");
}
}
return obj;
}
REST endpoints
#GetMapping(value = "/employees")
public ResponseEntity<PagedModel<EmployeeModel>> findEmployees(
EmployeeDto geoDto,
#Parameter(hidden=true) String sort,
#Parameter(hidden=true) String order,
#Parameter(hidden=true) Pageable pageRequest) {
EmployeeDto dto = (EmployeeDto) CommonsUtil.getObject(geoDto);
Page<CountryOut> response = countryService..............;
PagedModel<EmployeeModel> model = employeePagedAssembler.toModel(response, countryOutAssembler);
return new ResponseEntity<>(model, HttpStatus.OK);
}
You could do it a bit simpler, I guess. If you control EmployeeDto, for example:
#Accessors(chain = true)
#Getter
#Setter
#ToString
static class EmployeeDto {
private String firstname;
private String lastname;
private int age;
}
You could iterate over fields of the class and using MethodHandles invoke the needed setters, when some getters return the string you are interested in (and Strings are compared using equals, not ==). This can even be made into a tiny library. Here is a start:
private static final Lookup LOOKUP = MethodHandles.lookup();
/**
* this computes all the know fields of some class (EmployeeDTO in your case) and their getter/setter
*/
private static final Map<Class<?>, Map<Entry<String, ? extends Class<?>>, Entry<MethodHandle, MethodHandle>>> ALL_KNOWN =
Map.of(
EmployeeDto.class, metadata(EmployeeDto.class)
);
private Map<String, Entry<MethodHandle, MethodHandle>> MAP;
/**
* For example this will hold : {"firstname", String.class} -> getter/setter to "firstname"
*/
private static Map<Entry<String, ? extends Class<?>>, Entry<MethodHandle, MethodHandle>> metadata(Class<?> cls) {
return Arrays.stream(cls.getDeclaredFields())
.map(x -> new SimpleEntry<>(x.getName(), x.getType()))
.collect(Collectors.toMap(
Function.identity(),
entry -> {
try {
return new SimpleEntry<>(
LOOKUP.findGetter(cls, entry.getKey(), entry.getValue()),
LOOKUP.findSetter(cls, entry.getKey(), entry.getValue()));
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
));
}
With that information you can provide a public method for users to call, So you need to provide the actual instance of your DTO, the DTO class, the Class of the fields you want to "default to", the equality to check against and the actual defaultValue.
public static <T, R> T defaulter(T initial,
Class<T> dtoClass,
Class<R> fieldType,
R equality,
R defaultValue) throws Throwable {
Set<Entry<MethodHandle, MethodHandle>> all =
ALL_KNOWN.get(dtoClass)
.entrySet()
.stream()
.filter(x -> x.getKey().getValue() == fieldType)
.map(Entry::getValue)
.collect(Collectors.toSet());
for (Entry<MethodHandle, MethodHandle> getterAndSetter : all) {
R whatWeGot = (R) getterAndSetter.getKey().invoke(initial);
if (Objects.equals(whatWeGot, equality)) {
getterAndSetter.getValue().invoke(initial, defaultValue);
}
}
return initial;
}
And this is how your callers can call it:
public static void main(String[] args) throws Throwable {
EmployeeDto employeeDto = new EmployeeDto()
.setFirstname("string")
.setLastname("string");
EmployeeDto withDefaults = defaulter(employeeDto, EmployeeDto.class, String.class, "string", "defaultValue");
System.out.println(withDefaults);
}

Convert java.util.Set of <key,value> object to simple pojo object

Getting a java.util.Set of objects which has only key-value pairs as input like below
public class KeyValueObject {
String key;
String value;
// good old constructor, setters and getters
}
Input Object:
java.util.Set inputObject;
KeyValuePair pair1 = new KeyValuePair("Name":"John");
KeyValuePair pair2 = new KeyValuePair("Age":"28");
KeyValuePair pair3 = new KeyValuePair("Location":"Cincinnati");
inputObject.add(pair1);
inputObject.add(pair2);
inputObject.add(pair3);
With the "inputObject" as the request coming in, how to convert this to a simple POJO object which has all the keys described above as individual parameters like below:
Public class SimplePojoObject {
private String name;
private String age;
private String location;
// Good old setters and getters
}
The incoming object has around 52 objects and that is why the manual way of mapping is not the right way to solve this issue. Please suggest on any possible way of mapping this data
You can do it like this:
Convert Set<KeyValueObject> to a JsonNode (or Map<String, String>) object.
Convert the generated JsonNode (or Map<String, String>) object to SimplePojoObject using ObjectMapper (you can also use Gson library instead of ObjectMapper)
If you already have a Map<String, String> object instead of Set<KeyValueObject> then you can do it in just one line:
SimplePojoObject simplePojoObject = new ObjectMapper().convertValue(map, SimplePojoObject.class);
The easiest way is to write a small method that calls the setters:
public SimplePojoObject buildSimplePojoObject(Set<KeyValuePair> properties) {
SimplePojoObject result = new SimplePojoObject();
for (KeyValuePair prop : properties) {
switch (prop.getKey()) {
case "Name":
result.setName(prop.getValue());
break;
case "Age":
result.setAge(prop.getValue());
break;
case "Location":
result.setLocation(prop.getValue());
break;
default:
// Throw an exception or ignore it.
throw new IllegalArgumentException("Unknown property "+ prop.getKey());
}
}
return result;
}
But if you want to do that dynamically, you certainly could:
public SimplePojoObject buildSimplePojoObject(Set<KeyValuePair> properties) {
SimplePojoObject result = new SimplePojoObject();
Lookup l = MethodHandles.publicLookup();
MethodType mt = MethodType.methodType(void.class, String.class);
for (KeyValuePair prop : properties) {
MethodHandle mh = l.findVirtual(SimplePojoObject.class, "set" + prop.getKey());
try {
mh.invokeExact(result, prop.getValue());
} catch (Error | RuntimeException e) {
throw e;
} catch (Throwable t) {
// MethodHandle.invokeExact is declared to throw Throwable, so we have to catch it.
throw new RuntimeException(e);
}
}
return result;
}

Jackson how to convert array of objects to flat properties in class depends on value

I have the following json object came from remote service:
{
"accumulators": [
{
"balance": "100",
"name": "SMS",
"units": "International SMS"
},
{
"balance": "100",
"name": "VOICE",
"units": "minutes"
},
{
"balance": "50",
"name": "MMS",
"units": "MMS"
}
]
}
I wand to convert map it to the following class depends on the value of object inside that array, so if it is "name": "MMS" then the value must be set to the value of AccumulatorDTO MMS;:
public class BaseDTO {
private AccumulatorDTO messages;
private AccumulatorDTO minutes;
private AccumulatorDTO MMS;
// setters and geters
}
public class AccumulatorDTO {
private int balance;
private String name;
private String units;
// setters and geters
}
Any Idea how to do that using Jackson annotation or custom deserializer?
I can do something like:
AccumulatorDTO[] accumulators = (new ObjectMapper()).convertValue(response.asJson().get("accumulators"), AccumulatorDTO[].class);
Then make iteration over the array and set each property, but really hard to my project structure, I'm looking fo really better solution for a generic purpose (using 1 method for all remote servers, and the deserialization better to be inside DTO somehow, I'm doing some wrapping layer between frontend and backend).
Consider using reflection and name the BaseDTO fields according to the name field in the JSON. Using a #JsonCreator annotated constructor fulfils the requirement of "deserialization better to be inside DTO somehow". E.g.
class BaseDTO {
private AccumulatorDTO sms;
private AccumulatorDTO voice;
private AccumulatorDTO mms;
#JsonCreator
public BaseDTO(#JsonProperty("accumulators") final AccumulatorDTO[] accumulators) {
for (AccumulatorDTO accumulator : accumulators) {
String fieldName = accumulator.getName().toLowerCase();
try {
Field field = getClass().getDeclaredField(fieldName);
field.set(this, accumulator);
} catch (NoSuchFieldException | IllegalAccessException ignored) {
}
}
}
}
And deserialise like this:
BaseDTO accumulators = new ObjectMapper().readValue(response.asJson(), BaseDTO.class);
This will initialise the BaseDTO fields according to the array elements and their name. It will let a field be null if it can't match it to an array element and exceptions are thrown.
Jackson doesn't have an annotation to do what you want AFAIK.
The answer provided from #Manos Nikolaidis helped me so much to code my real answer, his answer is good to start, in my cause some of the values contains a spaces or just a non-standard, so I do create a map to map between fields on JSON and the class:
#JsonCreator
public AccountDTO(#JsonProperty("accumulators") final AccumulatorDTO[] accumulators) {
HashMap<String, String> accumulatorsMap = new HashMap<>();
// key is value from JSON, value is field name of class
accumulatorsMap.put("intl sms", "internationalSMS");
accumulatorsMap.put("voice", "minutes");
accumulatorsMap.put("mms", "MMS");
accumulatorsMap.put("voicemessage", "voiceMessages");
accumulatorsMap.put("message", "messages");
for (AccumulatorDTO accumulator : accumulators) {
String fieldName = accumulator.getName().toLowerCase();
try {
Field field = getClass().getDeclaredField(accumulatorsMap.get(fieldName));
field.set(this, accumulator);
} catch (NoSuchFieldException | IllegalAccessException ignored) {
}
}
}
I created this Jackson custom deserializer for your BaseDTO class that satisfies your requirement. It looks for the "balance" property and when it found it knows that the following are "name" and "units" so it takes them. Then it switchs on the "name" property and it sets the current AccumulatorDTO to the right field of the BaseDTO class.
public class CustomDeserializer extends StdDeserializer<BaseDTO>{
/**
*
*/
private static final long serialVersionUID = 1L;
public CustomDeserializer(Class<BaseDTO> t) {
super(t);
}
#Override
public BaseDTO deserialize(JsonParser jp, DeserializationContext dc)
throws IOException, JsonProcessingException {
BaseDTO bd = new BaseDTO();
JsonToken currentToken = null;
while ((currentToken = jp.nextValue()) != null) {
if (jp.getCurrentName() != null && jp.getCurrentName().equals("balance"))
{
System.out.println(jp.getCurrentName());
AccumulatorDTO adto = new AccumulatorDTO();
adto.setBalance(Integer.parseInt(jp.getValueAsString()));
currentToken = jp.nextValue();
adto.setName(jp.getValueAsString());
currentToken = jp.nextValue();
adto.setUnits(jp.getValueAsString());
switch (adto.getName().toLowerCase())
{
case "sms":
bd.setMessages(adto);
break;
case "voice":
bd.setMinutes(adto);
break;
case "mms":
bd.setMMS(adto);
break;
}
}
}
return bd;
}
}
I tested it with this simple program:
public class JsonArrayExampleApplication {
public static void main(String[] args) {
//
String json = "{\"accumulators\": [{\"balance\": \"100\",\"name\": \"SMS\",\"units\": \"International SMS\"" +
"},{\"balance\": \"100\",\"name\": \"VOICE\",\"units\": \"minutes\"},{\"balance\": \"50\",\"name\": \"MMS\"," +
"\"units\": \"MMS\"}]}";
ObjectMapper mapper = new ObjectMapper();
SimpleModule mod = new SimpleModule("MyModule");
mod.addDeserializer(BaseDTO.class, new CustomDeserializer(BaseDTO.class));
mapper.registerModule(mod);
BaseDTO bdto = null;
try {
bdto = mapper.readValue(json, BaseDTO.class);
}
catch (IOException e) {
e.printStackTrace();
}
System.out.println("\n--- JSON to JAVA ---\n" + bdto);
}
}
and I got the following output that seems to be ok, because every AccumulatorDTO has been associated to the right property.
--- JSON to JAVA ---
BaseDTO [messages=AccumulatorDTO [balance=100, name=SMS, units=International SMS], minutes=AccumulatorDTO [balance=100, name=VOICE, units=minutes], MMS=AccumulatorDTO [balance=50, name=MMS, units=MMS]]

Gson SerializedName change in some environnement

I have a DTO with some GSon annotation.
My probleme is that the value of these annotations have to change if my application run in developpement or in staging or in production...
For the moment, I have to package my application with the different value and I want this to be automatic... It is in a Spring Boot application and I want to use the spring.profiles.active to tell my application to take the right serializedName
Here is the kind of code I use
// Tests
// #SerializedName("customfield_10123")
// Prod
#SerializedName("customfield_10114")
private ActionDto action;
I hope there is a better way to do it?
Here is a very crude example on how you can achieve what you want:
First create a propery file for each possible profile (name can be anything, but the profile must be on the name):
application-dev.properties
application-prod.properties
...
Populate the properties with the values you want for each key accordingly to each profile:
test=abc.test
...
Annotate your POJOs:
public class Foo {
#SerializedName("${test}")
private String name;
...
}
Create a custom serializer for your class, which will interpret the custom names, something like this:
public class FooSerializer implements JsonSerializer<Foo> {
private static final Pattern PATTERN = Pattern.compile("\\$\\{(.*)\\}");
private static Properties props;
static {
try {
Resource resource = new ClassPathResource(String.format("/application-%s.properties", System.getProperty("spring.profiles.active")));
props = PropertiesLoaderUtils.loadProperties(resource);
} catch (IOException e) {
e.printStackTrace();
}
}
#Override
public JsonElement serialize(Foo foo, Type type, JsonSerializationContext jsonSerializationContext) {
Field[] fields = foo.getClass().getDeclaredFields();
JsonObject object = new JsonObject();
for (Field field : fields) {
field.setAccessible(true);
String name = field.getName();
if (field.isAnnotationPresent(SerializedName.class)) {
String value = field.getAnnotation(SerializedName.class).value();
Matcher matcher = PATTERN.matcher(value);
if (matcher.find()) {
name = props.get(matcher.group(1)).toString();
} else {
name = value;
}
}
try {
if (field.get(foo) != null) {
object.addProperty(name, field.get(foo).toString());
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return object;
}
}
Now you just need to register your custom serializer and you are good to go:
Gson gson = new GsonBuilder().registerTypeAdapter(Foo.class, new FooSerializer()).setPrettyPrinting().create();
Of course there may be better ways to recover the properties file according to the active profile, but the given snippet should be enough to get you going. Also, you need to consider the fact that there may be multiple profiles active at any given time, so if that is your scenario, you need to take it into consideration before recovering the properties.
You don't even need the regex part if you will always want to use the value from the properties. I used a regex to allow both cases.
If something wasn't clear, please let me know and I will try to improve it.
EDIT:
For the deserialization I can't come up with anything very good, so here is an example which I think is far from OK, but gets the job done:
Functional interface:
public interface Converter {
Object convert(String s);
}
Deserializer:
public class FooDeserializer implements JsonDeserializer<Foo> {
private static final Pattern PATTERN = Pattern.compile("\\$\\{(.*)\\}");
private static Properties props;
private static Map<Class, Converter> converterForClass = new HashMap<>();
static {
try {
Resource resource = new ClassPathResource(String.format("/application-%s.properties", System.getProperty("spring.profiles.active")));
props = PropertiesLoaderUtils.loadProperties(resource);
converterForClass.put(Integer.TYPE, s -> Integer.parseInt(s.replace("\"", "")));
converterForClass.put(Double.TYPE, s -> Double.parseDouble(s.replace("\"", "")));
converterForClass.put(String.class, s -> s);
converterForClass.put(Long.TYPE, s -> Long.parseLong(s.replace("\"", "")));
converterForClass.put(Boolean.TYPE, s -> Boolean.parseBoolean(s.replace("\"", "")));
} catch (IOException e) {
e.printStackTrace();
}
}
#Override
public Foo deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
Foo foo = new Foo();
JsonObject jobject = (JsonObject) jsonElement;
for (Entry entry : jobject.entrySet()) {
Field field = searchField(entry.getKey().toString());
if (field != null) {
field.setAccessible(true);
try {
Object r = converterForClass.get(field.getType()).convert(entry.getValue().toString());
field.set(foo, r);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return foo;
}
private Field searchField(String name) {
Field[] fields = Foo.class.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
if (field.isAnnotationPresent(SerializedName.class)) {
String value = field.getAnnotation(SerializedName.class).value();
Matcher matcher = PATTERN.matcher(value);
if (value.equals(name)) {
return field;
} else if (matcher.find()) {
if (props.get(matcher.group(1)).equals(name)) {
return field;
}
}
} else {
if (field.getName().equals(name)) {
return field;
}
}
}
return null;
}
Register the deserializer:
gsonBuilder.registerTypeAdapter(Foo.class, new FooDeserializer());
The problem with the above approach is it will NOT work with nested objects. You will have to some further verifications and implementation. It is using Java 8 features as well.

Recursive BeanUtils.describe()

Is there a version of BeanUtils.describe(customer) that recursively calls the describe() method on the complex attributes of 'customer'.
class Customer {
String id;
Address address;
}
Here, I would like the describe method to retrieve the contents of the address attribute as well.
Currently, all I have can see the name of the class as follows:
{id=123, address=com.test.entities.Address#2a340e}
Funny, I would like the describe method to retrieve the contents of nested attributes as well, I don't understand why it doesn't. I went ahead and rolled my own, though. Here it is, you can just call:
Map<String,String> beanMap = BeanUtils.recursiveDescribe(customer);
A couple of caveats.
I'm wasn't sure how commons BeanUtils formatted attributes in collections, so i went with "attribute[index]".
I'm wasn't sure how it formatted attributes in maps, so i went with "attribute[key]".
For name collisions the precedence is this: First properties are loaded from the fields of super classes, then the class, then from the getter methods.
I haven't analyzed the performance of this method. If you have objects with large collections of objects that also contain collections, you might have some issues.
This is alpha code, not garunteed to be bug free.
I am assuming that you have the latest version of commons beanutils
Also, fyi, this is roughly taken from a project I've been working on called, affectionately, java in jails so you could just download it and then run:
Map<String, String[]> beanMap = new SimpleMapper().toMap(customer);
Though, you'll notice that it returns a String[], instead of a String, which may not work for your needs. Anyway, the below code should work, so have at it!
public class BeanUtils {
public static Map<String, String> recursiveDescribe(Object object) {
Set cache = new HashSet();
return recursiveDescribe(object, null, cache);
}
private static Map<String, String> recursiveDescribe(Object object, String prefix, Set cache) {
if (object == null || cache.contains(object)) return Collections.EMPTY_MAP;
cache.add(object);
prefix = (prefix != null) ? prefix + "." : "";
Map<String, String> beanMap = new TreeMap<String, String>();
Map<String, Object> properties = getProperties(object);
for (String property : properties.keySet()) {
Object value = properties.get(property);
try {
if (value == null) {
//ignore nulls
} else if (Collection.class.isAssignableFrom(value.getClass())) {
beanMap.putAll(convertAll((Collection) value, prefix + property, cache));
} else if (value.getClass().isArray()) {
beanMap.putAll(convertAll(Arrays.asList((Object[]) value), prefix + property, cache));
} else if (Map.class.isAssignableFrom(value.getClass())) {
beanMap.putAll(convertMap((Map) value, prefix + property, cache));
} else {
beanMap.putAll(convertObject(value, prefix + property, cache));
}
} catch (Exception e) {
e.printStackTrace();
}
}
return beanMap;
}
private static Map<String, Object> getProperties(Object object) {
Map<String, Object> propertyMap = getFields(object);
//getters take precedence in case of any name collisions
propertyMap.putAll(getGetterMethods(object));
return propertyMap;
}
private static Map<String, Object> getGetterMethods(Object object) {
Map<String, Object> result = new HashMap<String, Object>();
BeanInfo info;
try {
info = Introspector.getBeanInfo(object.getClass());
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
Method reader = pd.getReadMethod();
if (reader != null) {
String name = pd.getName();
if (!"class".equals(name)) {
try {
Object value = reader.invoke(object);
result.put(name, value);
} catch (Exception e) {
//you can choose to do something here
}
}
}
}
} catch (IntrospectionException e) {
//you can choose to do something here
} finally {
return result;
}
}
private static Map<String, Object> getFields(Object object) {
return getFields(object, object.getClass());
}
private static Map<String, Object> getFields(Object object, Class<?> classType) {
Map<String, Object> result = new HashMap<String, Object>();
Class superClass = classType.getSuperclass();
if (superClass != null) result.putAll(getFields(object, superClass));
//get public fields only
Field[] fields = classType.getFields();
for (Field field : fields) {
try {
result.put(field.getName(), field.get(object));
} catch (IllegalAccessException e) {
//you can choose to do something here
}
}
return result;
}
private static Map<String, String> convertAll(Collection<Object> values, String key, Set cache) {
Map<String, String> valuesMap = new HashMap<String, String>();
Object[] valArray = values.toArray();
for (int i = 0; i < valArray.length; i++) {
Object value = valArray[i];
if (value != null) valuesMap.putAll(convertObject(value, key + "[" + i + "]", cache));
}
return valuesMap;
}
private static Map<String, String> convertMap(Map<Object, Object> values, String key, Set cache) {
Map<String, String> valuesMap = new HashMap<String, String>();
for (Object thisKey : values.keySet()) {
Object value = values.get(thisKey);
if (value != null) valuesMap.putAll(convertObject(value, key + "[" + thisKey + "]", cache));
}
return valuesMap;
}
private static ConvertUtilsBean converter = BeanUtilsBean.getInstance().getConvertUtils();
private static Map<String, String> convertObject(Object value, String key, Set cache) {
//if this type has a registered converted, then get the string and return
if (converter.lookup(value.getClass()) != null) {
String stringValue = converter.convert(value);
Map<String, String> valueMap = new HashMap<String, String>();
valueMap.put(key, stringValue);
return valueMap;
} else {
//otherwise, treat it as a nested bean that needs to be described itself
return recursiveDescribe(value, key, cache);
}
}
}
The challenge (or show stopper) is problem that we have to deal with an object graph instead of a simple tree. A graph may contain cycles and that requires to develop some custom rules or requirements for the stop criteria inside the recursive algorithm.
Have a look at a dead simple bean (a tree structure, getters are assumed but not shown):
public class Node {
private Node parent;
private Node left;
private Node right;
}
and initialize it like this:
root
/ \
A B
Now call a describe on root. A non-recursive call would result in
{parent=null, left=A, right=B}
A recursive call instead would do a
1: describe(root) =>
2: {parent=describe(null), left=describe(A), right=describe(B)} =>
3: {parent=null,
{A.parent=describe(root), A.left=describe(null), A.right= describe(null)}
{B.parent=describe(root), B.left=describe(null), B.right= describe(null)}}
and run into a StackOverflowError because describe is called with objects root, A and B over and over again.
One solution for a custom implementation could be to remember all objects that have been described so far (record those instances in a set, stop if set.contains(bean) return true) and store some kind of link in your result object.
You can simple use from the same commom-beanutils:
Map<String, Object> result = PropertyUtils.describe(obj);
Return the entire set of properties for which the specified bean provides a read method.

Categories