tags:

views:

270

answers:

5

I have a more complicated issue (than question 'Java map with values limited by key's type parameter' question) for mapping key and value type in a Map. Here it is:

interface AnnotatedFieldValidator<A extends Annotation> {
  void validate(Field f, A annotation, Object target);
  Class<A> getSupportedAnnotationClass();
}

Now, I want to store validators in a map, so that I can write the following method:

validate(Object o) {
  Field[] fields = getAllFields(o.getClass());
  for (Field field: fields) {
    for (Annotation a: field.getAnnotations()) {
      AnnotatedFieldValidator validator = validators.get(a);
      if (validator != null) {
        validator.validate(field, a, target);
      }
    }
  }
}

(type parameters are omitted here, since I do not have the solution). I also want to be able to register my validators:

public void addValidator(AnnotatedFieldValidator<? extends Annotation> v) {
  validators.put(v.getSupportedAnnotatedClass(), v);
}

With this (only) public modifier method, I can ensure the map contains entries for which the key (annotation class) matches the validator's supported annotation class.

Here is a try:

I declare the validators Map like this:

private Map<Class<? extends Annotation>, AnnotatedFieldValidator<? extends Annotation>> validators;

I'm aware I cannot properly link the key and value (link is assumed OK due to only access through addValidator()), so I tried a cast:

for (Annotation a: field.getAnnotations()) {
  AnnotatedFieldValidator<? extends Annotation> validator = validators.get(a);
  if (validator != null) {
    validator.validate(field, validator.getSupportedAnnotationClass().cast(a), target);
  }
}

But this does not work: The method validate(Field, capture#8-of ?, Object) in the type AnnotatedFieldValidator<capture#8-of ?> is not applicable for the arguments (Field, capture#9-of ?, Object).

I can't figure out why this does not work: the AnnotatedFieldValidator has a single type parameter (A), which is used both as the return type of getSupportedAnnotationClass() and as a parameter of validate(); thus, when casting the annotation to supportedAnnotationClass, I should be able to pass it as the parameter to validate(). Why is the result of getSupportedAnnotationClass() considered a different type than the parameter of validate()?

I can solve the validate() method by removing wildcards in the validators declaration and validate() method, but then, of course, addValidator() doesn't compile.

+1  A: 

You can extract a method to get the validator. All access to the validators Map is through type-checked method, and are thus type-safe.

 protected <A extends Annotation> AnnotatedFieldValidator<A> getValidator(A a) {
  // unchecked cast, but isolated in method
  return (AnnotatedFieldValidator<A>) validators.get(a);
 }

 public void validate(Object o) {
  Object target = null;
  Field[] fields = getAllFields(o.getClass());
  for (Field field : fields) {
   for (Annotation a : field.getAnnotations()) {
    AnnotatedFieldValidator<Annotation> validator = getValidator(a);
    if (validator != null) {
     validator.validate(field, a, target);
    }
   }
  }
 }

 // Generic map
 private Map<Class<? extends Annotation>, AnnotatedFieldValidator<? extends Annotation>> validators;

(Removed second suggestion as duplicate.)

flicken
A: 

Generics provide information which only exists at compile-time; at run-time all the information is lost. When compiling code with generics, the compiler will strip out all the generic type information and insert casts as necessary. For example,

List<String> list = new ArrayList<String>();
list.add("test");
String s = list.get(0);

will end up being compiled as

List list = new ArrayList();
list.add("test");
String s = (String) list.get(0);

with the compiler automatically adding the cast in the third line.

It looks to me like you are trying to use generics for run-time type safety. This is not possible. In the line

validator.validate(field, a, target);

there is no way for the compiler to know what subtype of Annotation the validator expects.

I think the best thing to do is to drop the type variable A and declare your interface as follows:

interface AnnotatedFieldValidator {
  void validate(Field f, Annotation annotation, Object target);
  Class<? extends Annotation> getSupportedAnnotationClass();
}

addValidator would then also lose its parameterised type, i.e.

public void addValidator(AnnotatedFieldValidator v) {
  validators.put(v.getSupportedAnnotationClass(), v);
}

The downside of this is that you will have to check that your validators are being passed an annotation of a class that they can validate. This is easiest to do in the code that calls the validator, e.g.

if (validator.getSupportedAnnotationClass().isInstance(a)) {
  validator.validate(field, a, target);
}
else {
  // wrong type of annotation, throw some exception.
}
Pourquoi Litytestdata
Yes, there is no way for the compiler to know. This is why I have a method that enforces the coherence (addValidator()). Now that I assume coherence whith this, I should be able to call Class.cast(Object). Your solution still needs to cast the object, which is what I want to avoid.
Gaetan
A: 

Haa .. I think I figure it out :)

for (Annotation a: field.getAnnotations()) {
  AnnotatedFieldValidator<? extends Annotation> validator = validators.get(a);
  if (validator != null) {
    validator.validate(field, validator.getSupportedAnnotationClass().cast(a), target);
  }
}

EDIT: (changed my viewpoint)

This semantic is correct when you look at it from a runtime perspective but not at COMPILE TIME.

Here the compiler assumes that validator.getSupportedAnnotationClass().cast() will give you a Class<? extends Annotation>.

and when you call:

validator.validate(field, validator.getSupportedAnnotationClass().cast(a), target);

the compiler expects a Class<? extends Annotation> as argument.

Here's the problem. From the compiler point-of-view, these types could be 2 different types at runtime although in this case the semantic doesn't allow this.

bruno conde
I disagree with you: when I call AnnotatedFieldValidator<Deprecated>.getSupportedAnnotationClass(), I get Class<Deprecated>. Therefore, cast() on this class should return an instance of Deprecated.
Gaetan
Please check my updated response.
bruno conde
A: 

Ok, I'll give this a go, since generics are tricky material and I may learn something from the reactions. So, please correct me if I'm wrong.

First, let me put your source code in one block. (renamed AnnotatedFieldValidator into AFV for conciseness)

interface AFV<A extends Annotation>
{
  void validate(Field f, A annotation, Object target);
  Class<A> getSupportedAnnotationClass();
}

private Map<Class<? extends Annotation>, AFV<? extends Annotation>> validators;

public void validate(Object o) {
  Field[] fields = o.getClass().getDeclaredFields();
  for (Field field: fields) {
    for (Annotation a: field.getAnnotations()) {
      AFV<? extends Annotation> validator = validators.get(a.getClass());
      if (validator != null) {
        validator.validate(field, a, o);
      }
    }
  }
}

public void addValidator(AFV<? extends Annotation> v) {
  validators.put(v.getSupportedAnnotationClass(), v);
}

The trouble is that when you iterate over the annotations of a field, the only type the compiler can deduce is Annotation, not a specialization of it. If you now pull the correct AFV from the Map, and you try to call validate on it, there is a type clash on the second parameter, since AFV wants to see the specific subclass of Annotation it is parameterized with, but only gets Annotation, which is too weak.

As you already said yourself, if the only way to add validators is through the addValidator method, then you can internally ensure type safety through it, and rewrite as follows:

interface AFV<A extends Annotation>
{
 void validate(Field f, A annotation, Object target);
 Class<A> getSupportedAnnotationClass();
}

private Map<Class<?>, AFV<?>> validators;

public void validate(Object o)
{
 Field[] fields = o.getClass().getDeclaredFields();
 for (Field field : fields)
 {
  for (Annotation a : field.getAnnotations())
  {
   // raw type to keep compiler happy
   AFV validator = validators.get(a.getClass());
   if (validator != null)
   {
    validator.validate(field, a, o); // statically unsafe
   }
  }
 }
}

public void addValidator(AFV<?> v)
{
 validators.put(v.getSupportedAnnotationClass(), v);
}

Note that the call validator.validate is now statically unsafe, and the compiler will issue a warning. But if you can live with that, this might do.

eljenso
I must agree I'm a bit "extremist", but I would prefer a solution without raw type.
Gaetan
I'm not sure you can. There is simply insufficient type information available, and casting will be necessary if you have a parameterized AFV. Generics here are simply used to "autocast" behind the scenes. Drop generics and you will have to do a type check yourself with Class.isInstance for example.
eljenso
A: 

Thank you all for your answers, it really helped me come to the following solution.

The answer from flicken showed me the way: I have to extract some code into a parameterized method. but instead of extracting validators.get() in a method, I can extract the whole validation process. Doing so, I can use programmatic cast (which I assume OK since I control the coherence of key to values map):

public void validate(Object o) {
  Field[] fields = getFields(o.getClass());
  for (Field field : fields) {
    Annotation[] annotations = field.getAnnotations();
    for (Annotation annotation : annotations) {
      AnnotatedFieldValidator<? extends Annotation> validator = 
          validators.get(annotation.annotationType());
      if (validator != null) {
        doValidate(field, validator, annotation, o);
      }
    }
  }
}

And then, the doValidate() method is as follows:

private <A extends Annotation> void doValidate(Field field, 
      AnnotatedFieldValidator<A> validator, Annotation a, Object o) {
    // I assume this is correct following only access to validators Map
    // through addValidator()
    A annotation = validator.getSupportedAnnotationClass().cast(a);
    try {
        validator.validate(field, annotation, bean, beanName);
    } catch (IllegalAccessException e) {
    }
}

No cast (OK, except Class.cast()...), no unchecked warnings, no raw type, I am happy.

Gaetan
You have turned an implicit cast (when using a raw type) into an explicit one. But I find this solution acceptable. I just don't have the reflex yet of pulling out another generic method out of my sleeve to do some typing magic.
eljenso
Glad you found an acceptable solution. I prefer extracting the getValidator method, because it encapsulates access to the validators Map. However, yours looks more elegant at the top-level validate method.
flicken