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

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

There is a newer version: 2025.1.0-rc2
Show 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.Field;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

import javax.annotation.Nullable;

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("", "whole object");
		}

		Deque objectsToCheck = new ArrayDeque<>();
		if (ReflectionUtils.isIterable(parsedObject)) {
			objectsToCheck.addAll(checkIterableAndGetContainedFields(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: " + ToStringHelpers.toReflectiveStringHelper(object)
				+ " - Missing @Nullable annotation?");
	}

	/** Checks a single object and returns the fields it references. */
	private static List checkObjectAndGetReferencedFields(Object objectToCheck)
			throws JsonSerializationException {
		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 (ReflectionUtils.isIterable(referencedObject)) {
					referencedObjects.addAll(checkIterableAndGetContainedFields(referencedObject, field.getName()));
					// We don't need to add the container object to referenced
					// objects
					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;
	}

	/** Check */
	private static List checkIterableAndGetContainedFields(Object arrayOrIterable, String checkedField)
			throws JsonSerializationException {
		List containedObjects = new ArrayList<>();
		for (Object containedObject : ReflectionUtils.asIterable(arrayOrIterable)) {
			if (containedObject == null) {
				throw createNullObjectException(arrayOrIterable, "at field " + checkedField);
			}
			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.isAnnotationPresent(Nullable.class)
				|| field.getName().contains("$");
	}
}