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}