Make validators extensible 89/116489/1
authorJim Hahn <jrh3@att.com>
Fri, 18 Dec 2020 20:53:20 +0000 (15:53 -0500)
committerJim Hahn <jrh3@att.com>
Sat, 19 Dec 2020 02:17:12 +0000 (21:17 -0500)
Modified the validator to make it extensible.  Also added annotations
to:
- cascade a validation to a sub-object
- perform regex
- examine items in a list
- examine entries in a map

Still need more junit tests.

Issue-ID: POLICY-2648
Change-Id: I94f1b9e8fbf7a6b9b002d0b05cc9119bdfcf8bf2
Signed-off-by: Jim Hahn <jrh3@att.com>
common-parameters/pom.xml
common-parameters/src/main/java/org/onap/policy/common/parameters/BeanValidator.java
common-parameters/src/main/java/org/onap/policy/common/parameters/EntryValidator.java [new file with mode: 0644]
common-parameters/src/main/java/org/onap/policy/common/parameters/FieldValidator.java [new file with mode: 0644]
common-parameters/src/main/java/org/onap/policy/common/parameters/ItemValidator.java [new file with mode: 0644]
common-parameters/src/main/java/org/onap/policy/common/parameters/ValueValidator.java [new file with mode: 0644]
common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Entries.java [new file with mode: 0644]
common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Items.java [new file with mode: 0644]
common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Pattern.java [new file with mode: 0644]
common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Valid.java [new file with mode: 0644]

index c7136bd..c0efcf7 100644 (file)
     <description>[${project.parent.artifactId}] module provides common property and parameter handling the ONAP Policy Framework</description>
 
     <dependencies>
+        <dependency>
+            <groupId>com.google.re2j</groupId>
+            <artifactId>re2j</artifactId>
+        </dependency>
          <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
index dbd3c7c..3f5abcc 100644 (file)
@@ -22,32 +22,24 @@ package org.onap.policy.common.parameters;
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.BiPredicate;
-import java.util.function.Predicate;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
 import org.apache.commons.lang3.StringUtils;
+import org.onap.policy.common.parameters.annotations.Entries;
+import org.onap.policy.common.parameters.annotations.Items;
 import org.onap.policy.common.parameters.annotations.Max;
 import org.onap.policy.common.parameters.annotations.Min;
 import org.onap.policy.common.parameters.annotations.NotBlank;
 import org.onap.policy.common.parameters.annotations.NotNull;
