/*-
* ============LICENSE_START=======================================================
* ONAP
* ================================================================================
* Copyright (C) 2020-2021 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.reflect.Field;
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.ClassName;
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.Size;
import org.onap.policy.common.parameters.annotations.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Bean validator, supporting the parameter annotations.
*/
public class BeanValidator {
public static final Logger logger = LoggerFactory.getLogger(BeanValidator.class);
/**
* 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) {
var 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;
}
/**
* Adds validators based on the annotations that are available.
*
* @param validator where to add the validators
*/
protected void addValidators(ValueValidator validator) {
validator.addAnnotation(NotNull.class, this::verNotNull);
validator.addAnnotation(NotBlank.class, this::verNotBlank);
validator.addAnnotation(Size.class, this::verSize);
validator.addAnnotation(Max.class, this::verMax);
validator.addAnnotation(Min.class, this::verMin);
validator.addAnnotation(Pattern.class, this::verRegex);
validator.addAnnotation(ClassName.class, this::verClassName);
validator.addAnnotation(Valid.class, this::verCascade);
}
/**
* 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()) {
var validator = makeFieldValidator(clazz, field);
validator.validateField(result, object);
}
}
/**
* 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
* @return {@code true} if the next check should be performed, {@code false} otherwise
*/
public boolean verNotNull(BeanValidationResult result, String fieldName, Object value) {
if (value == null) {
result.addResult(fieldName, xlate(value), ValidationStatus.INVALID, "is null");
return false;
}
return true;
}
/**
* 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
* @return {@code true} if the next check should be performed, {@code false} otherwise
*/
public boolean verNotBlank(BeanValidationResult result, String fieldName, Object value) {
if (value instanceof String && StringUtils.isBlank(value.toString())) {
result.addResult(fieldName, xlate(value), ValidationStatus.INVALID, "is blank");
return false;
}
return true;
}
/**
* Verifies that the value has the specified number of elements.
*
* @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
* @return {@code true} if the next check should be performed, {@code false} otherwise
*/
public boolean verSize(BeanValidationResult result, String fieldName, Size annot, Object value) {
int size;
if (value instanceof Collection) {
size = ((Collection>) value).size();
} else if (value instanceof Map) {
size = ((Map, ?>) value).size();
} else {
return true;
}
if (size < annot.min()) {
result.addResult(fieldName, xlate(value), ValidationStatus.INVALID,
"minimum number of elements: " + annot.min());
return false;
}
return true;
}
/**
* 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 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
*/
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())) {
return true;
}
} catch (RuntimeException e) {
logger.warn("validation error for regular expression: {}", annot.regexp(), e);
}
result.addResult(fieldName, xlate(value), ValidationStatus.INVALID,
"does not match regular expression " + annot.regexp());
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
* @return {@code true} if the next check should be performed, {@code false} otherwise
*/
public 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;
}
result.addResult(fieldName, xlate(value), ValidationStatus.INVALID,
"exceeds the maximum value: " + annot.value());
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
* @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
*/
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() >= min) {
return true;
}
} else if (num instanceof Float || num instanceof Double) {
if (num.doubleValue() >= min) {
return true;
}
} else {
return true;
}
result.addResult(fieldName, xlate(value), ValidationStatus.INVALID,
"is below the minimum value: " + min);
return false;
}
/**
* Verifies that the value is a valid class name.
*
* @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
*/
public boolean verClassName(BeanValidationResult result, String fieldName, Object value) {
if (!(value instanceof String)) {
return true;
}
try {
Class.forName(value.toString());
return true;
} catch (final ClassNotFoundException exp) {
result.addResult(fieldName, value, ValidationStatus.INVALID, "class is not in the classpath");
return false;
}
}
/**
* Verifies that the value is valid by recursively invoking
* {@link #validateTop(String, Object)}.
*
* @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
*/
public boolean verCascade(BeanValidationResult result, String fieldName, Object value) {
if (value == null || value instanceof Collection || value instanceof Map) {
return true;
}
BeanValidationResult result2 = (value instanceof ParameterGroup ? ((ParameterGroup) value).validate()
: validateTop(fieldName, value));
if (result2.isClean()) {
return true;
}
result.addResult(result2);
return result2.isValid();
}
/**
* 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)) {
return true;
}
Collection> list = (Collection>) value;
var result2 = new BeanValidationResult(fieldName, value);
var 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 keyValidator validator for an individual key within the Map entry
* @param valueValidator validator for an individual value within the Map entry
* @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, ValueValidator keyValidator,
ValueValidator valueValidator, Object value) {
if (!(value instanceof Map)) {
return true;
}
Map, ?> map = (Map, ?>) value;
var result2 = new BeanValidationResult(fieldName, value);
for (Entry, ?> entry : map.entrySet()) {
String name = getEntryName(entry);
var result3 = new BeanValidationResult(name, entry);
keyValidator.validateValue(result3, "key", entry.getKey());
valueValidator.validateValue(result3, "value", entry.getValue());
if (!result3.isClean()) {
result2.addResult(result3);
}
}
if (result2.isClean()) {
return true;
}
result.addResult(result2);
return false;
}
/**
* Gets a name for an entry.
*
* @param entry entry whose name is to be determined
* @return a name for the entry
*/
protected String getEntryName(Map.Entry entry) {
var key = entry.getKey();
if (key == null) {
return "";
}
return key.toString();
}
/**
* 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);
}
/**
* Translates a value to something printable, for use by
* {@link ObjectValidationResult}. This default method simply returns the original
* value.
*
* @param value value to be translated
* @return the translated value
*/
public Object xlate(Object value) {
return value;
}
}