All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.conqat.engine.commons.util.NullableFieldValidator Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) CQSE GmbH
 *
 * 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.
 */

package org.conqat.engine.commons.util;

import java.lang.reflect.AnnotatedArrayType;
import java.lang.reflect.AnnotatedParameterizedType;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Map;

import org.apache.commons.text.StringEscapeUtils;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.conqat.lib.commons.collections.CollectionMap;
import org.conqat.lib.commons.collections.IdentityHashSet;
import org.conqat.lib.commons.reflect.ReflectionUtils;
import org.conqat.lib.commons.string.StringUtils;

import com.teamscale.commons.lang.ToStringHelpers;

/**
 * Validator that checks whether all values of an object are non-null unless explicitly specified as
 * {@link Nullable}.
 */
public class NullableFieldValidator {

	/**
	 * Ensures that the given object is not null and that all its fields are set (throws
	 * {@link JsonSerializationException} otherwise). Arrays and other iterables are checked
	 * recursively, as well as all fields of the given object (but only if their type is defined in
	 * ConQAT/Teamscale packages).
	 * 

* If you get a false positive for a field from this method, annotate the field as {@link Nullable}. */ public static T ensureAllFieldsNonNull(T parsedObject, String queryContent) throws JsonSerializationException { if (StringUtils.isEmpty(queryContent)) { // Returns null, but that's okay for empty queries return parsedObject; } try { return ensureAllFieldsNonNull(parsedObject); } catch (JsonSerializationException e) { throw new JsonSerializationException(e.getMessage() + "\nQuery: " + queryContent, e); } } /** * Ensures that the given object is not null and that all its fields are set (throws * {@link JsonSerializationException} otherwise). Arrays and other iterables are checked * recursively, as well as all fields of the given object (but only if their type is defined in * ConQAT/Teamscale packages). *

* If you get a false positive for a field from this method, annotate the field as {@link Nullable}. */ public static T ensureAllFieldsNonNull(T parsedObject) throws JsonSerializationException { if (parsedObject == null) { throw createNullObjectException(null, "whole object"); } Deque objectsToCheck = new ArrayDeque<>(); if (ReflectionUtils.isIterable(parsedObject)) { objectsToCheck.addAll(checkTopLevelIterableAndGetContainedFields(parsedObject)); } else { objectsToCheck.push(parsedObject); } IdentityHashSet visitedObjects = new IdentityHashSet<>(); while (!objectsToCheck.isEmpty()) { Object objectToCheck = objectsToCheck.pop(); if (!visitedObjects.add(objectToCheck)) { // We have already checked this object continue; } objectsToCheck.addAll(checkObjectAndGetReferencedFields(objectToCheck)); } return parsedObject; } /** Create a null object exception. */ private static JsonSerializationException createNullObjectException(T object, String checkedPart) { return new JsonSerializationException("The query contained input that would have been deserialized to null (" + checkedPart + ").\nResulting object: " + StringEscapeUtils.escapeJava(ToStringHelpers.toReflectiveStringHelper(object).toString()) + " - Missing @Nullable annotation?"); } /** Checks a single object and returns the fields it references. */ private static List checkObjectAndGetReferencedFields(Object objectToCheck) throws JsonSerializationException { if (objectToCheck instanceof Map.Entry) { // can't set the fields of a Map entry accessible if the Map class is loaded in // another module, so we have to intercept them early Map.Entry entryObjectToCheck = (Map.Entry) objectToCheck; return List.of(entryObjectToCheck.getKey(), entryObjectToCheck.getValue()); } List referencedObjects = new ArrayList<>(); for (Field field : ReflectionUtils.getAllFields(objectToCheck.getClass())) { if (JsonUtils.isNotSerialized(field) || fieldMaySkipNullCheck(field)) { continue; } field.setAccessible(true); try { Object referencedObject = field.get(objectToCheck); if (referencedObject == null) { throw createNullObjectException(objectToCheck, "at field " + field.getName()); } if (referencedObject instanceof CollectionMap) { // only look at keys and values of CollectionMaps referencedObjects.add(checkKeysAndValuesOfCollectionMap((CollectionMap) referencedObject)); } else if (ReflectionUtils.isIterable(referencedObject)) { boolean reportNullObjects = !isElementTypeNullable(field); referencedObjects.addAll( checkIterableAndGetContainedFields(referencedObject, field.getName(), reportNullObjects)); // We don't need to add the container object to referenced // objects continue; } else if (referencedObject instanceof Map) { referencedObjects.addAll(checkKeyAndValueOfMap(field, (Map) referencedObject)); // We don't need to add the Map object or its key/value collection objects to // referenced objects. But all objects contained inside the key/value // collections. continue; } // Check for correct package. This can only be done down here, if we included it // in fieldMaySkipNullCheck() we would also skip over iterable Java core types if (field.getType().getPackage() != null && StringUtils.containsOneOf(field.getType().getPackage().getName(), "teamscale", "conqat")) { referencedObjects.add(referencedObject); } } catch (IllegalArgumentException | IllegalAccessException e) { throw new JsonSerializationException("Could not access field", e); } } return referencedObjects; } /** * Runs {@link #checkIterableAndGetContainedFields(Object, String, boolean)} on all keys and values * of the given map object. Returns the objects found in and below the keys/values. */ private static List checkKeyAndValueOfMap(Field field, Map referencedObject) throws JsonSerializationException { boolean reportNullObjects = !isMapKeyTypeNullable(field); List mapEntryObjects = new ArrayList<>(); mapEntryObjects.addAll( checkIterableAndGetContainedFields(referencedObject.keySet(), field.getName(), reportNullObjects)); reportNullObjects = !isMapValueTypeNullable(field); mapEntryObjects.addAll( checkIterableAndGetContainedFields(referencedObject.values(), field.getName(), reportNullObjects)); return mapEntryObjects; } /** Returns whether the key type in the map is annotated with Nullable. */ private static boolean isMapKeyTypeNullable(Field field) { AnnotatedType annotatedType = field.getAnnotatedType(); if (annotatedType instanceof AnnotatedParameterizedType) { AnnotatedType annotatedActualTypeArgument = ((AnnotatedParameterizedType) annotatedType) .getAnnotatedActualTypeArguments()[0]; return annotatedActualTypeArgument.isAnnotationPresent(Nullable.class); } return false; } /** Returns whether the value type in the map is annotated with Nullable. */ private static boolean isMapValueTypeNullable(Field field) { AnnotatedType annotatedType = field.getAnnotatedType(); if (annotatedType instanceof AnnotatedParameterizedType) { AnnotatedType annotatedActualTypeArgument = ((AnnotatedParameterizedType) annotatedType) .getAnnotatedActualTypeArguments()[1]; return annotatedActualTypeArgument.isAnnotationPresent(Nullable.class); } return false; } /** * Returns whether the element type in the collection is annotated with Nullable. */ private static boolean isElementTypeNullable(Field field) { AnnotatedType annotatedType = field.getAnnotatedType(); if (annotatedType instanceof AnnotatedParameterizedType) { AnnotatedType annotatedActualTypeArgument = ((AnnotatedParameterizedType) annotatedType) .getAnnotatedActualTypeArguments()[0]; return annotatedActualTypeArgument.isAnnotationPresent(Nullable.class); } else if (annotatedType instanceof AnnotatedArrayType) { AnnotatedType annotatedComponentType = ((AnnotatedArrayType) annotatedType) .getAnnotatedGenericComponentType(); return annotatedComponentType.isAnnotationPresent(Nullable.class); } return false; } /** Check */ private static List checkTopLevelIterableAndGetContainedFields(Object arrayOrIterable) { List containedObjects = new ArrayList<>(); for (Object containedObject : ReflectionUtils.asIterable(arrayOrIterable)) { if (containedObject == null) { continue; } if (ReflectionUtils.isIterable(containedObject)) { containedObjects.addAll(checkTopLevelIterableAndGetContainedFields(containedObject)); } else { containedObjects.add(containedObject); } } return containedObjects; } private static List checkKeysAndValuesOfCollectionMap(CollectionMap collectionMapReference) { List containedObjects = new ArrayList<>(collectionMapReference.getKeys()); containedObjects.addAll(collectionMapReference.getValues()); return containedObjects; } /** Check */ private static List checkIterableAndGetContainedFields(Object arrayOrIterable, String checkedField, boolean reportNullObjects) throws JsonSerializationException { List containedObjects = new ArrayList<>(); for (Object containedObject : ReflectionUtils.asIterable(arrayOrIterable)) { if (containedObject == null) { if (reportNullObjects) { throw createNullObjectException(arrayOrIterable, "at field " + checkedField); } else { continue; } } containedObjects.add(containedObject); } return containedObjects; } /** * Whether a field is relevant for the null check, i.e. it (a) can be null, and (b) is not expected * to actually be null. */ private static boolean fieldMaySkipNullCheck(Field field) { return field.getType().isPrimitive() // || field.getAnnotatedType().isAnnotationPresent(Nullable.class) // || field.getName().contains("$"); } }