001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 the ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| You may obtain a copy of the License at                                  |
008|                                                                          |
009|    http://www.apache.org/licenses/LICENSE-2.0                            |
010|                                                                          |
011| Unless required by applicable law or agreed to in writing, software      |
012| distributed under the License is distributed on an "AS IS" BASIS,        |
013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
014| See the License for the specific language governing permissions and      |
015| limitations under the License.                                           |
016+-------------------------------------------------------------------------*/
017package org.conqat.engine.commons.util;
018
019import java.lang.reflect.Field;
020import java.util.ArrayDeque;
021import java.util.ArrayList;
022import java.util.Deque;
023import java.util.List;
024
025import javax.annotation.Nullable;
026
027import org.conqat.engine.core.core.ConQATException;
028import org.conqat.lib.commons.collections.IdentityHashSet;
029import org.conqat.lib.commons.reflect.ReflectionUtils;
030import org.conqat.lib.commons.string.StringUtils;
031
032import com.teamscale.commons.lang.ToStringHelpers;
033
034/**
035 * Validator that checks whether all values of an object are non-null unless
036 * explicitly specified as {@link Nullable}.
037 */
038public class NullableFieldValidator {
039
040        /**
041         * Ensures that the given object is not null and that all its fields are set
042         * (throws {@link ConQATException} otherwise). Arrays and other iterables are
043         * checked recursively, as well as all fields of the given object (but only if
044         * their type is defined in ConQAT/Teamscale packages).
045         *
046         * If you get a false positive for a field from this method, annotate the field
047         * as {@link Nullable}.
048         */
049        public static <T> T ensureAllFieldsNonNull(T parsedObject, String queryContent) throws ConQATException {
050                if (StringUtils.isEmpty(queryContent)) {
051                        // Returns null, but that's okay for empty queries
052                        return parsedObject;
053                }
054
055                try {
056                        return ensureAllFieldsNonNull(parsedObject);
057                } catch (ConQATException e) {
058                        throw new ConQATException(e.getMessage() + "\nQuery: " + queryContent, e);
059                }
060        }
061
062        /**
063         * Ensures that the given object is not null and that all its fields are set
064         * (throws {@link ConQATException} otherwise). Arrays and other iterables are
065         * checked recursively, as well as all fields of the given object (but only if
066         * their type is defined in ConQAT/Teamscale packages).
067         *
068         * If you get a false positive for a field from this method, annotate the field
069         * as {@link Nullable}.
070         */
071        public static <T> T ensureAllFieldsNonNull(T parsedObject) throws ConQATException {
072                if (parsedObject == null) {
073                        throw createNullObjectException("<null>", "whole object");
074                }
075
076                Deque<Object> objectsToCheck = new ArrayDeque<>();
077                if (ReflectionUtils.isIterable(parsedObject)) {
078                        objectsToCheck.addAll(checkIterableAndGetContainedFields(parsedObject, "<object in given iterable>"));
079                } else {
080                        objectsToCheck.push(parsedObject);
081                }
082
083                IdentityHashSet<Object> visitedObjects = new IdentityHashSet<>();
084                while (!objectsToCheck.isEmpty()) {
085                        Object objectToCheck = objectsToCheck.pop();
086                        if (!visitedObjects.add(objectToCheck)) {
087                                // We have already checked this object
088                                continue;
089                        }
090                        objectsToCheck.addAll(checkObjectAndGetReferencedFields(objectToCheck));
091                }
092                return parsedObject;
093        }
094
095        /** Create a null object exception. */
096        private static <T> ConQATException createNullObjectException(T object, String checkedPart) {
097                return new ConQATException("The query contained input that would have been deserialized to null (" + checkedPart
098                                + ").\nResulting object: " + ToStringHelpers.toReflectiveStringHelper(object)
099                                + " - Missing @Nullable annotation?");
100        }
101
102        /** Checks a single object and returns the fields it references. */
103        private static List<Object> checkObjectAndGetReferencedFields(Object objectToCheck) throws ConQATException {
104                List<Object> referencedObjects = new ArrayList<>();
105                for (Field field : ReflectionUtils.getAllFields(objectToCheck.getClass())) {
106                        if (JsonUtils.isNotSerialized(field) || fieldMaySkipNullCheck(field)) {
107                                continue;
108                        }
109
110                        field.setAccessible(true);
111                        try {
112                                Object referencedObject = field.get(objectToCheck);
113                                if (referencedObject == null) {
114                                        throw createNullObjectException(objectToCheck, "at field " + field.getName());
115                                }
116
117                                if (ReflectionUtils.isIterable(referencedObject)) {
118                                        referencedObjects.addAll(checkIterableAndGetContainedFields(referencedObject, field.getName()));
119                                        // We don't need to add the container object to referenced
120                                        // objects
121                                        continue;
122                                }
123
124                                // Check for correct package. This can only be done down here, if we included it
125                                // in fieldMaySkipNullCheck() we would also skip over iterable Java core types
126                                if (field.getType().getPackage() != null
127                                                && StringUtils.containsOneOf(field.getType().getPackage().getName(), "teamscale", "conqat")) {
128                                        referencedObjects.add(referencedObject);
129                                }
130                        } catch (IllegalArgumentException | IllegalAccessException e) {
131                                throw new ConQATException("Could not access field", e);
132                        }
133                }
134                return referencedObjects;
135        }
136
137        /** Check */
138        private static List<Object> checkIterableAndGetContainedFields(Object arrayOrIterable, String checkedField)
139                        throws ConQATException {
140                List<Object> containedObjects = new ArrayList<>();
141                for (Object containedObject : ReflectionUtils.asIterable(arrayOrIterable)) {
142                        if (containedObject == null) {
143                                throw createNullObjectException(arrayOrIterable, "at field " + checkedField);
144                        }
145                        containedObjects.add(containedObject);
146                }
147                return containedObjects;
148        }
149
150        /**
151         * Whether a field is relevant for the null check, i.e. it (a) can be null, and
152         * (b) is not expected to actually be null.
153         */
154        private static boolean fieldMaySkipNullCheck(Field field) {
155                return field.getType().isPrimitive() || field.isAnnotationPresent(Nullable.class)
156                                || field.getName().contains("$");
157        }
158}