Handle validation of cascaded Arrays, Iterables and Maps.
[JSR 303 TCK Result] 62 of 258 (24.03%) Pass with 27 Failures and 5 Errors.

Review at http://gwt-code-reviews.appspot.com/1282801

Review by: rchandia@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9528 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/validation/src/com/google/gwt/sample/validation/shared/Person.java b/samples/validation/src/com/google/gwt/sample/validation/shared/Person.java
index 607bb11..ee882b8 100644
--- a/samples/validation/src/com/google/gwt/sample/validation/shared/Person.java
+++ b/samples/validation/src/com/google/gwt/sample/validation/shared/Person.java
@@ -17,6 +17,8 @@
 
 import com.google.gwt.user.client.rpc.IsSerializable;
 
+import java.util.Map;
+
 import javax.validation.Valid;
 import javax.validation.constraints.Max;
 import javax.validation.constraints.NotNull;
@@ -31,6 +33,9 @@
   @Valid
   private Address address;
 
+  @Valid
+  private Map<String, Address> otherAddresses;
+
   @NotNull
   @Size(min = 4, message = "{custom.name.size.message}")
   private String name;
diff --git a/user/src/com/google/gwt/validation/client/impl/GwtValidationContext.java b/user/src/com/google/gwt/validation/client/impl/GwtValidationContext.java
index 38d6ccd..0ff46f5 100644
--- a/user/src/com/google/gwt/validation/client/impl/GwtValidationContext.java
+++ b/user/src/com/google/gwt/validation/client/impl/GwtValidationContext.java
@@ -52,7 +52,33 @@
   public GwtValidationContext<T> append(String name) {
     GwtValidationContext<T> temp = new GwtValidationContext<T>(rootBean,
         beanDescriptor, messageInterpolator);
-    temp.path = temp.path.append(name);
+    temp.path = path.append(name);
+    return temp;
+  }
+
+  /**
+   * Append a indexed node to the path.
+   *
+   * @param name
+   * @return the new GwtValidationContext.
+   */
+  public GwtValidationContext<T> appendIndex(String name, int index) {
+    GwtValidationContext<T> temp = new GwtValidationContext<T>(rootBean,
+        beanDescriptor, messageInterpolator);
+    temp.path = path.appendIndex(name, index);
+    return temp;
+  }
+
+  /**
+   * Append a keyed node to the path.
+   *
+   * @param name
+   * @return the new GwtValidationContext.
+   */
+  public GwtValidationContext<T> appendKey(String name, String key) {
+    GwtValidationContext<T> temp = new GwtValidationContext<T>(rootBean,
+        beanDescriptor, messageInterpolator);
+    temp.path = path.appendKey(name, key);
     return temp;
   }
 
diff --git a/user/src/com/google/gwt/validation/client/impl/PathImpl.java b/user/src/com/google/gwt/validation/client/impl/PathImpl.java
index 8591073..f481564 100644
--- a/user/src/com/google/gwt/validation/client/impl/PathImpl.java
+++ b/user/src/com/google/gwt/validation/client/impl/PathImpl.java
@@ -40,7 +40,9 @@
   }
 
   private PathImpl(PathImpl originalPath, Node node) {
-    nodes.addAll(originalPath.nodes);
+    if (!originalPath.isRoot()) {
+      nodes.addAll(originalPath.nodes);
+    }
     nodes.add(node);
   }
 
@@ -111,4 +113,8 @@
     }
     return sb.toString();
   }
+
+  private boolean isRoot() {
+    return nodes.size() == 1 && nodes.get(0) == NodeImpl.ROOT_NODE;
+  }
 }
diff --git a/user/src/com/google/gwt/validation/rebind/AbstractCreator.java b/user/src/com/google/gwt/validation/rebind/AbstractCreator.java
index 1fb9e02..3e02b60 100644
--- a/user/src/com/google/gwt/validation/rebind/AbstractCreator.java
+++ b/user/src/com/google/gwt/validation/rebind/AbstractCreator.java
@@ -20,6 +20,7 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.JPackage;
+import com.google.gwt.core.ext.typeinfo.JType;
 import com.google.gwt.user.rebind.AbstractSourceCreator;
 import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
 import com.google.gwt.user.rebind.SourceWriter;
