1

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

BigT
  • 1,413
  • 5
  • 24
  • 52

2 Answers2

0

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.

Jason C
  • 38,729
  • 14
  • 126
  • 182
  • For option 2, how would I call `addUserPreferences()` from the annotation `@UserPreference`? I am not familiar with annotations so I am a little lost. – BigT Apr 14 '14 at 18:37
  • @BigT The way it would work is you would scan all the fields of an object using reflection. You would check each field to see if it *has* that annotation, e.g. by using [`Field.getAnnotation(UserPreference.class)`](http://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Field.html#getAnnotation(java.lang.Class)). If it does then you now have a `String` name for the field, an `Object` value for the field, and you can write that as an individual preference. You still recurse through inner classes and continue looking for these fields as you are doing now, of course. Does that make more sense? – Jason C Apr 14 '14 at 18:42
  • @BigT See the annotation example I've added. – Jason C Apr 14 '14 at 19:00
  • Although that is a good example, wouldn't I run into the same problem? Like I have detailed in my question, my class has classes inside it. So if `Example` had classes within it. If I were to use this example how would I get the object for the value when I go into `Class1` or `Class2`. That is the question I have. Hope I was clear before – BigT Apr 14 '14 at 19:15
  • @BigT No, you would not have that problem. Reflection operates on `Object`, it doesn't matter what type it is. One of your original problems stems from the fact that you're trying to cast `obj` to a subclass but a) it's not (e.g. if `obj` is a `Login`, you *can't* cast it to a `Class1`) and b) even if it were, you're trying to analyze the *values* of those inner class fields. You don't actually need to get the declared inner class list at all (I think you are confused, `getDeclaredClasses()` returns inner class *types*, it's not related to field values). – Jason C Apr 14 '14 at 19:51
  • @BigT See the updated example in the answer. It operates on your `Login` class and produces the output you described. I have added a second `@HasUserPreferences` annotation that can be used to mark objects that should be recursively descended into and scanned. Note that no casting is necessary here. Also in my last comment when I wrote "you're trying to analyze", I meant "you actually want to analyze". Sorry. – Jason C Apr 14 '14 at 19:58
  • I have changed the structure of Login to meet another one of the classes I have to put in. With your code above I can get through `Class1` but `Class2` is forgotten about. – BigT Apr 14 '14 at 20:16
  • @BigT Your edited example in your question doesn't actually use `Class2` (note you declared both of your fields as type `Class1` - you probably didn't mean `Class1 class2;` in `Login`). – Jason C Apr 14 '14 at 20:21
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/50643/discussion-between-bigt-and-jason-c) – BigT Apr 14 '14 at 20:23
0

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.

Community
  • 1
  • 1
BigT
  • 1,413
  • 5
  • 24
  • 52