From 5f1b1162d047d2a743f1ce57cc17494a6150c75c Mon Sep 17 00:00:00 2001 From: Jim Hahn Date: Fri, 18 Dec 2020 15:53:20 -0500 Subject: Make validators extensible 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 --- common-parameters/pom.xml | 4 + .../policy/common/parameters/BeanValidator.java | 423 ++++++++++++--------- .../policy/common/parameters/EntryValidator.java | 84 ++++ .../policy/common/parameters/FieldValidator.java | 216 +++++++++++ .../policy/common/parameters/ItemValidator.java | 108 ++++++ .../policy/common/parameters/ValueValidator.java | 158 ++++++++ .../common/parameters/annotations/Entries.java | 45 +++ .../common/parameters/annotations/Items.java | 66 ++++ .../common/parameters/annotations/Pattern.java | 37 ++ .../common/parameters/annotations/Valid.java | 34 ++ 10 files changed, 996 insertions(+), 179 deletions(-) create mode 100644 common-parameters/src/main/java/org/onap/policy/common/parameters/EntryValidator.java create mode 100644 common-parameters/src/main/java/org/onap/policy/common/parameters/FieldValidator.java create mode 100644 common-parameters/src/main/java/org/onap/policy/common/parameters/ItemValidator.java create mode 100644 common-parameters/src/main/java/org/onap/policy/common/parameters/ValueValidator.java create mode 100644 common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Entries.java create mode 100644 common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Items.java create mode 100644 common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Pattern.java create mode 100644 common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Valid.java diff --git a/common-parameters/pom.xml b/common-parameters/pom.xml index c7136bd3..c0efcf70 100644 --- a/common-parameters/pom.xml +++ b/common-parameters/pom.xml @@ -31,6 +31,10 @@ [${project.parent.artifactId}] module provides common property and parameter handling the ONAP Policy Framework + + com.google.re2j + re2j + org.apache.commons commons-lang3 diff --git a/common-parameters/src/main/java/org/onap/policy/common/parameters/BeanValidator.java b/common-parameters/src/main/java/org/onap/policy/common/parameters/BeanValidator.java index dbd3c7ce..3f5abccc 100644 --- a/common-parameters/src/main/java/org/onap/policy/common/parameters/BeanValidator.java +++ b/common-parameters/src/main/java/org/onap/policy/common/parameters/BeanValidator.java @@ -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. - *

- * 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> 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 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 void addAnnotation(Class clazz, Field field, List> checkers, - Class annotClass, BiPredicate 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 not 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 index 00000000..965c95e5 --- /dev/null +++ b/common-parameters/src/main/java/org/onap/policy/common/parameters/EntryValidator.java @@ -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 void validateEntry(BeanValidationResult result, Map.Entry 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 String getName(Map.Entry 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 index 00000000..e762dc0e --- /dev/null +++ b/common-parameters/src/main/java/org/onap/policy/common/parameters/FieldValidator.java @@ -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 getAnnotation(Class 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 index 00000000..d0c027c1 --- /dev/null +++ b/common-parameters/src/main/java/org/onap/policy/common/parameters/ItemValidator.java @@ -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 getAnnotation(Class 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 getAnnotation2(Class 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 index 00000000..9095bfd0 --- /dev/null +++ b/common-parameters/src/main/java/org/onap/policy/common/parameters/ValueValidator.java @@ -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. + *

+ * 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. + *

+ * 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 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 void addAnnotation(Class 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 void addAnnotation(Class annotClass, CheckerWithAnnot 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 getAnnotation(Class 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 { + 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 index 00000000..89c9ce26 --- /dev/null +++ b/common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Entries.java @@ -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 index 00000000..d022d95e --- /dev/null +++ b/common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Items.java @@ -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 index 00000000..a30d814c --- /dev/null +++ b/common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Pattern.java @@ -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 index 00000000..a3a1d59f --- /dev/null +++ b/common-parameters/src/main/java/org/onap/policy/common/parameters/annotations/Valid.java @@ -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 { + +} -- cgit 1.2.3-korg