Question

I would like to pass in a generic object into my method and have it get the property name, type, and value.

Here is my class

public class Login {

    public String token;   
    public String customerid;
    public Class1 class1;
    public Class2 class2;

    public class Class1 {
        public Class3 class3;
        public String string1;

        public class Class3 {
                public int int1;
                public String string2;
                public String string3;
        }
    }

    public class Class2 {
        public int int1;
        public String string2;
        public String string3;
    }
}

I would like the output to look like this

User Preferences customerid - class java.lang.String - 586969
User Preferences token - class java.lang.String - token1
User Preferences string1 - class java.lang.String - string1Value
User Preferences string2 - class java.lang.String - string2Value
User Preferences string3 - class java.lang.String - string3Value

The code I have right now gives me issues. Here is the code:

    try {
        // Loop over all the fields and add the info for each field
        for (Field field : obj.getClass().getDeclaredFields()) {
            if(!field.isSynthetic()){
                field.setAccessible(true);
                System.out.println("User Preferences " + field.getName() + " - " + field.getType() + " - " + field.get(obj));
            }
        }

        // For any internal classes, recursively call this method and add the results
        // (which will in turn do this for all of that subclass's subclasses)
        for (Class<?> subClass : obj.getClass().getDeclaredClasses()) {
            Object subObject = subClass.cast(obj); // ISSUE
            addUserPreferences(subObject, prefs);
        }
    }catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }catch(ClassCastException e) {
        e.printStackTrace();
    }

Getting the subObject, in this case Class1 or Class2, and passing it to the method is what Im having an issue with. I have tried with a class instead of an object but then I can't get the object from the class.

Is there anyway to cast the object I pass in to the subclass?

Thanks

Was it helpful?

Solution 2

I have figured out a simplistic way to do this. Anyone who has suggestions to make this better or has issues with the code please comment. The code below does work for me

    try {
        Class<?> objClass = obj.getClass();
        List<Object> subObjectList = new ArrayList<Object>();
        // Loop over all the fields and add the info for each field
        for (Field field: objClass.getDeclaredFields()) {
            if(!field.isSynthetic()){
                if(isWrapperType(field.getType())){
                    System.out.println("Name: " + field.getName() + " Value: " + field.get(obj));
                }
                else{
                    if(field.getType().isArray()){
                        Object[] fieldArray = (Object[]) field.get(obj);
                        for(int i = 0; i < fieldArray.length; i++){
                            subObjectList.add(fieldArray[i]);
                        }
                    }
                    else{
                        subObjectList.add(field.get(obj));
                    }
                }
            }
        }

        for(Object subObj: subObjectList){
            printObjectFields(subObj);
        }
    }catch(IllegalArgumentException e){
        // TODO Auto-generated catch block
        e.getLocalizedMessage();
    } catch (IllegalAccessException e) {
        // TODO Auto-generated catch block
        e.getLocalizedMessage();
    }

The isWrapperType come from code I found in this stack overflow question. All i did was add String and int to the set.

OTHER TIPS

You have a few options:


One option is to consider defining some interface that defines an object that provides user preferences, e.g.:

interface UserPreferenceProvider {
    Map<String,Object> getUserPrefences();
}

Then you can make your classes implement that interface, e.g.:

public class Login implements UserPreferenceProvider {
    ...
    public class Class1 implements UserPreferenceProvider {
        ...
        public class Class2 implements UserPreferenceProvider {
            ...
        }
    }
}

Where their getUserPreferences() implementations return the preferences to write.

Then you can change addUserPreferences() to take a UserPreferenceProvider, and when you are traversing fields, check if you find a UserPreferenceProvider and, if so, cast it to that and pass it off to addUserPreferences().

This would more accurately represent your intentions, as well. I believe the fundamental issue here is you have these arbitrary objects that you're trying to work with, and while conceptually they have something in common, your code is not representing that concept; I know that's a bit vague but by not having your code reflect that, you are now faced with the awkward task of having to find a way to force your arbitrary objects to be treated in a common way.


A second option could be to create a custom annotation, e.g. @UserPreference, and use that to mark the fields you want to write. Then you can traverse the fields and when you find a field with this annotation, add it's single key/value to the user preferences (that is, operate on the fields themselves, instead of passing entire container classes to addUserPreferences()).

This may or may not be more appropriate than the first option for your design. It has the advantage of not forcing you to use those interfaces, and not having to write code to pack data into maps or whatever for getUserPreferences(); it also gives you finer grained control over which properties get exported -- essentially this shifts your focus from the objects to the individual properties themselves. It would be a very clean approach with minimal code.

A possible way to make this more convenient if you already have bean-style getters is to use e.g. Apache BeanUtils to get the values instead of rolling your own; but for your situation it's a pretty basic use of reflection that may not be worth an additional dependency.


Here is an example of getting names and values of the fields of an object tagged with a custom annotation. A second annotation is used to mark fields that contain objects that should be recursively descended into and scanned. It's very straightforward:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;

// @UserPreference marks a field that should be exported.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface UserPreference {
}

// @HasUserPreferences marks a field that should be recursively scanned.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface HasUserPreferences {
}


// Your example Login class, with added annotations.
class Login {

    @UserPreference public String token;      // <= a preference
    @UserPreference public String customerid; // <= a preference
    @HasUserPreferences public Class1 class1; // <= contains preferences

    public class Class1 {
        @HasUserPreferences public Class2 class2; // <= contains preferences
        @UserPreference public String string1;    // <= a preference

        public class Class2 {
                public int int1; // <= not a preference
                @UserPreference public String string2; // <= a preference
                @UserPreference public String string3; // <= a preference
        }
    }

    // Construct example:
    public Login () {
        token = "token1";
        customerid = "586969";
        class1 = new Class1();
        class1.string1 = "string1Value";
        class1.class2 = class1.new Class2();
        class1.class2.string2 = "string2Value";
        class1.class2.string3 = "string3Value";
    }

}


public class ValueScanExample {

    // Recursively print user preferences. 
    // Fields tagged with @UserPreference are printed.    
    // Fields tagged with @HasUserPreferences are recursively scanned.
    static void printUserPreferences (Object obj) throws Exception {
        for (Field field : obj.getClass().getDeclaredFields()) { 
            // Is it a @UserPreference?
            if (field.getAnnotation(UserPreference.class) != null) {
                String name = field.getName();
                Class<?> type = field.getType();
                Object value = field.get(obj);
                System.out.println(name + " - " + type + " - " + value);
            }
            // Is it tagged with @HasUserPreferences?
            if (field.getAnnotation(HasUserPreferences.class) != null) {
                printUserPreferences(field.get(obj)); // <= note: no casts
            }
        }
    }

    public static void main (String[] args) throws Exception {
        printUserPreferences(new Login());
    }

}

The output is:

token - class java.lang.String - token1
customerid - class java.lang.String - 586969
string2 - class java.lang.String - string2Value
string3 - class java.lang.String - string3Value
string1 - class java.lang.String - string1Value

Note that "int1" is not present in the output, as it is not tagged. You can run the example on ideone.

The original basic annotation example can still be found here.

You can do all sorts of fun things with annotations, by the way, e.g. add optional parameters that let you override the field name in the preferences, add a parameter that lets you specify a custom object -> user preference string converter, etc.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top