1. Michael Ludwig
  2. entreri

Commits

Michael Ludwig  committed 78cf4ec

Implement validation annotations.

  • Participants
  • Parent commits aa75dab
  • Branches default

Comments (0)

Files changed (12)

File src/main/java/com/lhkbob/entreri/NotNull.java

View file
+package com.lhkbob.entreri;
+
+import java.lang.annotation.*;
+
+/**
+ * NotNull is a validation annotation that can be added to the setter method or setter parameters for a
+ * property to declare that the references cannot be null. The proxy implementation will generate code to
+ * check for null and throw a NullPointerException as necessary. Applying NotNull to the entire method
+ * declares all parameters to be non-null and checks will be generated for each.
+ * <p/>
+ * This should not be used with primitive typed data because they cannot be null. Some property
+ * implementations may implicitly enforce a non-null rule already in which case this is unnecessary.
+ * <p/>
+ * This annotation is ignored when placed on the getter for a property
+ *
+ * @author Michael Ludwig
+ */
+@Documented
+@Target({ ElementType.PARAMETER, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface NotNull {
+}

File src/main/java/com/lhkbob/entreri/Validate.java

View file
+package com.lhkbob.entreri;
+
+import java.lang.annotation.*;
+
+/**
+ * Validate is a generic validation annotation that lets you specify a Java-like snippet to be inserted into
+ * the generated proxy to perform validation on a setter method.  Unlike {@link NotNull} and {@link Within},
+ * this annotation cannot be placed on setter parameters. Because of the flexibility this offers, Validate
+ * allows you to perform validation between different properties of the same component (such as ensuring a
+ * minimum is less than a maximum value).
+ * <p/>
+ * The Java-like validation snippet must evaluate to a boolean expression. When that expression is true, the
+ * inputs are considered valid; otherwise, the proxy will throw an IllegalArgumentException. The snippet must
+ * use valid Java syntax, except that the symbols {@code $1 - $n} should be used to refer to the first through
+ * nth setter parameters. Those symbols will be replaced with the generated parameter name at compile time.
+ * Additionally, the syntax {@code $propertyName} will be replaced with {@code getPropertyName()} to refer to
+ * properties on a component. Validation is performed before the property values are assigned, so referencing
+ * a property name with this syntax in the setter method for that property will produce the old value.
+ * <p/>
+ * After this syntax replacement, any other errors may produce Java syntax errors when the generated source is
+ * compiled.
+ * <p/>
+ * This annotation is ignored if placed on the getter property method.
+ *
+ * @author Michael Ludwig
+ */
+@Documented
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Validate {
+    /**
+     * @return Get the Java-like validation snippet that represents a boolean expression evaluating to true
+     *         when input parameters are valid
+     */
+    String value();
+
+    /**
+     * @return Optional error message to include in the thrown exception
+     */
+    String errorMsg() default "";
+}

File src/main/java/com/lhkbob/entreri/Within.java

View file
+package com.lhkbob.entreri;
+
+import java.lang.annotation.*;
+
+/**
+ * Within is a validation annotation for numeric properties to ensure values fall within a specific range. For
+ * simplicity, the annotation expects minimum and maximum values in doubles but it works with any primitive
+ * type that has {@code &lt;} and {@code &gt;} defined. Specifying only one half of the range is valid and
+ * produces an open-ended range. Inputs that fall outside the declared range will cause an
+ * IllegalArgumentException to be thrown.
+ * <p/>
+ * Compilation failures will result if applied to non-primitive parameters. When applied to a setter method,
+ * the range operates on the first parameter regardless of the number of method inputs. When applied to a
+ * specific parameter, the proxy generates code for that parameter. In this way, multi-parameter setters can
+ * have Within applied to each parameter.
+ * <p/>
+ * This annotation is ignored when placed on the property getter.
+ *
+ * @author Michael Ludwig
+ */
+@Documented
+@Target({ ElementType.PARAMETER, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Within {
+    /**
+     * @return Minimum bound of input, inclusive, or leave unspecified for unbounded on the low side of the
+     *         range
+     */
+    double min() default Double.NEGATIVE_INFINITY;
+
+    /**
+     * @return Maximum bound of input, inclusive, or leave unspecified for unbounded on the high side of the
+     *         range
+     */
+    double max() default Double.POSITIVE_INFINITY;
+}

File src/main/java/com/lhkbob/entreri/impl/ComponentFactoryProvider.java

View file
 package com.lhkbob.entreri.impl;
 
 import com.lhkbob.entreri.Component;
+import com.lhkbob.entreri.NotNull;
+import com.lhkbob.entreri.Validate;
+import com.lhkbob.entreri.Within;
 
 import javax.lang.model.SourceVersion;
+import java.lang.annotation.Annotation;
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
 import java.security.MessageDigest;
         }
         sb.append(") {\n");
 
+        // perform any validation, first from the method annotations
+        for (Annotation validate : spec.getValidationAnnotations(name)) {
+            if (validate instanceof NotNull) {
+                // add not null checks for every parameter
+                for (PropertyDeclaration p : params) {
+                    int idx = properties.indexOf(p);
+                    addNotNullValidationForParameter(idx, p, sb);
+                }
+            } else if (validate instanceof Within) {
+                // when added to the method, this only affects the first property
+                addWithinValidationForParameter(0, params.get(0), (Within) validate, sb);
+            } else {
+                // assume its a Validate annotation
+                Validate v = (Validate) validate;
+                String javaValidation = filterValidationSnippet(spec, params, v.value());
+                sb.append("\t\tif(!(").append(javaValidation)
+                  .append(")) {\n\t\t\tthrow new IllegalArgumentException(\"").append(v.errorMsg())
+                  .append("\");\n\t\t}\n");
+            }
+        }
+        // then parameter validation
+        for (PropertyDeclaration p : params) {
+            int idx = properties.indexOf(p);
+            for (Annotation validate : p.getValidationAnnotations()) {
+                if (validate instanceof NotNull) {
+                    addNotNullValidationForParameter(idx, p, sb);
+                } else {
+                    // assume within since Validate is not allowed on parameters
+                    addWithinValidationForParameter(idx, p, (Within) validate, sb);
+                }
+            }
+        }
+
         // implement the body
         for (PropertyDeclaration p : params) {
             int idx = properties.indexOf(p);
         }
         sb.append("\t}\n");
     }
+
+    private static String filterValidationSnippet(ComponentSpecification spec,
+                                                  List<PropertyDeclaration> params, String snippet) {
+        // first filter $n parameter names
+        List<? extends PropertyDeclaration> allProps = spec.getProperties();
+        for (int i = 0; i < params.size(); i++) {
+            int varIndex = allProps.indexOf(params.get(i));
+            snippet = snippet.replace("$" + (i + 1), SETTER_PARAM_PREFIX + varIndex);
+        }
+        // filter property names
+        for (PropertyDeclaration p : allProps) {
+            snippet = snippet.replace("$" + p.getName(), p.getGetterMethod() + "()");
+        }
+        return snippet;
+    }
+
+    private static void addWithinValidationForParameter(int param, PropertyDeclaration p, Within w,
+                                                        StringBuilder sb) {
+
+        if (Double.isInfinite(w.max())) {
+            // less than min check only
+            sb.append("\t\tif (").append(SETTER_PARAM_PREFIX).append(param).append(" < ").append(w.min())
+              .append(") {\n\t\t\tthrow new IllegalArgumentException(\"").append(p.getName())
+              .append(" must be greater than ").append(w.min()).append("\");\n\t\t}\n");
+        } else if (Double.isInfinite(w.min())) {
+            // greater than max check only
+            sb.append("\t\tif (").append(SETTER_PARAM_PREFIX).append(param).append(" > ").append(w.max())
+              .append(") {\n\t\t\tthrow new IllegalArgumentException(\"").append(p.getName())
+              .append(" must be less than ").append(w.max()).append("\");\n\t\t}\n");
+        } else {
+            // both
+            sb.append("\t\tif (").append(SETTER_PARAM_PREFIX).append(param).append(" < ").append(w.min())
+              .append(" || ").append(SETTER_PARAM_PREFIX).append(param).append(" > ").append(w.max())
+              .append(") {\n\t\t\tthrow new IllegalArgumentException(\"").append(p.getName())
+              .append(" must be in [").append(w.min()).append(", ").append(w.max()).append("]\");\n\t\t}\n");
+        }
+    }
+
+    private static void addNotNullValidationForParameter(int param, PropertyDeclaration p, StringBuilder sb) {
+        sb.append("\t\tif (").append(SETTER_PARAM_PREFIX).append(param).append(" == null) {\n")
+          .append("\t\t\tthrow new NullPointerException(\"").append(p.getName())
+          .append(" cannot be null\");\n\t\t}\n");
+    }
 }

File src/main/java/com/lhkbob/entreri/impl/ComponentSpecification.java

View file
 
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.lang.model.element.TypeElement;
+import java.lang.annotation.Annotation;
 import java.util.List;
 
 /**
      */
     public List<? extends PropertyDeclaration> getProperties();
 
+    /**
+     * Get all validation annotations applied directly to the setter with the given method name. Included
+     * annotations will be instances of {@link com.lhkbob.entreri.NotNull}, {@link com.lhkbob.entreri.Within},
+     * and {@link com.lhkbob.entreri.Validate}.
+     *
+     * @param setterName The setter method name to look up
+     *
+     * @return All annotations present on the given method
+     */
+    public List<Annotation> getValidationAnnotations(String setterName);
+
     public static final class Factory {
         private Factory() {
         }

File src/main/java/com/lhkbob/entreri/impl/MirrorComponentSpecification.java

View file
  */
 package com.lhkbob.entreri.impl;
 
-import com.lhkbob.entreri.Component;
-import com.lhkbob.entreri.IllegalComponentDefinitionException;
-import com.lhkbob.entreri.Ownable;
-import com.lhkbob.entreri.Owner;
+import com.lhkbob.entreri.*;
 import com.lhkbob.entreri.property.*;
 
 import javax.annotation.processing.Filer;
 import javax.tools.FileObject;
 import javax.tools.StandardLocation;
 import java.io.IOException;
+import java.lang.annotation.Annotation;
 import java.util.*;
 
 /**
     private final String typeName;
     private final String packageName;
     private final List<MirrorPropertyDeclaration> properties;
+    private final Map<String, List<Annotation>> setterValidationAnnotations;
 
     public MirrorComponentSpecification(TypeElement type, Types tu, Elements eu, Filer io) {
         TypeMirror baseComponentType = eu.getTypeElement(Component.class.getCanonicalName()).asType();
         Map<String, ExecutableElement> getters = new HashMap<>();
         Map<String, ExecutableElement> setters = new HashMap<>();
         Map<String, Integer> setterParameters = new HashMap<>();
+        setterValidationAnnotations = new HashMap<>();
 
         for (ExecutableElement m : methods) {
             // exclude methods defined in Component, Owner, and Ownable
             } else if (name.startsWith("get")) {
                 processGetter(m, "get", getters);
             } else if (name.startsWith("set")) {
-                processSetter(m, setters, setterParameters, tu);
+                processSetter(m, setters, setterParameters, setterValidationAnnotations, tu);
             } else {
                 throw fail(declare, name + " is an illegal property method");
             }
         return properties;
     }
 
+    @Override
+    public List<Annotation> getValidationAnnotations(String setterName) {
+        List<Annotation> v = setterValidationAnnotations.get(setterName);
+        if (v == null) {
+            return Collections.emptyList();
+        } else {
+            return Collections.unmodifiableList(v);
+        }
+    }
+
     private static IllegalComponentDefinitionException fail(TypeMirror type, String msg) {
         return new IllegalComponentDefinitionException(type.toString(), msg);
     }
         private final String type;
         private final String propertyType;
 
+        private final List<Annotation> validationAnnotations;
+
         public MirrorPropertyDeclaration(String name, ExecutableElement getter, ExecutableElement setter,
                                          int parameter, TypeElement propertyType) {
             this.name = name;
 
             isSharedInstance = getter.getAnnotation(SharedInstance.class) != null;
             isGeneric = propertyType.getAnnotation(GenericProperty.class) != null;
+
+            List<Annotation> annots = new ArrayList<>();
+            for (VariableElement param : setter.getParameters()) {
+                NotNull notNull = param.getAnnotation(NotNull.class);
+                if (notNull != null) {
+                    annots.add(notNull);
+                }
+                Within within = param.getAnnotation(Within.class);
+                if (within != null) {
+                    annots.add(within);
+                }
+            }
+            validationAnnotations = Collections.unmodifiableList(annots);
         }
 
         @Override
         }
 
         @Override
+        public List<Annotation> getValidationAnnotations() {
+            return validationAnnotations;
+        }
+
+        @Override
         public boolean isShared() {
             return isSharedInstance;
         }
     }
 
     private static void processSetter(ExecutableElement m, Map<String, ExecutableElement> setters,
-                                      Map<String, Integer> parameters, Types tu) {
+                                      Map<String, Integer> parameters,
+                                      Map<String, List<Annotation>> setterValidationAnnotations, Types tu) {
         TypeMirror declaringClass = m.getEnclosingElement().asType();
         if (!tu.isSameType(m.getReturnType(), m.getEnclosingElement().asType()) &&
             !m.getReturnType().getKind().equals(TypeKind.VOID)) {
                 parameters.put(name, i++);
             }
         }
+
+        List<Annotation> annots = new ArrayList<>();
+        NotNull notNull = m.getAnnotation(NotNull.class);
+        if (notNull != null) {
+            annots.add(notNull);
+        }
+        Within within = m.getAnnotation(Within.class);
+        if (within != null) {
+            annots.add(within);
+        }
+        Validate validate = m.getAnnotation(Validate.class);
+        if (validate != null) {
+            annots.add(validate);
+        }
+
+        setterValidationAnnotations.put(m.getSimpleName().toString(), annots);
     }
 
     private static void processGetter(ExecutableElement m, String prefix,

File src/main/java/com/lhkbob/entreri/impl/PropertyDeclaration.java

View file
 
 import com.lhkbob.entreri.property.PropertyFactory;
 
+import java.lang.annotation.Annotation;
+import java.util.List;
+
 /**
  * PropertyDeclaration represents a particular "property" instance declared in a Component sub-interface. A
  * property is represented by a bean getter method and an associated setter. This interface captures the
     public boolean getSetterReturnsComponent();
 
     /**
+     * Get all validation annotations applied directly to the setter parameter of this property. Included
+     * annotations will be instances of {@link com.lhkbob.entreri.NotNull} or {@link
+     * com.lhkbob.entreri.Within}.
+     *
+     * @return All annotations present on the given parameter
+     */
+    public List<Annotation> getValidationAnnotations();
+
+    /**
      * Get whether or not this property should use the shared instance API to store and get values of the
      * property.
      *

File src/main/java/com/lhkbob/entreri/impl/ReflectionComponentSpecification.java

View file
  */
 package com.lhkbob.entreri.impl;
 
-import com.lhkbob.entreri.Component;
-import com.lhkbob.entreri.IllegalComponentDefinitionException;
-import com.lhkbob.entreri.Ownable;
-import com.lhkbob.entreri.Owner;
+import com.lhkbob.entreri.*;
 import com.lhkbob.entreri.property.*;
 
 import java.lang.annotation.Annotation;
 class ReflectionComponentSpecification implements ComponentSpecification {
     private final Class<? extends Component> type;
     private final List<ReflectionPropertyDeclaration> properties;
+    private final Map<String, List<Annotation>> setterValidationAnnotations;
 
     public ReflectionComponentSpecification(Class<? extends Component> type) {
         if (!Component.class.isAssignableFrom(type)) {
         Map<String, Method> getters = new HashMap<>();
         Map<String, Method> setters = new HashMap<>();
         Map<String, Integer> setterParameters = new HashMap<>();
+        setterValidationAnnotations = new HashMap<>();
 
         for (Method method : type.getMethods()) {
             // exclude methods defined in Component, Owner, Ownable, and Object
             } else if (method.getName().startsWith("get")) {
                 processGetter(method, "get", getters);
             } else if (method.getName().startsWith("set")) {
-                processSetter(method, setters, setterParameters);
+                processSetter(method, setters, setterParameters, setterValidationAnnotations);
             } else {
                 throw fail(md, method + " is an illegal property method");
             }
         return properties;
     }
 
+    @Override
+    public List<Annotation> getValidationAnnotations(String setterName) {
+        List<Annotation> v = setterValidationAnnotations.get(setterName);
+        if (v == null) {
+            return Collections.emptyList();
+        } else {
+            return Collections.unmodifiableList(v);
+        }
+    }
+
     private static IllegalComponentDefinitionException fail(Class<?> cls, String msg) {
         return new IllegalComponentDefinitionException(cls.getCanonicalName(), msg);
     }
         private final boolean isGeneric;
 
         private final Class<? extends Property> propertyType;
+        private final List<Annotation> validationAnnotations;
 
         @SuppressWarnings("unchecked")
         private ReflectionPropertyDeclaration(String name, PropertyFactory<?> factory, Method getter,
 
             propertyType = getCreatedType((Class<? extends PropertyFactory<?>>) factory.getClass());
             isGeneric = propertyType.getAnnotation(GenericProperty.class) != null;
+
+            List<Annotation> v = new ArrayList<>();
+            for (Annotation a : setter.getParameterAnnotations()[setterParameter]) {
+                if (a instanceof NotNull || a instanceof Within) {
+                    v.add(a);
+                }
+            }
+            validationAnnotations = Collections.unmodifiableList(v);
         }
 
         @Override
         }
 
         @Override
+        public List<Annotation> getValidationAnnotations() {
+            return validationAnnotations;
+        }
+
+        @Override
         public boolean isShared() {
             return isSharedInstance;
         }
         }
     }
 
-    private static void processSetter(Method m, Map<String, Method> setters,
-                                      Map<String, Integer> parameters) {
+    private static void processSetter(Method m, Map<String, Method> setters, Map<String, Integer> parameters,
+                                      Map<String, List<Annotation>> setterValidationAnnotations) {
         if (!m.getReturnType().equals(m.getDeclaringClass()) && !m.getReturnType().equals(void.class)) {
             throw fail(m.getDeclaringClass(), m + " has invalid return type for setter");
         }
                 parameters.put(name, i);
             }
         }
+
+        // add all validation annotations applied directly to the method
+        // parameter annotations are handled by the declaration constructor
+        List<Annotation> annots = new ArrayList<>();
+        for (Annotation a : m.getAnnotations()) {
+            if (a instanceof NotNull || a instanceof Within || a instanceof Validate) {
+                annots.add(a);
+            }
+        }
+        // note this key needs to be method name, not property name
+        setterValidationAnnotations.put(m.getName(), annots);
     }
 
     private static void processGetter(Method m, String prefix, Map<String, Method> getters) {

File src/test/java/com/lhkbob/entreri/ComponentTest.java

View file
  */
 package com.lhkbob.entreri;
 
-import com.lhkbob.entreri.components.ComplexComponent;
-import com.lhkbob.entreri.components.CustomProperty;
-import com.lhkbob.entreri.components.FloatPropertyFactory;
-import com.lhkbob.entreri.components.IntComponent;
+import com.lhkbob.entreri.components.*;
 import org.junit.Assert;
 import org.junit.Test;
 
     }
 
     @Test