@@ -68,6 +69,11 @@
     return BeanHelper.createBeanHelper(clazz, logger, context);
   }
 
+  protected BeanHelper createBeanHelper(JType jType)
+      throws UnableToCompleteException {
+    return BeanHelper.createBeanHelper(jType, logger, context);
+  }
+
   protected final String getPackage() {
     JPackage serviceIntfPkg = validatorType.getPackage();
     String packageName = serviceIntfPkg == null ? "" : serviceIntfPkg.getName();
@@ -85,7 +91,7 @@
     sw.println(" = ");
     sw.indent();
     sw.indent();
-  
+
     // GWT.create(MyBeanValidator.class);
     sw.println("GWT.create(" + bean.getFullyQualifiedValidatorName()
         + ".class);");
diff --git a/user/src/com/google/gwt/validation/rebind/BeanHelper.java b/user/src/com/google/gwt/validation/rebind/BeanHelper.java
index e3c6d84..f87a6e9 100644
--- a/user/src/com/google/gwt/validation/rebind/BeanHelper.java
+++ b/user/src/com/google/gwt/validation/rebind/BeanHelper.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2010 Google Inc.
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- * 
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -17,7 +17,9 @@
 
 import com.google.gwt.core.ext.GeneratorContext;
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JType;
 import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
 import com.google.gwt.user.rebind.SourceWriter;
 import com.google.gwt.validation.client.impl.GwtSpecificValidator;
@@ -41,7 +43,7 @@
 
   // stash the map in a ThreadLocal, since each GWT module lives in its own
   // thread in DevMode
-  private static final ThreadLocal<Map<JClassType, BeanHelper>> threadLocalHelperMap = 
+  private static final ThreadLocal<Map<JClassType, BeanHelper>> threadLocalHelperMap =
       new ThreadLocal<Map<JClassType, BeanHelper>>() {
     @Override
     protected synchronized Map<JClassType, BeanHelper> initialValue() {
@@ -63,6 +65,18 @@
     return helper;
   }
 
+  protected static BeanHelper createBeanHelper(JType jType, TreeLogger logger,
+      GeneratorContext context) throws UnableToCompleteException {
+    try {
+      Class<?> clazz = Class.forName(jType.getQualifiedSourceName());
+      return createBeanHelper(clazz, logger, context);
+    } catch (ClassNotFoundException e) {
+      logger.log(TreeLogger.ERROR, "Unable to create BeanHelper for " + jType,
+          e);
+      throw new UnableToCompleteException();
+    }
+  }
+
   protected static boolean isClassConstrained(Class<?> clazz) {
     return serverSideValidor.getConstraintsForClass(clazz).isBeanConstrained();
   }
diff --git a/user/src/com/google/gwt/validation/rebind/GwtSpecificValidatorCreator.java b/user/src/com/google/gwt/validation/rebind/GwtSpecificValidatorCreator.java
index 6e2c00c..409e546 100644
--- a/user/src/com/google/gwt/validation/rebind/GwtSpecificValidatorCreator.java
+++ b/user/src/com/google/gwt/validation/rebind/GwtSpecificValidatorCreator.java
@@ -22,9 +22,11 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JArrayType;
 import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.JField;
 import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JParameterizedType;
 import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
 import com.google.gwt.core.ext.typeinfo.JType;
 import com.google.gwt.core.ext.typeinfo.NotFoundException;
@@ -50,11 +52,13 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 
 import javax.validation.ConstraintValidator;
 import javax.validation.ConstraintViolation;
 import javax.validation.Payload;
+import javax.validation.Valid;
 import javax.validation.ValidationException;
 import javax.validation.metadata.BeanDescriptor;
 import javax.validation.metadata.ConstraintDescriptor;
@@ -198,14 +202,6 @@
     this.beanHelper = beanHelper;
   }
 
-  public JType getElementType(PropertyDescriptor p, boolean useField) {
-    if (useField) {
-      return beanType.findField(p.getPropertyName()).getType();
-    } else {
-      return beanType.findMethod(this.asGetter(p), NO_ARGS).getReturnType();
-    }
-  }
-
   @Override
   protected void compose(ClassSourceFileComposerFactory composerFactory) {
     addImports(composerFactory, GWT.class, GwtBeanDescriptor.class,
@@ -280,24 +276,51 @@
   }
 
   private Annotation getAnnotation(PropertyDescriptor p, boolean useField,
-      ConstraintDescriptor<?> constraint) {
-    Class<? extends Annotation> expectedAnnotaionClass =
-        ((Annotation) constraint.getAnnotation()).annotationType();
+      Class<? extends Annotation> expectedAnnotationClass) {
     Annotation annotation = null;
     if (useField) {
       JField field = beanType.findField(p.getPropertyName());
       if (field.getEnclosingType().equals(beanType)) {
-        annotation = field.getAnnotation(expectedAnnotaionClass);
+        annotation = field.getAnnotation(expectedAnnotationClass);
       }
     } else {
       JMethod method = beanType.findMethod(asGetter(p), NO_ARGS);
       if (method.getEnclosingType().equals(beanType)) {
-        annotation = method.getAnnotation(expectedAnnotaionClass);
+        annotation = method.getAnnotation(expectedAnnotationClass);
       }
     }
     return annotation;
   }
 
+  private Annotation getAnnotation(PropertyDescriptor p, boolean useField,
+      ConstraintDescriptor<?> constraint) {
+    Class<? extends Annotation> expectedAnnotationClass =
+        ((Annotation) constraint.getAnnotation()).annotationType();
+    return getAnnotation(p, useField, expectedAnnotationClass);
+  }
+
+
+  private JType getAssociationType(PropertyDescriptor p, boolean useField) {
+    JType type = getElementType(p, useField);
+    JArrayType jArray = type.isArray();
+    if (jArray != null) {
+      // TODO(nchalko) check stuff
+      return jArray.getComponentType();
+    }
+    JParameterizedType pType = type.isParameterized();
+    JClassType[] typeArgs = pType.getTypeArgs();
+    // it is either a Iterable or a Map use the last type arg.
+    return typeArgs[typeArgs.length - 1];
+  }
+
+  private JType getElementType(PropertyDescriptor p, boolean useField) {
+    if (useField) {
+      return beanType.findField(p.getPropertyName()).getType();
+    } else {
+      return beanType.findMethod(this.asGetter(p), NO_ARGS).getReturnType();
+    }
+  }
+
   /**
    * @param elementType
    * @return
@@ -328,6 +351,10 @@
     }
   }
 
+  private boolean hasValid(PropertyDescriptor p, boolean useField) {
+    return getAnnotation(p, useField, Valid.class) != null;
+  }
+
   private boolean isIterableOrMap(Class<?> elementClass) {
     // TODO(nchalko) handle iterables everywhere this is called.
     return elementClass.isArray()
@@ -514,8 +541,10 @@
         count++; // index starts at zero.
       }
       writePropertyDescriptor(sw, p);
-      if (p.isCascaded() && !isIterableOrMap(p.getElementClass())) {
-        beansToValidate.add(createBeanHelper(p.getElementClass()));
+      if (p.isCascaded()) {
+        beansToValidate.add(isIterableOrMap(p.getElementClass())
+            ? createBeanHelper(getAssociationType(p, true))
+            : createBeanHelper(p.getElementClass()));
       }
     }
 
@@ -699,7 +728,8 @@
     sw.outdent();
   }
 
-  private void writePropertyValidators(SourceWriter sw) {
+  private void writePropertyValidators(SourceWriter sw)
+      throws UnableToCompleteException {
     for (PropertyDescriptor p :
         beanHelper.getBeanDescriptor().getConstrainedProperties()) {
       if (hasField(p)) {
@@ -779,8 +809,6 @@
       writeValidateInheritance(sw, clazz, Stage.OBJECT, null);
 
       // all reachable and cascadable associations
-
-      beanType.getFields();
     }
     // return violations;
     sw.println("return violations;");
@@ -801,9 +829,9 @@
 
     } else {
       // TODO(nchalko) handle constraint.isReportAsSingleViolation()
-      // validate(context, violations, object, value, new MyValidator(),
+      // validate(myContext, violations, object, value, new MyValidator(),
       // constraintDescriptor, groups);
-      sw.print("validate(context, violations, object, value, ");
+      sw.print("validate(myContext, violations, object, value, ");
       sw.print("new "); // new one each time because validators are not
                         // thread safe
                         // TODO(nchalko) use ConstraintValidatorFactory
@@ -904,6 +932,99 @@
     }
   }
 
+  private void writeValidateIterable(SourceWriter sw, PropertyDescriptor p,
+      JType associationType, BeanHelper helper) {
+    // int i = 0;
+    sw.println("int i = 0;");
+
+    // for (BeanType instance : value) {
+    sw.print("for(");
+    sw.print(associationType.getQualifiedSourceName());
+    sw.println(" instance : value) {");
+    sw.indent();
+
+    // if(instance != null ) {
+    sw.println(" if(instance != null ) {");
+    sw.indent();
+
+    // violations.addAll(
+    sw.println("violations.addAll(");
+    sw.indent();
+    sw.indent();
+
+    // myGwtValidator.validate(
+    sw.print(helper.getValidatorInstanceName());
+    sw.println(".validate(");
+    sw.indent();
+    sw.indent();
+
+    // context.appendIndex("myProperty",i++),
+    sw.print("context.appendIndex(\"");
+    sw.print(p.getPropertyName());
+    sw.println("\",i++),");
+
+    // instance, groups));
+    sw.println("instance, groups));");
+    sw.outdent();
+    sw.outdent();
+    sw.outdent();
+    sw.outdent();
+
+    // }
+    sw.outdent();
+    sw.println("}");
+
+    // }
+    sw.outdent();
+    sw.println("}");
+  }
+
+  private void writeValidateMap(SourceWriter sw, PropertyDescriptor p,
+      JType associationType, BeanHelper helper) {
+    // for (Entry<?, Type> entry : value.entrySet()) {
+    sw.print("for(");
+    sw.print(Entry.class.getCanonicalName());
+    sw.print("<?, ");
+    sw.print(associationType.getQualifiedSourceName());
+    sw.println("> entry : value.entrySet()) {");
+    sw.indent();
+
+    // if(entry.getValue() != null ) {
+    sw.println(" if(entry.getValue() != null ) {");
+    sw.indent();
+
+    // violations.addAll(
+    sw.println("violations.addAll(");
+    sw.indent();
+    sw.indent();
+
+    // myGwtValidator.validate(
+    sw.print(helper.getValidatorInstanceName());
+    sw.println(".validate(");
+    sw.indent();
+    sw.indent();
+
+    // context.appendKey("myProperty",String.valueOf(entry.getKey())),
+    sw.print("context.appendKey(\"");
+    sw.print(p.getPropertyName());
+    sw.println("\",String.valueOf(entry.getKey())),");
+
+    // entry.getValue(), groups));
+    sw.println("entry.getValue(), groups));");
+    sw.outdent();
+    sw.outdent();
+    sw.outdent();
+    sw.outdent();
+
+    // }
+    sw.outdent();
+    sw.println("}");
+
+    // }
+    sw.outdent();
+    sw.println("}");
+  }
+
   private void writeValidateProperty(SourceWriter sw) {
     // public <T> Set<ConstraintViolation<T>> validate(
     sw.println("public <T> Set<ConstraintViolation<T>> validateProperty(");
@@ -1023,7 +1144,7 @@
   }
 
   private void writeValidatePropertyMethod(SourceWriter sw,
-      PropertyDescriptor p, boolean useField) {
+      PropertyDescriptor p, boolean useField) throws UnableToCompleteException {
     Class<?> elementClass = p.getElementClass();
     JType elementType = getElementType(p, useField);
 
@@ -1035,37 +1156,55 @@
       sw.print(validateMethodGetterName(p));
     }
     sw.println("(");
-    // GwtValidationContext<T> context, Set<ConstraintViolation<T>> violations,
-    // BeanType object, <Type> value, Class<?>... groups) {
     sw.indent();
     sw.indent();
-    sw.println("GwtValidationContext<T> context,");
-    sw.println("Set<ConstraintViolation<T>> violations,");
+
+    // final GwtValidationContext<T> context,
+    sw.println("final GwtValidationContext<T> context,");
+
+    // final Set<ConstraintViolation<T>> violations,
+    sw.println("final Set<ConstraintViolation<T>> violations,");
+
+    // final BeanType object, <Type> value,
     sw.println(beanHelper.getTypeCanonicalName() + " object,");
-    sw.print(elementType.getQualifiedSourceName());
+
+    // Class<?>... groups) {
+    sw.print("final ");
+    sw.print(elementType.getParameterizedQualifiedSourceName());
     sw.println(" value,");
     sw.println("Class<?>... groups) {");
     sw.outdent();
 
-    // TODO(nchalko) handle the other path types.
     // context = context.append("myProperty");
-    sw.print("context = context.append(\"");
+    sw.print("final GwtValidationContext<T> myContext = context.append(\"");
     sw.print(p.getPropertyName());
     sw.println("\");");
 
     // TODO(nchalko) move this out of here to the Validate method
-    if (p.isCascaded() && !isIterableOrMap(elementClass)) {
-
-      BeanHelper helper = createBeanHelper(elementClass);
+    if (p.isCascaded()) {
 
       // if(value != null) {
       sw.println("if(value != null) {");
       sw.indent();
 
-      // violations.addAll(myGwtValidator.validate(context, value, groups));
-      sw.print("violations.addAll(");
-      sw.print(helper.getValidatorInstanceName());
-      sw.println(".validate(context, value, groups));");
+      if (isIterableOrMap(elementClass)) {
+        if (hasValid(p, useField)) {
+          JType associationType = getAssociationType(p, useField);
+          BeanHelper helper = createBeanHelper(associationType);
+          // TODO(nchalko) assume iterable for now
+          if (Map.class.isAssignableFrom(elementClass)) {
+            writeValidateMap(sw, p, associationType, helper);
+          } else {
+            writeValidateIterable(sw, p, associationType, helper);
+          }
+        }
+      } else {
+        BeanHelper helper = createBeanHelper(elementClass);
+        // violations.addAll(myGwtValidator.validate(context, value, groups));
+        sw.print("violations.addAll(");
+        sw.print(helper.getValidatorInstanceName());
+        sw.println(".validate(myContext, value, groups));");
+      }
 
       // }
       sw.outdent();
diff --git a/user/test/org/hibernate/jsr303/tck/tests/validation/ValidatePropertyGwtTest.java b/user/test/org/hibernate/jsr303/tck/tests/validation/ValidatePropertyGwtTest.java
index 535d08b..39fa78d 100644
--- a/user/test/org/hibernate/jsr303/tck/tests/validation/ValidatePropertyGwtTest.java
+++ b/user/test/org/hibernate/jsr303/tck/tests/validation/ValidatePropertyGwtTest.java
@@ -62,6 +62,7 @@
     delegate.testValidatePropertyWithNullProperty();
   }
 
+  @Failing(issue = 5804)
   public void testValidIsNotHonoredValidateProperty() {
     delegate.testValidIsNotHonoredValidateProperty();
   }