+import org.onap.policy.common.parameters.annotations.Pattern;
+import org.onap.policy.common.parameters.annotations.Valid;
 
 /**
  * Bean validator, supporting the parameter annotations.
- * <p/>
- * Note: this currently does not support Min/Max validation of "short" or "byte"; these
- * annotations are simply ignored for these types.
  */
 public class BeanValidator {
 
-    /**
-     * {@code True} if there is a field-level annotation, {@code false} otherwise.
-     */
-    private boolean fieldIsAnnotated;
-
     /**
      * Validates top level fields within an object. For each annotated field, it retrieves
      * the value using the public "getter" method for the field. If there is no public
@@ -75,162 +67,95 @@ public class BeanValidator {
     }
 
     /**
-     * Performs validation of all annotated fields found within the class.
+     * Adds validators based on the annotations that are available.
      *
-     * @param result validation results are added here
-     * @param object object whose fields are to be validated
-     * @param clazz class, within the object's hierarchy, to be examined for fields to be
-     *        verified
+     * @param validator where to add the validators
      */
-    private void validateFields(BeanValidationResult result, Object object, Class<?> clazz) {
-        for (Field field : clazz.getDeclaredFields()) {
-            validateField(result, object, clazz, field);
-        }
+    protected void addValidators(ValueValidator validator) {
+        validator.addAnnotation(NotNull.class, this::verNotNull);
+        validator.addAnnotation(NotBlank.class, this::verNotBlank);
+        validator.addAnnotation(Max.class, this::verMax);
+        validator.addAnnotation(Min.class, this::verMin);
+        validator.addAnnotation(Pattern.class, this::verRegex);
+        validator.addAnnotation(Valid.class, this::verCascade);
+        validator.addAnnotation(Items.class, this::verCollection);
+        validator.addAnnotation(Entries.class, this::verMap);
     }
 
     /**
-     * Performs validation of a single field.
+     * Performs validation of all annotated fields found within the class.
      *
      * @param result validation results are added here
      * @param object object whose fields are to be validated
-     * @param clazz class, within the object's hierarchy, containing the field
-     * @param field field whose value is to be validated
-     */
-    private void validateField(BeanValidationResult result, Object object, Class<?> clazz, Field field) {
-        final String fieldName = field.getName();
-        if (fieldName.contains("$")) {
-            return;
-        }
-
-        /*
-         * Identify the annotations. NotNull MUST be first so the check is run before the
-         * others.
-         */
-        fieldIsAnnotated = false;
-        List<Predicate<Object>> checkers = new ArrayList<>(10);
-        addAnnotation(clazz, field, checkers, NotNull.class, (annot, value) -> verNotNull(result, fieldName, value));
-        addAnnotation(clazz, field, checkers, NotBlank.class, (annot, value) -> verNotBlank(result, fieldName, value));
-        addAnnotation(clazz, field, checkers, Max.class, (annot, value) -> verMax(result, fieldName, annot, value));
-        addAnnotation(clazz, field, checkers, Min.class, (annot, value) -> verMin(result, fieldName, annot, value));
-
-        if (checkers.isEmpty()) {
-            // has no annotations - nothing to check
-            return;
-        }
-
-        // verify the field type is of interest
-        int mod = field.getModifiers();
-        if (Modifier.isStatic(mod)) {
-            classOnly(clazz.getName() + "." + fieldName + " is annotated but the field is static");
-            return;
-        }
-
-        // get the field's "getter" method
-        Method accessor = getAccessor(object.getClass(), fieldName);
-        if (accessor == null) {
-            classOnly(clazz.getName() + "." + fieldName + " is annotated but has no \"get\" method");
-            return;
-        }
-
-        // get the value
-        Object value = getValue(object, clazz, fieldName, accessor);
-
-        // perform the checks
-        if (value == null && field.getAnnotation(NotNull.class) == null && clazz.getAnnotation(NotNull.class) == null) {
-            // value is null and there's no null check - just return
-            return;
-        }
-
-        for (Predicate<Object> checker : checkers) {
-            if (!checker.test(value)) {
-                // invalid - don't bother with additional checks
-                return;
-            }
-        }
-    }
-
-    /**
-     * Gets the value from the object using the accessor function.
-     *
-     * @param object object whose value is to be retrieved
-     * @param clazz class containing the field
-     * @param fieldName name of the field
-     * @param accessor "getter" method
-     * @return the object's value
-     */
-    private Object getValue(Object object, Class<?> clazz, final String fieldName, Method accessor) {
-        try {
-            return accessor.invoke(object);
-
-        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
-            throw new IllegalArgumentException(clazz.getName() + "." + fieldName + " accessor threw an exception", e);
-        }
-    }
-
-    /**
-     * Throws an exception if there are field-level annotations.
-     *
-     * @param exceptionMessage exception message
+     * @param clazz class, within the object's hierarchy, to be examined for fields to be
+     *        verified
      */
-    private void classOnly(String exceptionMessage) {
-        if (fieldIsAnnotated) {
-            throw new IllegalArgumentException(exceptionMessage);
+    private void validateFields(BeanValidationResult result, Object object, Class<?> clazz) {
+        for (Field field : clazz.getDeclaredFields()) {
+            FieldValidator validator = makeFieldValidator(clazz, field);
+            validator.validateField(result, object);
         }
     }
 
     /**
-     * Looks for an annotation at the class or field level. If an annotation is found at
-     * either the field or class level, then it adds a verifier to the list of checkers.
+     * Verifies that the value is not null.
      *
-     * @param clazz class to be searched
-     * @param field field to be searched
-     * @param checkers where to place the new field verifier
-     * @param annotClass class of annotation to find
-     * @param check verification function to be added to the list, if the annotation is
-     *        found
+     * @param result where to add the validation result
+     * @param fieldName field whose value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
      */
-    private <T extends Annotation> void addAnnotation(Class<?> clazz, Field field, List<Predicate<Object>> checkers,
-                    Class<T> annotClass, BiPredicate<T, Object> check) {
-
-        // field annotation takes precedence over class annotation
-        T annot = field.getAnnotation(annotClass);
-        if (annot != null) {
-            fieldIsAnnotated = true;
-
-        } else if ((annot = clazz.getAnnotation(annotClass)) == null) {
-            return;
+    public boolean verNotNull(BeanValidationResult result, String fieldName, Object value) {
+        if (value == null) {
+            ObjectValidationResult result2 =
+                            new ObjectValidationResult(fieldName, xlate(value), ValidationStatus.INVALID, "is null");
+            result.addResult(result2);
+            return false;
         }
 
-        T annot2 = annot;
-        checkers.add(value -> check.test(annot2, value));
+        return true;
     }
 
     /**
-     * Verifies that the value is not null.
+     * Verifies that the value is not blank. Note: this does <i>not</i> verify that the
+     * value is not {@code null}.
      *
      * @param result where to add the validation result
      * @param fieldName field whose value is being verified
-     * @param value value to be verified, assumed to be non-null
-     * @return {@code true} if the value is valid, {@code false} otherwise
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
      */
-    private boolean verNotNull(BeanValidationResult result, String fieldName, Object value) {
-        return result.validateNotNull(fieldName, value);
+    public boolean verNotBlank(BeanValidationResult result, String fieldName, Object value) {
+        if (value instanceof String && StringUtils.isBlank(value.toString())) {
+            ObjectValidationResult result2 =
+                            new ObjectValidationResult(fieldName, xlate(value), ValidationStatus.INVALID, "is blank");
+            result.addResult(result2);
+            return false;
+        }
+
+        return true;
     }
 
     /**
-     * Verifies that the value is not blank.
+     * Verifies that the value matches a regular expression.
      *
      * @param result where to add the validation result
      * @param fieldName field whose value is being verified
-     * @param value value to be verified, assumed to be non-null
-     * @return {@code true} if the value is valid, {@code false} otherwise
+     * @param annot annotation against which the value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
      */
-    private boolean verNotBlank(BeanValidationResult result, String fieldName, Object value) {
-        if (value instanceof String && StringUtils.isBlank(value.toString())) {
-            ObjectValidationResult fieldResult =
-                            new ObjectValidationResult(fieldName, value, ValidationStatus.INVALID, "is blank");
-            result.addResult(fieldResult);
-            return false;
+    public boolean verRegex(BeanValidationResult result, String fieldName, Pattern annot, Object value) {
+        try {
+            if (value instanceof String && !com.google.re2j.Pattern.matches(annot.regexp(), value.toString())) {
+                ObjectValidationResult result2 = new ObjectValidationResult(fieldName, xlate(value),
+                                ValidationStatus.INVALID, "does not match regular expression " + annot.regexp());
+                result.addResult(result2);
+                return false;
+            }
+        } catch (RuntimeException e) {
+            // TODO log at trace level
+            return true;
         }
 
         return true;
@@ -242,10 +167,10 @@ public class BeanValidator {
      * @param result where to add the validation result
      * @param fieldName field whose value is being verified
      * @param annot annotation against which the value is being verified
-     * @param value value to be verified, assumed to be non-null
-     * @return {@code true} if the value is valid, {@code false} otherwise
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
      */
-    private boolean verMax(BeanValidationResult result, String fieldName, Max annot, Object value) {
+    public boolean verMax(BeanValidationResult result, String fieldName, Max annot, Object value) {
         if (!(value instanceof Number)) {
             return true;
         }
@@ -265,9 +190,9 @@ public class BeanValidator {
             return true;
         }
 
-        ObjectValidationResult fieldResult = new ObjectValidationResult(fieldName, value, ValidationStatus.INVALID,
+        ObjectValidationResult result2 = new ObjectValidationResult(fieldName, xlate(value), ValidationStatus.INVALID,
                         "exceeds the maximum value: " + annot.value());
-        result.addResult(fieldResult);
+        result.addResult(result2);
         return false;
     }
 
@@ -277,22 +202,35 @@ public class BeanValidator {
      * @param result where to add the validation result
      * @param fieldName field whose value is being verified
      * @param annot annotation against which the value is being verified
-     * @param value value to be verified, assumed to be non-null
-     * @return {@code true} if the value is valid, {@code false} otherwise
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verMin(BeanValidationResult result, String fieldName, Min annot, Object value) {
+        return verMin(result, fieldName, annot.value(), value);
+    }
+
+    /**
+     * Verifies that the value is >= the minimum value.
+     *
+     * @param result where to add the validation result
+     * @param fieldName field whose value is being verified
+     * @param min minimum against which the value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
      */
-    private boolean verMin(BeanValidationResult result, String fieldName, Min annot, Object value) {
+    public boolean verMin(BeanValidationResult result, String fieldName, long min, Object value) {
         if (!(value instanceof Number)) {
             return true;
         }
 
         Number num = (Number) value;
         if (num instanceof Integer || num instanceof Long) {
-            if (num.longValue() >= annot.value()) {
+            if (num.longValue() >= min) {
                 return true;
             }
 
         } else if (num instanceof Float || num instanceof Double) {
-            if (num.doubleValue() >= annot.value()) {
+            if (num.doubleValue() >= min) {
                 return true;
             }
 
@@ -300,54 +238,181 @@ public class BeanValidator {
             return true;
         }
 
-        ObjectValidationResult fieldResult = new ObjectValidationResult(fieldName, value, ValidationStatus.INVALID,
-                        "is below the minimum value: " + annot.value());
-        result.addResult(fieldResult);
+        ObjectValidationResult result2 = new ObjectValidationResult(fieldName, xlate(value), ValidationStatus.INVALID,
+                        "is below the minimum value: " + min);
+        result.addResult(result2);
         return false;
     }
 
     /**
-     * Gets an accessor method for the given field.
+     * Verifies that the value is valid by recursively invoking
+     * {@link #validateTop(String, Object)}.
      *
-     * @param clazz class whose methods are to be searched
-     * @param fieldName field whose "getter" is to be identified
-     * @return the field's "getter" method, or {@code null} if it is not found
+     * @param result where to add the validation result
+     * @param fieldName field whose value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
      */
-    private Method getAccessor(Class<?> clazz, String fieldName) {
-        String capname = StringUtils.capitalize(fieldName);
-        Method accessor = getMethod(clazz, "get" + capname);
-        if (accessor != null) {
-            return accessor;
+    public boolean verCascade(BeanValidationResult result, String fieldName, Object value) {
+        if (value == null || value instanceof Collection || value instanceof Map) {
+            return true;
         }
 
-        return getMethod(clazz, "is" + capname);
+        BeanValidationResult result2 = validateTop(fieldName, value);
+
+        if (result2.isClean()) {
+            return true;
+        }
+
+        result.addResult(result2);
+
+        return result2.isValid();
     }
 
     /**
-     * Gets the "getter" method having the specified name.
+     * Validates the items in a collection.
      *
-     * @param clazz class whose methods are to be searched
-     * @param methodName name of the method of interest
-     * @return the method, or {@code null} if it is not found
+     * @param result where to add the validation result
+     * @param fieldName name of the field containing the collection
+     * @param annot validation annotations for individual items
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
      */
-    private Method getMethod(Class<?> clazz, String methodName) {
-        for (Method method : clazz.getMethods()) {
-            if (methodName.equals(method.getName()) && validMethod(method)) {
-                return method;
-            }
+    public boolean verCollection(BeanValidationResult result, String fieldName, Annotation annot, Object value) {
+
+        if (!(value instanceof Collection)) {
+            return true;
+        }
+
+        ItemValidator itemValidator = makeItemValidator(annot);
+
+        return verCollection(result, fieldName, itemValidator, value);
+    }
+
+    /**
+     * Validates the items in a collection.
+     *
+     * @param result where to add the validation result
+     * @param fieldName name of the field containing the collection
+     * @param itemValidator validator for individual items within the list
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verCollection(BeanValidationResult result, String fieldName, ValueValidator itemValidator,
+                    Object value) {
+
+        if (!(value instanceof Collection) || itemValidator.isEmpty()) {
+            return true;
+        }
+
+        Collection<?> list = (Collection<?>) value;
+
+        BeanValidationResult result2 = new BeanValidationResult(fieldName, value);
+        int count = 0;
+        for (Object item : list) {
+            itemValidator.validateValue(result2, String.valueOf(count++), item);
+        }
+
+        if (result2.isClean()) {
+            return true;
+        }
+
+        result.addResult(result2);
+        return false;
+    }
+
+    /**
+     * Validates the items in a Map.
+     *
+     * @param result where to add the validation result
+     * @param fieldName name of the field containing the map
+     * @param annot validation annotations for individual entries
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verMap(BeanValidationResult result, String fieldName, Entries annot, Object value) {
+
+        if (!(value instanceof Map)) {
+            return true;
+        }
+
+        EntryValidator entryValidator = makeEntryValidator(annot.key(), annot.value());
+
+        return verMap(result, fieldName, entryValidator, value);
+    }
+
+    /**
+     * Validates the items in a Map.
+     *
+     * @param result where to add the validation result
+     * @param fieldName name of the field containing the map
+     * @param entryValidator validator for individual entries within the Map
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verMap(BeanValidationResult result, String fieldName, EntryValidator entryValidator, Object value) {
+
+        if (!(value instanceof Map)) {
+            return true;
+        }
+
+        Map<?, ?> map = (Map<?, ?>) value;
+
+        BeanValidationResult result2 = new BeanValidationResult(fieldName, value);
+
+        for (Entry<?, ?> entry : map.entrySet()) {
+            entryValidator.validateEntry(result2, entry);
+        }
+
+        if (result2.isClean()) {
+            return true;
         }
 
-        return null;
+        result.addResult(result2);
+        return false;
+    }
+
+    /**
+     * Makes a field validator.
+     *
+     * @param clazz class containing the field
+     * @param field field of interest
+     * @return a validator for the given field
+     */
+    protected FieldValidator makeFieldValidator(Class<?> clazz, Field field) {
+        return new FieldValidator(this, clazz, field);
+    }
+
+    /**
+     * Makes an item validator.
+     *
+     * @param annot container for the item annotations
+     * @return a new item validator
+     */
+    protected ItemValidator makeItemValidator(Annotation annot) {
+        return new ItemValidator(this, annot);
+    }
+
+    /**
+     * Makes an entry validator.
+     *
+     * @param keyAnnot container for the annotations associated with the entry key
+     * @param valueAnnot container for the annotations associated with the entry value
+     * @return a new entry validator
+     */
+    protected EntryValidator makeEntryValidator(Annotation keyAnnot, Annotation valueAnnot) {
+        return new EntryValidator(this, keyAnnot, valueAnnot);
     }
 
     /**
-     * Determines if a method is a valid "getter".
+     * Translates a value to something printable, for use by
+     * {@link ObjectValidationResult}. This default method simply returns the original
+     * value.
      *
-     * @param method method to be checked
-     * @return {@code true} if the method is a valid "getter", {@code false} otherwise
+     * @param value value to be translated
+     * @return the translated value
      */
-    private boolean validMethod(Method method) {
-        int mod = method.getModifiers();
-        return !(Modifier.isStatic(mod) || method.getReturnType() == void.class || method.getParameterCount() != 0);
+    public Object xlate(Object value) {
+        return value;
     }
 }
diff --git a/common-parameters/src/main/java/org/onap/policy/common/parameters/EntryValidator.java b/common-parameters/src/main/java/org/onap/policy/common/parameters/EntryValidator.java
new file mode 100644 (file)
index 0000000..965c95e
--- /dev/null
@@ -0,0 +1,84 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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 License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters;
+
+import java.lang.annotation.Annotation;
+import java.util.Map;
+
+/**
+ * Validator of an entry within a Map.
+ */
+public class EntryValidator {
+    private final ItemValidator keyValidator;
+    private final ItemValidator valueValidator;
+
+    /**
+     * Constructs the object.
+     *
+     * @param validator provider of validation methods
+     * @param keyAnnotationContainer an annotation containing validation annotations to be
+     *        applied to the entry key
+     * @param valueAnnotationContainer an annotation containing validation annotations to
+     *        be applied to the entry value
+     */
+    public EntryValidator(BeanValidator validator, Annotation keyAnnotationContainer,
+                    Annotation valueAnnotationContainer) {
+        keyValidator = new ItemValidator(validator, keyAnnotationContainer);
+        valueValidator = new ItemValidator(validator, valueAnnotationContainer);
+    }
+
+    public boolean isEmpty() {
+        return (keyValidator.isEmpty() && valueValidator.isEmpty());
+    }
+
+    /**
+     * Performs validation of a single entry.
+     *
+     * @param result validation results are added here
+     * @param entry value to be validated
+     */
+    public <K, V> void validateEntry(BeanValidationResult result, Map.Entry<K, V> entry) {
+        String name = getName(entry);
+
+        BeanValidationResult result2 = new BeanValidationResult(name, entry);
+        keyValidator.validateValue(result2, "key", entry.getKey());
+        valueValidator.validateValue(result2, "value", entry.getValue());
+
+        if (!result2.isClean()) {
+            result.addResult(result2);
+        }
+    }
+
+    /**
+     * Gets a name for the entry.
+     *
+     * @param entry entry whose name is to be determined
+     * @return a name for the entry
+     */
+    protected <K, V> String getName(Map.Entry<K, V> entry) {
+        K key = entry.getKey();
+        if (key == null) {
+            return "";
+        }
+
+        return key.toString();
+    }
+}
diff --git a/common-parameters/src/main/java/org/onap/policy/common/parameters/FieldValidator.java b/common-parameters/src/main/java/org/onap/policy/common/parameters/FieldValidator.java
new file mode 100644 (file)
index 0000000..e762dc0
--- /dev/null
@@ -0,0 +1,216 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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 License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+/**
+ * Validator of the contents of a field, supporting the parameter annotations.
+ */
+public class FieldValidator extends ValueValidator {
+
+    /**
+     * {@code True} if there is a field-level annotation, {@code false} otherwise.
+     */
+    @Getter
+    @Setter(AccessLevel.PROTECTED)
+    private boolean fieldAnnotated = false;
+
+    /**
+     * Class containing the field of interest.
+     */
+    private final Class<?> clazz;
+
+    /**
+     * Field of interest.
+     */
+    private final Field field;
+
+    /**
+     * Method to retrieve the field's value.
+     */
+    private Method accessor;
+
+
+    /**
+     * Constructs the object.
+     *
+     * @param validator provider of validation methods
+     * @param clazz class containing the field
+     * @param field field whose value is to be validated
+     */
+    public FieldValidator(BeanValidator validator, Class<?> clazz, Field field) {
+        this.clazz = clazz;
+        this.field = field;
+
+        String fieldName = field.getName();
+        if (fieldName.contains("$")) {
+            return;
+        }
+
+        validator.addValidators(this);
+
+        if (checkers.isEmpty()) {
+            // has no annotations - nothing to check
+            return;
+        }
+
+        // verify the field type is of interest
+        int mod = field.getModifiers();
+        if (Modifier.isStatic(mod)) {
+            classOnly(clazz.getName() + "." + fieldName + " is annotated but the field is static");
+            checkers.clear();
+            return;
+        }
+
+        // get the field's "getter" method
+        accessor = getAccessor(clazz, fieldName);
+        if (accessor == null) {
+            classOnly(clazz.getName() + "." + fieldName + " is annotated but has no \"get\" method");
+            checkers.clear();
+            return;
+        }
+
+        // determine if null is allowed
+        if (field.getAnnotation(NotNull.class) != null || clazz.getAnnotation(NotNull.class) != null) {
+            setNullAllowed(false);
+        }
+    }
+
+    /**
+     * Performs validation of a single field.
+     *
+     * @param result validation results are added here
+     * @param object object whose field is to be validated
+     */
+    public void validateField(BeanValidationResult result, Object object) {
+        if (isEmpty()) {
+            // has no annotations - nothing to check
+            return;
+        }
+
+        // get the value
+        Object value = getValue(object, accessor);
+
+        validateValue(result, field.getName(), value);
+    }
+
+    /**
+     * Gets the value from the object using the accessor function.
+     *
+     * @param object object whose value is to be retrieved
+     * @param accessor "getter" method
+     * @return the object's value
+     */
+    private Object getValue(Object object, Method accessor) {
+        try {
+            return accessor.invoke(object);
+
+        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+            throw new IllegalArgumentException(clazz.getName() + "." + field.getName() + " accessor threw an exception",
+                            e);
+        }
+    }
+
+    /**
+     * Throws an exception if there are field-level annotations.
+     *
+     * @param exceptionMessage exception message
+     */
+    private void classOnly(String exceptionMessage) {
+        if (isFieldAnnotated()) {
+            throw new IllegalArgumentException(exceptionMessage);
+        }
+    }
+
+    /**
+     * Gets an annotation from the field or the class.
+     *
+     * @param annotClass annotation class of interest
+     * @return the annotation, or {@code null} if neither the field nor the class has the
+     *         desired annotation
+     */
+    public <T extends Annotation> T getAnnotation(Class<T> annotClass) {
+
+        // field annotation takes precedence over class annotation
+        T annot = field.getAnnotation(annotClass);
+        if (annot != null) {
+            setFieldAnnotated(true);
+            return annot;
+        }
+
+        return clazz.getAnnotation(annotClass);
+    }
+
+    /**
+     * Gets an accessor method for the given field.
+     *
+     * @param clazz class whose methods are to be searched
+     * @param fieldName field whose "getter" is to be identified
+     * @return the field's "getter" method, or {@code null} if it is not found
+     */
+    private Method getAccessor(Class<?> clazz, String fieldName) {
+        String capname = StringUtils.capitalize(fieldName);
+        Method accessor = getMethod(clazz, "get" + capname);
+        if (accessor != null) {
+            return accessor;
+        }
+
+        return getMethod(clazz, "is" + capname);
+    }
+
+    /**
+     * Gets the "getter" method having the specified name.
+     *
+     * @param clazz class whose methods are to be searched
+     * @param methodName name of the method of interest
+     * @return the method, or {@code null} if it is not found
+     */
+    private Method getMethod(Class<?> clazz, String methodName) {
+        for (Method method : clazz.getMethods()) {
+            if (methodName.equals(method.getName()) && validMethod(method)) {
+                return method;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Determines if a method is a valid "getter".
+     *
+     * @param method method to be checked
+     * @return {@code true} if the method is a valid "getter", {@code false} otherwise
+     */
+    private boolean validMethod(Method method) {
+        int mod = method.getModifiers();
+        return !(Modifier.isStatic(mod) || method.getReturnType() == void.class || method.getParameterCount() != 0);
+    }
+}
diff --git a/common-parameters/src/main/java/org/onap/policy/common/parameters/ItemValidator.java b/common-parameters/src/main/java/org/onap/policy/common/parameters/ItemValidator.java
new file mode 100644 (file)
index 0000000..d0c027c
--- /dev/null
@@ -0,0 +1,108 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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 License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Validator of an "item", which is typically found in a collection, or the key or value
+ * components of an entry in a Map.
+ */
+public class ItemValidator extends ValueValidator {
+    private final Annotation annotationContainer;
+
+    /**
+     * Constructs the object.
+     *
+     * @param validator provider of validation methods
+     * @param annotationContainer an annotation containing validation annotations to be
+     *        applied to the item
+     */
+    public ItemValidator(BeanValidator validator, Annotation annotationContainer) {
+        this(validator, annotationContainer, true);
+    }
+
+    /**
+     * Constructs the object.
+     *
+     * @param validator provider of validation methods
+     * @param annotationContainer an annotation containing validation annotations to be
+     *        applied to the item
+     * @param addValidators {@code true} if to add validators
+     */
+    public ItemValidator(BeanValidator validator, Annotation annotationContainer, boolean addValidators) {
+        this.annotationContainer = annotationContainer;
+
+        if (addValidators) {
+            validator.addValidators(this);
+        }
+    }
+
+    /**
+     * Gets an annotation from the field or the class.
+     *
+     * @param annotClass annotation class of interest
+     * @return the annotation, or {@code null} if the {@link #annotationContainer} does
+     *         not contain the desired annotation
+     */
+    public <T extends Annotation> T getAnnotation(Class<T> annotClass) {
+        try {
+            for (Method meth : annotationContainer.getClass().getDeclaredMethods()) {
+                T annot = getAnnotation2(annotClass, meth);
+                if (annot != null) {
+                    return annot;
+                }
+            }
+        } catch (RuntimeException | IllegalAccessException | InvocationTargetException e) {
+            throw new IllegalArgumentException("cannot determine " + annotClass.getName(), e);
+        }
+
+        return null;
+    }
+
+    private <T extends Annotation> T getAnnotation2(Class<T> annotClass, Method method)
+                    throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+
+        Class<?> ret = method.getReturnType();
+        if (!ret.isArray()) {
+            return null;
+        }
+
+        Class<?> comp = ret.getComponentType();
+        if (comp != annotClass) {
+            return null;
+        }
+
+        // get the array for this type of annotation
+        @SuppressWarnings("unchecked")
+        T[] arrobj = (T[]) method.invoke(annotationContainer);
+
+        if (arrobj.length == 0) {
+            return null;
+        }
+
+        // TODO log if there's more than one item
+
+        return arrobj[0];
+    }
+}
diff --git a/common-parameters/src/main/java/org/onap/policy/common/parameters/ValueValidator.java b/common-parameters/src/main/java/org/onap/policy/common/parameters/ValueValidator.java
new file mode 100644 (file)
index 0000000..9095bfd
--- /dev/null
@@ -0,0 +1,158 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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 License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+/**
+ * Validator of a value.
+ * <p/>
+ * Note: this currently does not support Min/Max validation of "short" or "byte"; these
+ * annotations are simply ignored for these types.
+ */
+public class ValueValidator {
+
+    /**
+     * {@code True} if there is a field-level annotation, {@code false} otherwise.
+     */
+    @Setter(AccessLevel.PROTECTED)
+    private boolean fieldIsAnnotated = false;
+
+    /**
+     * {@code True} if the value is allowed to be {@code null}, {@code false} otherwise.
+     * Subclasses are expected to set this, typically based on the validation annotations
+     * associated with the value.
+     */
+    @Getter
+    @Setter(AccessLevel.PROTECTED)
+    private boolean nullAllowed = true;
+
+    /**
+     * Predicates to invoke to validate an object.
+     * <p/>
+     * Note: each predicate is expected to return {@code true} if the next check is
+     * allowed to proceed, {@code false} otherwise. In addition, if {@link #nullAllowed}
+     * is {@code true}, then the predicates must be prepared to deal with a {@code null}
+     * Object as their input parameter.
+     */
+    protected List<Checker> checkers = new ArrayList<>(10);
+
+
+    /**
+     * Constructs the object.
+     */
+    public ValueValidator() {
+        // do nothing
+    }
+
+    /**
+     * Determines if the validator has anything to check.
+     *
+     * @return {@code true} if the validator is empty (i.e., has nothing to check)
+     */
+    public boolean isEmpty() {
+        return checkers.isEmpty();
+    }
+
+    /**
+     * Performs validation of a single field.
+     *
+     * @param result validation results are added here
+     * @param fieldName field whose value is being verified
+     * @param value value to be validated
+     */
+    protected void validateValue(BeanValidationResult result, String fieldName, Object value) {
+
+        if (value == null && isNullAllowed()) {
+            // value is null and null is allowed - just return
+            return;
+        }
+
+        for (Checker checker : checkers) {
+            if (!checker.test(result, fieldName, value)) {
+                // invalid - don't bother with additional checks
+                return;
+            }
+        }
+    }
+
+    /**
+     * Looks for an annotation at the class or field level. If an annotation is found at
+     * either the field or class level, then it adds a verifier to
+     * {@link ValueValidator#checkers}.
+     *
+     * @param annotClass class of annotation to find
+     * @param checker function to validate the value
+     */
+    public <T extends Annotation> void addAnnotation(Class<T> annotClass, Checker checker) {
+        T annot = getAnnotation(annotClass);
+        if (annot != null) {
+            checkers.add(checker);
+
+            if (annotClass == NotNull.class) {
+                setNullAllowed(false);
+            }
+        }
+    }
+
+    /**
+     * Looks for an annotation at the class or field level. If an annotation is found at
+     * either the field or class level, then it adds a verifier to
+     * {@link ValueValidator#checkers}.
+     *
+     * @param annotClass class of annotation to find
+     * @param checker function to validate the value
+     */
+    public <T extends Annotation> void addAnnotation(Class<T> annotClass, CheckerWithAnnot<T> checker) {
+        T annot = getAnnotation(annotClass);
+        if (annot != null) {
+            checkers.add((result, fieldName, value) -> checker.test(result, fieldName, annot, value));
+        }
+    }
+
+    /**
+     * Gets an annotation from the field or the class. The default method simply returns
+     * {@code null}.
+     *
+     * @param annotClass annotation class of interest
+     * @return the annotation, or {@code null} if neither the field nor the class has the
+     *         desired annotation
+     */
+    public <T extends Annotation> T getAnnotation(Class<T> annotClass) {
+        return null;
+    }
+
+    // functions to validate a value extracted from a field
+
+    public static interface Checker {
+        boolean test(BeanValidationResult result, String fieldName, Object value);
+    }
+
+    public static interface CheckerWithAnnot<T extends Annotation> {
+        boolean test(BeanValidationResult result, String fieldName, T annotation, Object value);
+    }
+}
diff --git a/common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Entries.java b/common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Entries.java
new file mode 100644 (file)
index 0000000..89c9ce2
--- /dev/null
@@ -0,0 +1,45 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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 License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Validations on entries within a Map.
+ */
+@Retention(RUNTIME)
+@Target(FIELD)
+public @interface Entries {
+
+    /**
+     * Validations to perform on each entry's key.
+     */
+    Items key();
+
+    /**
+     * Validations to perform on each entry's value.
+     */
+    Items value();
+}
diff --git a/common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Items.java b/common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Items.java
new file mode 100644 (file)
index 0000000..d022d95
--- /dev/null
@@ -0,0 +1,66 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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 License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Validations on individual items, typically within a collection.
+ */
+@Retention(RUNTIME)
+@Target(FIELD)
+public @interface Items {
+
+    /**
+     * Validates the item is not {@code null}.
+     */
+    NotNull[] notNull() default {};
+
+    /**
+     * Validates the item is not blank.
+     */
+    NotBlank[] notBlank() default {};
+
+    /**
+     * Validates the item matches a regular expression.
+     */
+    Pattern[] pattern() default {};
+
+    /**
+     * Validates the item is not greater than a certain value.
+     */
+    Max[] max() default {};
+
+    /**
+     * Validates the item is not less than a certain value.
+     */
+    Min[] min() default {};
+
+    /**
+     * Validates the item is valid, using a {@link BeanValidator}.
+     */
+    Valid[] valid() default {};
+
+}
diff --git a/common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Pattern.java b/common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Pattern.java
new file mode 100644 (file)
index 0000000..a30d814
--- /dev/null
@@ -0,0 +1,37 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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 License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target(FIELD)
+public @interface Pattern {
+
+    /**
+     * Regular expression to be matched.
+     */
+    String regexp();
+}
diff --git a/common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Valid.java b/common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Valid.java
new file mode 100644 (file)
index 0000000..a3a1d59
--- /dev/null
@@ -0,0 +1,34 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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 License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target({TYPE, FIELD})
+public @interface Valid {
+
+}