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.