From ce4ba883cc815e107dd8671ccaa03b64ae08b840 Mon Sep 17 00:00:00 2001 From: Jim Hahn Date: Thu, 30 Jan 2020 00:11:26 -0500 Subject: Add a bean validator Added a bean validator that will validate the fields within an arbitrary object using the annotations in the parameters package. Also added validateMap to the bean validators. Issue-ID: POLICY-1625 Signed-off-by: Jim Hahn Change-Id: I2192f3d1ba735d3779c35711a7dba053918aa547 --- .../common/parameters/BeanValidationResult.java | 35 +- .../policy/common/parameters/BeanValidator.java | 353 +++++++++++ .../parameters/TestBeanValidationResult.java | 60 +- .../common/parameters/TestBeanValidator.java | 651 +++++++++++++++++++++ 4 files changed, 1093 insertions(+), 6 deletions(-) create mode 100644 common-parameters/src/main/java/org/onap/policy/common/parameters/BeanValidator.java create mode 100644 common-parameters/src/test/java/org/onap/policy/common/parameters/TestBeanValidator.java diff --git a/common-parameters/src/main/java/org/onap/policy/common/parameters/BeanValidationResult.java b/common-parameters/src/main/java/org/onap/policy/common/parameters/BeanValidationResult.java index f8eebcf1..752e4d40 100644 --- a/common-parameters/src/main/java/org/onap/policy/common/parameters/BeanValidationResult.java +++ b/common-parameters/src/main/java/org/onap/policy/common/parameters/BeanValidationResult.java @@ -1,6 +1,6 @@ -/* +/*- * ============LICENSE_START======================================================= - * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2019-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. @@ -23,6 +23,9 @@ package org.onap.policy.common.parameters; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.BiConsumer; import java.util.function.Function; /** @@ -128,6 +131,34 @@ public class BeanValidationResult extends ValidationResultImpl { } } + /** + * Validates the entries in a map. + * + * @param mapName name of the list + * @param map map whose entries are to be validated, or {@code null} + * @param entryValidator function to validate an entry in the map + * @return {@code true} if all entries in the map are valid, {@code false} otherwise + */ + public boolean validateMap(String mapName, Map map, + BiConsumer> entryValidator) { + if (map == null) { + return true; + } + + BeanValidationResult result = new BeanValidationResult(mapName, null); + for (Entry ent : map.entrySet()) { + entryValidator.accept(result, ent); + } + + if (result.isValid()) { + return true; + + } else { + addResult(result); + return false; + } + } + /** * Gets the validation result. * 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 new file mode 100644 index 00000000..dbd3c7ce --- /dev/null +++ b/common-parameters/src/main/java/org/onap/policy/common/parameters/BeanValidator.java @@ -0,0 +1,353 @@ +/*- + * ============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 java.util.ArrayList; +import java.util.List; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import org.apache.commons.lang3.StringUtils; +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; + +/** + * 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 + * "getter" method, then it throws an exception. Otherwise, it validates the retrieved + * value based on the annotations. This recurses through super classes looking for + * fields to be verified, but it does not examine any interfaces. + * + * @param name name of the object being validated + * @param object object to be validated. If {@code null}, then an empty result is + * returned + * @return the validation result + */ + public BeanValidationResult validateTop(String name, Object object) { + BeanValidationResult result = new BeanValidationResult(name, object); + if (object == null) { + return result; + } + + // check class hierarchy - don't need to check interfaces + for (Class clazz = object.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + validateFields(result, object, clazz); + } + + return result; + } + + /** + * 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, to be examined for fields to be + * verified + */ + private void validateFields(BeanValidationResult result, Object object, Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + validateField(result, object, clazz, field); + } + } + + /** + * Performs validation of a single field. + * + * @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 + */ + private void classOnly(String exceptionMessage) { + if (fieldIsAnnotated) { + throw new IllegalArgumentException(exceptionMessage); + } + } + + /** + * 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. + * + * @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 + */ + 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; + } + + T annot2 = annot; + checkers.add(value -> check.test(annot2, value)); + } + + /** + * Verifies that the value is not 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 + */ + private boolean verNotNull(BeanValidationResult result, String fieldName, Object value) { + return result.validateNotNull(fieldName, value); + } + + /** + * Verifies that the value is not blank. + * + * @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 + */ + 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; + } + + return true; + } + + /** + * 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 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 + */ + private boolean verMax(BeanValidationResult result, String fieldName, Max annot, Object value) { + if (!(value instanceof Number)) { + return true; + } + + Number num = (Number) value; + if (num instanceof Integer || num instanceof Long) { + if (num.longValue() <= annot.value()) { + return true; + } + + } else if (num instanceof Float || num instanceof Double) { + if (num.doubleValue() <= annot.value()) { + return true; + } + + } else { + return true; + } + + ObjectValidationResult fieldResult = new ObjectValidationResult(fieldName, value, ValidationStatus.INVALID, + "exceeds the maximum value: " + annot.value()); + result.addResult(fieldResult); + return false; + } + + /** + * 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 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 + */ + private boolean verMin(BeanValidationResult result, String fieldName, Min annot, Object value) { + if (!(value instanceof Number)) { + return true; + } + + Number num = (Number) value; + if (num instanceof Integer || num instanceof Long) { + if (num.longValue() >= annot.value()) { + return true; + } + + } else if (num instanceof Float || num instanceof Double) { + if (num.doubleValue() >= annot.value()) { + return true; + } + + } else { + return true; + } + + ObjectValidationResult fieldResult = new ObjectValidationResult(fieldName, value, ValidationStatus.INVALID, + "is below the minimum value: " + annot.value()); + result.addResult(fieldResult); + return false; + } + + /** + * 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/test/java/org/onap/policy/common/parameters/TestBeanValidationResult.java b/common-parameters/src/test/java/org/onap/policy/common/parameters/TestBeanValidationResult.java index 12cd80cb..8f978c69 100644 --- a/common-parameters/src/test/java/org/onap/policy/common/parameters/TestBeanValidationResult.java +++ b/common-parameters/src/test/java/org/onap/policy/common/parameters/TestBeanValidationResult.java @@ -1,8 +1,8 @@ -/* +/*- * ============LICENSE_START======================================================= * ONAP * ================================================================================ - * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2019-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. @@ -27,17 +27,25 @@ import static org.junit.Assert.assertTrue; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.function.BiConsumer; import org.junit.Before; import org.junit.Test; public class TestBeanValidationResult { + private static final String TEXT1 = "abc"; + private static final String TEXT2 = "def"; private static final String MY_LIST = "my-list"; + private static final String MY_MAP = "my-map"; private static final String OBJECT = "an object"; private static final String INITIAL_INDENT = "xx "; private static final String NEXT_INDENT = "yy "; private static final String MID_INDENT = "xx yy "; private static final String NAME = "my-name"; private static final String MY_LIST_INVALID = " 'my-list' INVALID, item has status INVALID\n "; + private static final String MY_MAP_INVALID = " 'my-map' INVALID, item has status INVALID\n "; private static final String BEAN_INVALID_MSG = requote("'my-name' INVALID, item has status INVALID\n"); private String cleanMsg; @@ -52,10 +60,10 @@ public class TestBeanValidationResult { */ @Before public void setUp() { - clean = new ObjectValidationResult("abc", 10); + clean = new ObjectValidationResult(TEXT1, 10); cleanMsg = clean.getResult("", "", true); - invalid = new ObjectValidationResult("def", 20); + invalid = new ObjectValidationResult(TEXT2, 20); invalid.setResult(ValidationStatus.INVALID, "invalid"); invalidMsg = invalid.getResult(); @@ -150,6 +158,50 @@ public class TestBeanValidationResult { } + @Test + public void testValidateMap() { + Map map = null; + bean = new BeanValidationResult(NAME, OBJECT); + assertTrue(bean.validateMap(MY_MAP, map, validMapEntry())); + assertTrue(bean.isValid()); + assertNull(bean.getResult()); + + map = Map.of(TEXT1, clean, TEXT2, clean); + bean = new BeanValidationResult(NAME, OBJECT); + assertTrue(bean.validateMap(MY_MAP, map, validMapEntry())); + assertTrue(bean.isValid()); + assertNull(bean.getResult()); + + // null value in the map + map = new TreeMap<>(); + map.put(TEXT1, clean); + map.put(TEXT2, null); + bean = new BeanValidationResult(NAME, OBJECT); + assertFalse(bean.validateMap(MY_MAP, map, validMapEntry())); + assertFalse(bean.isValid()); + assertEquals(requote(BEAN_INVALID_MSG + MY_MAP_INVALID + + "item 'def' value 'null' INVALID, is null\n"), bean.getResult()); + + map = Map.of(TEXT1, invalid, TEXT2, invalid); + bean = new BeanValidationResult(NAME, OBJECT); + assertFalse(bean.validateMap(MY_MAP, map, validMapEntry())); + assertFalse(bean.isValid()); + assertEquals(requote(BEAN_INVALID_MSG + MY_MAP_INVALID + invalidMsg + + " " + invalidMsg), bean.getResult()); + + } + + private BiConsumer> validMapEntry() { + return (result, entry) -> { + var value = entry.getValue(); + if (value == null) { + result.validateNotNull(entry.getKey(), value); + } else { + result.addResult(value); + } + }; + } + private static String requote(String text) { return text.replace('\'', '"'); } diff --git a/common-parameters/src/test/java/org/onap/policy/common/parameters/TestBeanValidator.java b/common-parameters/src/test/java/org/onap/policy/common/parameters/TestBeanValidator.java new file mode 100644 index 00000000..f1e468b0 --- /dev/null +++ b/common-parameters/src/test/java/org/onap/policy/common/parameters/TestBeanValidator.java @@ -0,0 +1,651 @@ +/*- + * ============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 static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import lombok.AccessLevel; +import lombok.Getter; +import org.junit.Before; +import org.junit.Test; +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; + +public class TestBeanValidator { + private static final String GET_MSG = "\"get\""; + private static final IllegalStateException EXPECTED_EXCEPTION = new IllegalStateException("expected exception"); + private static final String TOP = "top"; + private static final String STR_FIELD = "strValue"; + private static final String INT_FIELD = "intValue"; + private static final String NUM_FIELD = "numValue"; + private static final String BOOL_FIELD = "boolValue"; + private static final String STRING_VALUE = "string value"; + private static final int INT_VALUE = 20; + + private BeanValidator validator; + + @Before + public void setUp() { + validator = new BeanValidator(); + } + + @Test + public void testValidateTop_testValidateFields() { + // validate null + assertTrue(validator.validateTop(TOP, null).isValid()); + + // validate something that has no annotations + assertTrue(validator.validateTop(TOP, validator).isValid()); + + @NotNull + @Getter + class Data { + String strValue; + } + + // one failure case + Data data = new Data(); + BeanValidationResult result = validator.validateTop(TOP, data); + assertInvalid("testValidateFields", result, STR_FIELD, "null"); + assertTrue(result.getResult().contains(TOP)); + + // one success case + data.strValue = STRING_VALUE; + assertTrue(validator.validateTop(TOP, data).isValid()); + + /** + * Repeat with a subclass. + */ + @Getter + class Derived extends Data { + @Min(10) + int intValue; + } + + Derived derived = new Derived(); + derived.strValue = STRING_VALUE; + derived.intValue = INT_VALUE; + + // success case + assertTrue(validator.validateTop(TOP, derived).isValid()); + + // failure cases + derived.strValue = null; + assertInvalid("testValidateFields", validator.validateTop(TOP, derived), STR_FIELD, "null"); + derived.strValue = STRING_VALUE; + + derived.intValue = 1; + assertInvalid("testValidateFields", validator.validateTop(TOP, derived), INT_FIELD, "minimum"); + derived.intValue = INT_VALUE; + + // both invalid + derived.strValue = null; + derived.intValue = 1; + result = validator.validateTop(TOP, derived); + assertInvalid("testValidateFields", result, STR_FIELD, "null"); + assertInvalid("testValidateFields", result, INT_FIELD, "minimum"); + derived.strValue = STRING_VALUE; + derived.intValue = INT_VALUE; + } + + @Test + public void testValidateField() { + /* + * Note: nested classes contain fields like "$this", thus the check for "$" in the + * variable name is already covered by the other tests. + */ + + /* + * Class with no annotations. + */ + class NoAnnotations { + @SuppressWarnings("unused") + String strValue; + } + + NoAnnotations noAnnot = new NoAnnotations(); + noAnnot.strValue = null; + assertTrue(validator.validateTop(TOP, noAnnot).isValid()); + + /* + * Class containing a static field with an annotation. + */ + AnnotFieldStatic annotFieldStatic = new AnnotFieldStatic(); + assertThatIllegalArgumentException().isThrownBy(() -> validator.validateTop(TOP, annotFieldStatic)) + .withMessageContaining(STR_FIELD).withMessageContaining("static"); + + /* + * Class containing a static field, with an annotation at the class level. + */ + AnnotClassStatic annotClassStatic = new AnnotClassStatic(); + assertTrue(validator.validateTop(TOP, annotClassStatic).isValid()); + + /* + * Class with no getter method, with field-level annotation. + */ + class NoGetter { + @NotNull + String strValue; + } + + NoGetter noGetter = new NoGetter(); + assertThatIllegalArgumentException().isThrownBy(() -> validator.validateTop(TOP, noGetter)) + .withMessageContaining(STR_FIELD).withMessageContaining(GET_MSG); + + /* + * Class with no getter method, with class-level annotation. + */ + @NotNull + class ClassNoGetter { + @SuppressWarnings("unused") + String strValue; + } + + ClassNoGetter classNoGetter = new ClassNoGetter(); + assertTrue(validator.validateTop(TOP, classNoGetter).isValid()); + + /* + * Class with "blank", but no "null" check. Value is null. + */ + class NoNullCheck { + @NotBlank + @Getter + String strValue; + } + + NoNullCheck noNullCheck = new NoNullCheck(); + assertTrue(validator.validateTop(TOP, noNullCheck).isValid()); + + /* + * Class with conflicting minimum and maximum, where the value doesn't satisfy + * either of them. This should only generate one result, rather than one for each + * check. Note: the "max" check occurs before the "min" check, so that's the one + * we expect in the result. + */ + class MinAndMax { + @Getter + @Min(200) + @Max(100) + Integer intValue; + } + + MinAndMax minAndMax = new MinAndMax(); + minAndMax.intValue = 150; + BeanValidationResult result = validator.validateTop(INT_FIELD, minAndMax); + assertFalse(result.isValid()); + assertInvalid("testValidateField", result, INT_FIELD, "maximum"); + assertFalse(result.getResult().contains("minimum")); + } + + @Test + public void testGetValue() { + /* + * Class where the getter throws an exception. + */ + class GetExcept { + @NotNull + String strValue; + + @SuppressWarnings("unused") + public String getStrValue() { + throw EXPECTED_EXCEPTION; + } + } + + GetExcept getExcept = new GetExcept(); + assertThatIllegalArgumentException().isThrownBy(() -> validator.validateTop(TOP, getExcept)) + .withMessageContaining(STR_FIELD).withMessageContaining("accessor threw"); + } + + @Test + public void testVerNotNull() { + class NotNullCheck { + @Getter + @Min(1) + @NotNull + Integer intValue; + } + + NotNullCheck notNullCheck = new NotNullCheck(); + assertInvalid("testVerNotNull", validator.validateTop(TOP, notNullCheck), INT_FIELD, "null"); + + notNullCheck.intValue = INT_VALUE; + assertTrue(validator.validateTop(TOP, notNullCheck).isValid()); + + notNullCheck.intValue = 0; + assertInvalid("testVerNotNull", validator.validateTop(TOP, notNullCheck), INT_FIELD, "minimum"); + } + + @Test + public void testVerNotBlank() { + class NotBlankCheck { + @Getter + @NotBlank + String strValue; + } + + NotBlankCheck notBlankCheck = new NotBlankCheck(); + + // null + assertTrue(validator.validateTop(TOP, notBlankCheck).isValid()); + + // empty + notBlankCheck.strValue = ""; + assertInvalid("testVerNotNull", validator.validateTop(TOP, notBlankCheck), STR_FIELD, "blank"); + + // spaces + notBlankCheck.strValue = " "; + assertInvalid("testVerNotNull", validator.validateTop(TOP, notBlankCheck), STR_FIELD, "blank"); + + // not blank + notBlankCheck.strValue = STRING_VALUE; + assertTrue(validator.validateTop(TOP, notBlankCheck).isValid()); + + /* + * Class with "blank" annotation on an integer. + */ + class NotBlankInt { + @Getter + @NotBlank + int intValue; + } + + NotBlankInt notBlankInt = new NotBlankInt(); + notBlankInt.intValue = 0; + assertTrue(validator.validateTop(TOP, notBlankInt).isValid()); + } + + @Test + public void testVerMax() { + /* + * Field is not a number. + */ + class NonNumeric { + @Getter + @Max(100) + String strValue; + } + + NonNumeric nonNumeric = new NonNumeric(); + nonNumeric.strValue = STRING_VALUE; + assertTrue(validator.validateTop(TOP, nonNumeric).isValid()); + + /* + * Integer field. + */ + class IntField { + @Getter + @Max(100) + Integer intValue; + } + + // ok value + IntField intField = new IntField(); + assertNumeric("testVerMax-integer", intField, value -> { + intField.intValue = value; + }, INT_FIELD, "maximum", INT_VALUE, 100, 101); + + /* + * Long field. + */ + class LongField { + @Getter + @Max(100) + Long numValue; + } + + // ok value + LongField longField = new LongField(); + assertNumeric("testVerMax-long", longField, value -> { + longField.numValue = (long) value; + }, NUM_FIELD, "maximum", INT_VALUE, 100, 101); + + /* + * Float field. + */ + class FloatField { + @Getter + @Max(100) + Float numValue; + } + + // ok value + FloatField floatField = new FloatField(); + assertNumeric("testVerMax-float", floatField, value -> { + floatField.numValue = (float) value; + }, NUM_FIELD, "maximum", INT_VALUE, 100, 101); + + /* + * Double field. + */ + class DoubleField { + @Getter + @Max(100) + Double numValue; + } + + // ok value + DoubleField doubleField = new DoubleField(); + assertNumeric("testVerMax-double", doubleField, value -> { + doubleField.numValue = (double) value; + }, NUM_FIELD, "maximum", INT_VALUE, 100, 101); + + /* + * Atomic Integer field (which is a subclass of Number). + */ + class AtomIntValue { + @Getter + @Max(100) + AtomicInteger numValue; + } + + // ok value + AtomIntValue atomIntField = new AtomIntValue(); + atomIntField.numValue = new AtomicInteger(INT_VALUE); + assertTrue(validator.validateTop(TOP, atomIntField).isValid()); + + // invalid value - should be OK, because it isn't an Integer + atomIntField.numValue.set(101); + assertTrue(validator.validateTop(TOP, atomIntField).isValid()); + } + + @Test + public void testVerMin() { + /* + * Field is not a number. + */ + class NonNumeric { + @Getter + @Min(10) + String strValue; + } + + NonNumeric nonNumeric = new NonNumeric(); + nonNumeric.strValue = STRING_VALUE; + assertTrue(validator.validateTop(TOP, nonNumeric).isValid()); + + /* + * Integer field. + */ + class IntField { + @Getter + @Min(10) + Integer intValue; + } + + // ok value + IntField intField = new IntField(); + assertNumeric("testVerMin-integer", intField, value -> { + intField.intValue = value; + }, INT_FIELD, "minimum", INT_VALUE, 10, 1); + + /* + * Long field. + */ + class LongField { + @Getter + @Min(10) + Long numValue; + } + + // ok value + LongField longField = new LongField(); + assertNumeric("testVerMin-long", longField, value -> { + longField.numValue = (long) value; + }, NUM_FIELD, "minimum", INT_VALUE, 10, 1); + + /* + * Float field. + */ + class FloatField { + @Getter + @Min(10) + Float numValue; + } + + // ok value + FloatField floatField = new FloatField(); + assertNumeric("testVerMin-float", floatField, value -> { + floatField.numValue = (float) value; + }, NUM_FIELD, "minimum", INT_VALUE, 10, 1); + + /* + * Double field. + */ + class DoubleField { + @Getter + @Min(10) + Double numValue; + } + + // ok value + DoubleField doubleField = new DoubleField(); + assertNumeric("testVerMin-double", doubleField, value -> { + doubleField.numValue = (double) value; + }, NUM_FIELD, "minimum", INT_VALUE, 10, 1); + + /* + * Atomic Integer field (which is a subclass of Number). + */ + class AtomIntValue { + @Getter + @Min(10) + AtomicInteger numValue; + } + + // ok value + AtomIntValue atomIntField = new AtomIntValue(); + atomIntField.numValue = new AtomicInteger(INT_VALUE); + assertTrue(validator.validateTop(TOP, atomIntField).isValid()); + + // invalid value - should be OK, because it isn't an Integer + atomIntField.numValue.set(101); + assertTrue(validator.validateTop(TOP, atomIntField).isValid()); + } + + private void assertNumeric(String testName, T object, Consumer setter, String fieldName, + String expectedText, int inside, int edge, int outside) { + setter.accept(inside); + assertTrue(validator.validateTop(TOP, object).isValid()); + + // on the edge + setter.accept(edge); + assertTrue(validator.validateTop(TOP, object).isValid()); + + // invalid + setter.accept(outside); + assertInvalid("testVerNotNull", validator.validateTop(TOP, object), fieldName, expectedText); + } + + @Test + public void testGetAccessor() { + /* + * Class with "get" method has been tested through-out this junit, so no need to + * do more. + */ + + /* + * Class with "is" method. + */ + class IsField { + @NotNull + Boolean boolValue; + + @SuppressWarnings("unused") + public Boolean isBoolValue() { + return boolValue; + } + } + + // ok value + IsField isField = new IsField(); + isField.boolValue = true; + assertTrue(validator.validateTop(TOP, isField).isValid()); + + // invalid value + isField.boolValue = null; + assertInvalid("testGetAccessor", validator.validateTop(TOP, isField), BOOL_FIELD, "null"); + } + + @Test + public void testGetMethod() { + /* + * Class with some fields annotated and some not. + */ + @Getter + class Mixed { + Integer intValue; + + @NotNull + String strValue; + } + + // invalid + Mixed mixed = new Mixed(); + BeanValidationResult result = validator.validateTop(TOP, mixed); + assertInvalid("testGetMethod", result, STR_FIELD, "null"); + assertFalse(result.getResult().contains(INT_FIELD)); + + // intValue is null, but it isn't annotated so this should be valid + mixed.strValue = STRING_VALUE; + assertTrue(validator.validateTop(TOP, mixed).isValid()); + } + + @Test + public void testValidMethod() { + + /* + * Plain getter. + */ + class PlainGetter { + @NotNull + @Getter + String strValue; + } + + // invalid + PlainGetter plainGetter = new PlainGetter(); + assertInvalid("testValidMethod", validator.validateTop(TOP, plainGetter), STR_FIELD, "null"); + + // valid + plainGetter.strValue = STRING_VALUE; + assertTrue(validator.validateTop(TOP, plainGetter).isValid()); + + /* + * Static getter - should throw an exception. + */ + StaticGetter staticGetter = new StaticGetter(); + assertThatIllegalArgumentException().isThrownBy(() -> validator.validateTop(TOP, staticGetter)) + .withMessageContaining(STR_FIELD).withMessageContaining(GET_MSG); + + /* + * Protected getter - should throw an exception. + */ + class ProtectedGetter { + @NotNull + @Getter(AccessLevel.PROTECTED) + String strValue; + } + + ProtectedGetter protectedGetter = new ProtectedGetter(); + assertThatIllegalArgumentException().isThrownBy(() -> validator.validateTop(TOP, protectedGetter)) + .withMessageContaining(STR_FIELD).withMessageContaining(GET_MSG); + + /* + * getter is a "void" function - should throw an exception. + */ + class VoidGetter { + @NotNull + String strValue; + + @SuppressWarnings("unused") + public void getStrValue() { + // do nothing + } + } + + VoidGetter voidGetter = new VoidGetter(); + assertThatIllegalArgumentException().isThrownBy(() -> validator.validateTop(TOP, voidGetter)) + .withMessageContaining(STR_FIELD).withMessageContaining(GET_MSG); + + /* + * getter takes an argument - should throw an exception. + */ + class ArgGetter { + @NotNull + String strValue; + + @SuppressWarnings("unused") + public String getStrValue(String echo) { + return echo; + } + } + + ArgGetter argGetter = new ArgGetter(); + assertThatIllegalArgumentException().isThrownBy(() -> validator.validateTop(TOP, argGetter)) + .withMessageContaining(STR_FIELD).withMessageContaining(GET_MSG); + } + + + private void assertInvalid(String testName, BeanValidationResult result, String fieldName, String message) { + String text = result.getResult(); + assertNotNull(testName, text); + assertTrue(testName, text.contains(fieldName)); + assertTrue(testName, text.contains(message)); + } + + /** + * Annotated static field. + */ + private static class AnnotFieldStatic { + @NotNull + static String strValue; + } + + /** + * Annotated class with a static field. + */ + @NotNull + private static class AnnotClassStatic { + @SuppressWarnings("unused") + static String strValue; + } + + /** + * Class with an annotated field, but a static "getter". + */ + private static class StaticGetter { + @NotNull + String strValue; + + @SuppressWarnings("unused") + public static String getStrValue() { + return STRING_VALUE; + } + } +} -- cgit 1.2.3-korg