3

Currently I have classes with several nested classes inside is, like:

public class Foo {

  private Bar bar;

  //getter & setter

  public class Bar {

    private Abc abc;

    //getter & setter

    public class Abc {
      @RequiredParam
      private String property;

      //getter & setter
    }
  }

}

I am trying to get the value of the fields but I am having a hard time how to achieve this.

So far I have:

  public static boolean isValid(Object paramClazz) throws Exception {
    List<Class> classes = new ArrayList<>();
    getClasses(classes, paramClazz.getClass());

    for (Class clazz : classes) {
      for (Field field : clazz.getDeclaredFields()) {
        if (field.isAnnotationPresent(RequiredParam.class)) {
          field.setAccessible(true);
          //how to get the value? field.get(paramClazz) doesn't work
        }
      }
    }
    return true;
  }

  private static void getClasses(List<Class> classes, Class<?> clazz) {
    if (clazz.getDeclaredClasses().length > 0) {
      for (Class<?> c : clazz.getDeclaredClasses()) {
        getClasses(classes, c);
      }
    }
    classes.add(clazz);
  }

My goal is to the able to check if the field annotated with @RequiredParam is not null, so I have the method isValid() which will received an object and should be able to check all fields (even the ones inside nested classes) and see if any is missing.

The problem is when I try to call field.get() and I don't know which object I am supposed to pass to this method. Passing the highest level object won't work, because I need somehow to pass only the Abc object to the method.

How can I get the correct object to pass to the field.get() call, considering I can have more or less nested levels in my classes?

dambros
  • 4,252
  • 1
  • 23
  • 39
  • If you could clarify your code, that would help a lot. Make sure to tell us exactly what you're trying to get. I don't see an object named field, or a method named get – z7r1k3 Feb 24 '16 at 17:54
  • http://stackoverflow.com/questions/17485297/how-to-instantiate-inner-class-with-reflection-in-java – Ashish Feb 24 '16 at 17:56
  • The rule is the same as for top-level classes: You need an instance of the class whose (non-static) field value you're trying to read. Meaning, you need an existing instance of `Foo.Bar.Abc`. – VGR Feb 24 '16 at 17:57
  • @VGR the problem is how can I navigate through the top-level class instance and get the specific instance I need programmatically? – dambros Feb 24 '16 at 17:59
  • Unless you are planning to instantiate the inner class yourself, you don't need to. Presumably the instance was created elsewhere, just like any other Java object; and like any other Java object, the program can have any number of references to it. – VGR Feb 24 '16 at 18:01
  • @VGR I still don't get it. Can you give me a code example? For instance I will always have the top most object populated (```Foo``` in the example) in hands, how I am supposed to call ```field.get()``` on ```Foo.Bar.Abc.property```? – dambros Feb 24 '16 at 18:05
  • Having a Foo object doesn't mean you have a Foo.Bar.Abc object. Consider the [java.text.NumberFormat](http://docs.oracle.com/javase/8/docs/api/java/text/NumberFormat.html) class: Just because you have a NumberFormat object doesn't mean you have a [NumberFormat.Field](http://docs.oracle.com/javase/8/docs/api/java/text/NumberFormat.Field.html) object. Inner classes, just like top-level classes, have to be separately instantiated with `new`. – VGR Feb 24 '16 at 18:09
  • @VGR Jackson will be responsible for creating the Foo object after deserializing a json and, sometimes the inner classes will exist and other they wont. In this case, how can validate the existence of the field (the annotation in my case) through reflection? Or I simply can't? – dambros Feb 24 '16 at 18:13

1 Answers1

5

This is an example code that scans all fields of an object recursively until it finds values of all annotated fields:

public static Collection<Object> getAnnotatedValues(final Object root) throws ReflectiveOperationException {
    return getAnnotatedValues(root, new HashSet<>());
}

private static Collection<Object> getAnnotatedValues(final Object root, final Set<Object> inspected)
        throws ReflectiveOperationException {
    final List<Object> annotatedValues = new ArrayList<>();
    if (inspected.contains(root)) { // Prevents stack overflow.
        return Collections.EMPTY_LIST;
    }
    inspected.add(root);
    for (final Field field : gatherFields(root.getClass())) {
        field.setAccessible(true);
        final Object currentValue = field.get(root);
        field.setAccessible(false);
        if (field.isAnnotationPresent(RequiredParam.class)) {
            // Found required value, search finished:
            annotatedValues.add(currentValue);
            if (currentValue != null) {
                inspected.add(currentValue);
            }
        } else if (currentValue != null) {
            // Searching for annotated fields in nested classes:
            annotatedValues.addAll(getAnnotatedValues(currentValue, inspected));
        }
    }
    return annotatedValues;
}

private static Iterable<Field> gatherFields(Class<?> fromClass) {
    // Finds ALL fields, even the ones from super classes.
    final List<Field> fields = new ArrayList<>();
    while (fromClass != null) {
        fields.addAll(Arrays.asList(fromClass.getDeclaredFields()));
        fromClass = fromClass.getSuperclass();
    }
    return fields;
}

You might have implemented something similar, but had trouble getting to the last nested class. This is because Field instance is just a description of a class and (unless the field is static) it needs an actual instance the be able to extract a value. From Field#get(Object) method docs:

If the underlying field is a static field, the obj argument is ignored; it may be null.

Otherwise, the underlying field is an instance field. If the specified obj argument is null, the method throws a NullPointerException. If the specified object is not an instance of the class or interface declaring the underlying field, the method throws an IllegalArgumentException.

If you did not initiate fields in your class structure, you would have a hard time extracting a value - even if you found annotated fields, you would still need an instance of the class to extract them. For example, given these classes:

public class Foo {
    private final Bar bar = new Bar();
    public class Bar {
        private final Abc abc = new Abc();
        public class Abc {
            @RequiredParam private final String property = "Result.";
        }
    }
    @Retention(RetentionPolicy.RUNTIME)
    public static @interface RequiredParam {}
}

...getAnnotatedValues(new Foo()) returns collection containing "Result.". You can easily modify the methods to fit your needs (for example, return true as soon as the first valid annotated field is found or simply return false if the collection is empty/contains nulls).

Czyzby
  • 2,999
  • 1
  • 22
  • 40
  • Amazing job. Way more elegant than what I was trying to do. – dambros Feb 24 '16 at 22:23
  • I am trying to tweak this to return the fields themselves but not sure where to start. Any suggestions? – Half_Duplex Oct 09 '17 at 20:35
  • I don't even remember writing it by now, but changing `annotatedValues.add(currentValue)` to `annotatedValues.add(field)` would probably be a good start. You might need to store references to both `field` and `root` though, a small data class could be a good idea. If you post what you're trying to achieve and what you've already tried as a separate question, I'll try my best to find some time and answer. – Czyzby Oct 10 '17 at 08:18