+    public void testValidation() {
+        EntitySystem system = EntitySystem.Factory.create();
+        Entity e = system.addEntity();
+
+        ObjectComponent nullTest = e.add(ObjectComponent.class);
+        FloatComponent withinTest = e.add(FloatComponent.class);
+        ComplexComponent validTest = e.add(ComplexComponent.class);
+
+        try {
+            nullTest.setObject(null);
+            Assert.fail("Expected NullPointerException");
+        } catch (NullPointerException ne) {
+            // expected
+        }
+        nullTest.setObject(new ObjectComponent.FooBlah());
+
+        try {
+            withinTest.setFloat(-56);
+            Assert.fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException ie) {
+            // expected
+        }
+        try {
+            withinTest.setFloat(5000);
+            Assert.fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException ie) {
+            // expected
+        }
+        withinTest.setFloat(0);
+
+        try {
+            validTest.setParams((short) 10, (short) 1);
+            Assert.fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException ie) {
+            // expected
+        }
+        validTest.setParams((short) 1, (short) 10);
+    }
+
+    @Test
     public void testFlyweightIsAliveAfterRemoval() {
         EntitySystem system = EntitySystem.Factory.create();
 

File src/test/java/com/lhkbob/entreri/components/ComplexComponent.java

View file
  */
 package com.lhkbob.entreri.components;
 
+import com.lhkbob.entreri.Validate;
 import com.lhkbob.entreri.property.Factory;
 import com.lhkbob.entreri.property.IntProperty.DefaultInt;
 import com.lhkbob.entreri.property.LongProperty.DefaultLong;
 
     public short getParam2();
 
+    @Validate("$1 < $2")
     public ComplexComponent setParams(@Named("param1") short p1, @Named("param2") short p2);
 
     @Named("foo-blah")

File src/test/java/com/lhkbob/entreri/components/FloatComponent.java

View file
 package com.lhkbob.entreri.components;
 
 import com.lhkbob.entreri.Component;
+import com.lhkbob.entreri.Within;
 
 /**
  * A test component that tests the float primitive type.
 public interface FloatComponent extends Component {
     public float getFloat();
 
-    public void setFloat(float value);
+    public void setFloat(@Within(min = -12, max = 4500) float value);
 }

File src/test/java/com/lhkbob/entreri/components/ObjectComponent.java

View file
 package com.lhkbob.entreri.components;
 
 import com.lhkbob.entreri.Component;
+import com.lhkbob.entreri.NotNull;
 
 /**
  * A test component that tests the default object property for unknown types.
 public interface ObjectComponent extends Component {
     public FooBlah getObject();
 
+    @NotNull
     public void setObject(FooBlah value);
 
     public static class FooBlah {