| 1 | package com.reallifedeveloper.tools.test.database; | |
| 2 | ||
| 3 | import java.lang.reflect.Field; | |
| 4 | import java.lang.reflect.InvocationTargetException; | |
| 5 | import java.lang.reflect.Method; | |
| 6 | import java.lang.reflect.ParameterizedType; | |
| 7 | import java.lang.reflect.Type; | |
| 8 | import java.lang.reflect.TypeVariable; | |
| 9 | import java.util.Collection; | |
| 10 | import java.util.List; | |
| 11 | import java.util.Locale; | |
| 12 | import java.util.Map; | |
| 13 | import java.util.Optional; | |
| 14 | ||
| 15 | import org.checkerframework.checker.nullness.qual.Nullable; | |
| 16 | import org.slf4j.Logger; | |
| 17 | import org.slf4j.LoggerFactory; | |
| 18 | ||
| 19 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | |
| 20 | import jakarta.persistence.Column; | |
| 21 | import jakarta.persistence.EmbeddedId; | |
| 22 | import jakarta.persistence.Entity; | |
| 23 | import jakarta.persistence.Id; | |
| 24 | import jakarta.persistence.IdClass; | |
| 25 | import jakarta.persistence.JoinColumn; | |
| 26 | import jakarta.persistence.MapKey; | |
| 27 | import jakarta.persistence.Table; | |
| 28 | import lombok.experimental.UtilityClass; | |
| 29 | ||
| 30 | /** | |
| 31 | * A utility class for working with JPA entities and mappings. | |
| 32 | * | |
| 33 | * @author RealLifeDeveloper | |
| 34 | */ | |
| 35 | @UtilityClass | |
| 36 | @SuppressWarnings("PMD") | |
| 37 | @SuppressFBWarnings(value = { "CRLF_INJECTION_LOGS", "IMPROPER_UNICODE" }) | |
| 38 | public class JpaUtil { | |
| 39 | ||
| 40 | private static final Logger LOG = LoggerFactory.getLogger(JpaUtil.class); | |
| 41 | ||
| 42 | /** | |
| 43 | * Gives the table name associated with an {@link Entity}. | |
| 44 | * | |
| 45 | * @param <T> the type of entity | |
| 46 | * @param entityType the class object representing {@code T} | |
| 47 | * @return the table name associated with {@code entityType} | |
| 48 | */ | |
| 49 | public static <T> String getTableName(Class<T> entityType) { | |
| 50 | Table table = entityType.getAnnotation(Table.class); | |
| 51 |
1
1. getTableName : negated conditional → KILLED |
if (table == null) { |
| 52 |
1
1. getTableName : replaced return value with "" for com/reallifedeveloper/tools/test/database/JpaUtil::getTableName → KILLED |
return entityType.getSimpleName(); |
| 53 | } else { | |
| 54 |
1
1. getTableName : replaced return value with "" for com/reallifedeveloper/tools/test/database/JpaUtil::getTableName → KILLED |
return table.name(); |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | /** | |
| 59 | * Gets the {@code Field} object for a field with a given name in a given object, also making the field accessible by calling | |
| 60 | * {@code Field.setAccessible(true}. | |
| 61 | * <p> | |
| 62 | * This method never returns {@code null}; if the field is not found, a {@code NoSuchFieldException} is thrown | |
| 63 | * | |
| 64 | * @param entity the object to search for the field | |
| 65 | * @param fieldName the name of the field for which to search | |
| 66 | * @return the {@code Field} object representing the field named {@code fieldName} in {@code entity | |
| 67 | * } | |
| 68 | * @throws NoSuchFieldException if the field could not be found | |
| 69 | */ | |
| 70 | public static Field getField(Object entity, String fieldName) throws NoSuchFieldException { | |
| 71 | Class<?> entityType = entity.getClass(); | |
| 72 |
1
1. getField : negated conditional → KILLED |
while (entityType != null) { |
| 73 | for (Field field : entityType.getDeclaredFields()) { | |
| 74 |
1
1. getField : negated conditional → KILLED |
if (field.getName().equalsIgnoreCase(fieldName)) { |
| 75 |
1
1. getField : removed call to java/lang/reflect/Field::setAccessible → KILLED |
field.setAccessible(true); |
| 76 |
1
1. getField : replaced return value with null for com/reallifedeveloper/tools/test/database/JpaUtil::getField → KILLED |
return field; |
| 77 | } | |
| 78 | } | |
| 79 | entityType = entityType.getSuperclass(); | |
| 80 | } | |
| 81 | throw new NoSuchFieldException(fieldName); | |
| 82 | } | |
| 83 | ||
| 84 | /** | |
| 85 | * Gets thd ID field of an entity, i.e., the field annotated with an {@code ID} annotation. | |
| 86 | * <p> | |
| 87 | * This method never returns {@code null}; if no ID field is found, an {@code IllegalStateException} is thrown. | |
| 88 | * | |
| 89 | * @param entity the entity to search | |
| 90 | * @return the {@code Field} object representing the ID field of {@code entity} | |
| 91 | */ | |
| 92 | public static Field getIdField(Object entity) { | |
| 93 | Class<?> entityType = entity.getClass(); | |
| 94 |
1
1. getIdField : negated conditional → KILLED |
while (entityType != null) { |
| 95 | for (Field field : entityType.getDeclaredFields()) { | |
| 96 |
1
1. getIdField : negated conditional → KILLED |
if (field.getDeclaredAnnotation(Id.class) != null) { |
| 97 |
1
1. getIdField : replaced return value with null for com/reallifedeveloper/tools/test/database/JpaUtil::getIdField → KILLED |
return field; |
| 98 | } | |
| 99 | } | |
| 100 | entityType = entityType.getSuperclass(); | |
| 101 | } | |
| 102 | throw new IllegalStateException("Id field not found for entity " + entity); | |
| 103 | } | |
| 104 | ||
| 105 | /** | |
| 106 | * Gets the value of the ID field of an entity. | |
| 107 | * | |
| 108 | * @param entity the entity to search | |
| 109 | * | |
| 110 | * @return the value of the ID Field | |
| 111 | * | |
| 112 | * @throws IllegalAccessException if there was a problem getting the value using reflection | |
| 113 | */ | |
| 114 | public static Object getIdValue(Object entity) throws IllegalAccessException { | |
| 115 | Field idField = JpaUtil.getIdField(entity); | |
| 116 |
1
1. getIdValue : removed call to java/lang/reflect/Field::setAccessible → KILLED |
idField.setAccessible(true); |
| 117 | Object id = idField.get(entity); | |
| 118 |
1
1. getIdValue : replaced return value with null for com/reallifedeveloper/tools/test/database/JpaUtil::getIdValue → KILLED |
return id; |
| 119 | } | |
| 120 | ||
| 121 | /** | |
| 122 | * Gets the name of the field representing a given attribute in a given class or one of its superclasses. | |
| 123 | * <p> | |
| 124 | * A field is considered to represent an attribute if one of the following is true: | |
| 125 | * <ul> | |
| 126 | * <li>The field has a {@code Column} annotation with a {@code name} equal to the attribute.</li> | |
| 127 | * <li>The field has a {@code JoinColunm} annotation with a {@code name} equal to the attribute.</li> | |
| 128 | * <li>The field name is the same as the attribute.</li> | |
| 129 | * </ul> | |
| 130 | * <p> | |
| 131 | * This method never returns {@code null}; if no matching field is found, an {@code IllegalArgumentException} is thrown. | |
| 132 | * | |
| 133 | * @param <T> the type of the entity to search | |
| 134 | * @param attributeName the attribute for which to try to find a matching field | |
| 135 | * @param entityType the class in which to search for the field, continuing with superclasses if necessary | |
| 136 | * @return the name of the field representing {@code attributeName} | |
| 137 | * @throws IllegalArgumentException if no matching field could be found | |
| 138 | */ | |
| 139 | public static <T> String getFieldName(String attributeName, Class<T> entityType) { | |
| 140 |
1
1. getFieldName : replaced return value with "" for com/reallifedeveloper/tools/test/database/JpaUtil::getFieldName → KILLED |
return getFieldName(attributeName, entityType, entityType); |
| 141 | } | |
| 142 | ||
| 143 | private static <T> String getFieldName(String attributeName, Class<T> entityType, Class<?> originalEntityType) { | |
| 144 | for (Field field : entityType.getDeclaredFields()) { | |
| 145 |
1
1. getFieldName : negated conditional → KILLED |
if (checkFieldName(attributeName, field)) { |
| 146 |
1
1. getFieldName : replaced return value with "" for com/reallifedeveloper/tools/test/database/JpaUtil::getFieldName → KILLED |
return field.getName(); |
| 147 | } | |
| 148 | } | |
| 149 |
1
1. getFieldName : negated conditional → KILLED |
if (entityType.getSuperclass() == null) { |
| 150 | throw new IllegalArgumentException("Cannot find any field matching attribute '" + attributeName.toLowerCase(Locale.getDefault()) | |
| 151 | + "' for " + originalEntityType); | |
| 152 | } else { | |
| 153 |
1
1. getFieldName : replaced return value with "" for com/reallifedeveloper/tools/test/database/JpaUtil::getFieldName → KILLED |
return getFieldName(attributeName, entityType.getSuperclass(), originalEntityType); |
| 154 | } | |
| 155 | } | |
| 156 | ||
| 157 | private static boolean checkFieldName(String attributeName, Field field) { | |
| 158 | Column column = field.getAnnotation(Column.class); | |
| 159 |
2
1. checkFieldName : negated conditional → KILLED 2. checkFieldName : negated conditional → KILLED |
if (column == null || column.name() == null) { |
| 160 | JoinColumn joinColumn = field.getAnnotation(JoinColumn.class); | |
| 161 |
2
1. checkFieldName : negated conditional → KILLED 2. checkFieldName : negated conditional → KILLED |
if (joinColumn == null || joinColumn.name() == null) { |
| 162 |
2
1. checkFieldName : replaced boolean return with false for com/reallifedeveloper/tools/test/database/JpaUtil::checkFieldName → KILLED 2. checkFieldName : replaced boolean return with true for com/reallifedeveloper/tools/test/database/JpaUtil::checkFieldName → KILLED |
return field.getName().equalsIgnoreCase(attributeName); |
| 163 | } else { | |
| 164 |
2
1. checkFieldName : replaced boolean return with false for com/reallifedeveloper/tools/test/database/JpaUtil::checkFieldName → KILLED 2. checkFieldName : replaced boolean return with true for com/reallifedeveloper/tools/test/database/JpaUtil::checkFieldName → KILLED |
return joinColumn.name().equalsIgnoreCase(attributeName); |
| 165 | } | |
| 166 | } else { | |
| 167 |
2
1. checkFieldName : replaced boolean return with false for com/reallifedeveloper/tools/test/database/JpaUtil::checkFieldName → KILLED 2. checkFieldName : replaced boolean return with true for com/reallifedeveloper/tools/test/database/JpaUtil::checkFieldName → KILLED |
return column.name().equalsIgnoreCase(attributeName); |
| 168 | } | |
| 169 | } | |
| 170 | ||
| 171 | /** | |
| 172 | * Gives the primary key class for a given entity class. | |
| 173 | * | |
| 174 | * @param <ID> the type of the primary key | |
| 175 | * @param entityType the class object representing the entity | |
| 176 | * @return the primary key class of {@code entityType} | |
| 177 | */ | |
| 178 | @SuppressWarnings("unchecked") | |
| 179 | public static <ID> Class<ID> getPrimaryKeyType(Class<?> entityType) { | |
| 180 |
1
1. getPrimaryKeyType : negated conditional → KILLED |
if (entityType.getAnnotation(Entity.class) == null) { |
| 181 | throw new IllegalArgumentException("entityType does not have @Entity annotation: entityType=" + entityType); | |
| 182 | } | |
| 183 | Class<?> entityTypeOrSuperClass = entityType; | |
| 184 | Type genericSuperclass = null; | |
| 185 |
1
1. getPrimaryKeyType : negated conditional → KILLED |
while (entityTypeOrSuperClass.getSuperclass() != null) { |
| 186 |
1
1. getPrimaryKeyType : negated conditional → KILLED |
if (entityTypeOrSuperClass.getAnnotation(IdClass.class) != null) { |
| 187 |
1
1. getPrimaryKeyType : replaced return value with null for com/reallifedeveloper/tools/test/database/JpaUtil::getPrimaryKeyType → KILLED |
return (Class<ID>) entityTypeOrSuperClass.getAnnotation(IdClass.class).value(); |
| 188 | } | |
| 189 | for (Field field : entityTypeOrSuperClass.getDeclaredFields()) { | |
| 190 |
1
1. getPrimaryKeyType : negated conditional → KILLED |
if (field.getAnnotation(EmbeddedId.class) != null) { |
| 191 |
1
1. getPrimaryKeyType : replaced return value with null for com/reallifedeveloper/tools/test/database/JpaUtil::getPrimaryKeyType → KILLED |
return (Class<ID>) field.getType(); |
| 192 | } | |
| 193 |
1
1. getPrimaryKeyType : negated conditional → KILLED |
if (field.getAnnotation(Id.class) != null) { |
| 194 |
1
1. getPrimaryKeyType : replaced return value with null for com/reallifedeveloper/tools/test/database/JpaUtil::getPrimaryKeyType → KILLED |
return (Class<ID>) getActualIdType(genericSuperclass, entityTypeOrSuperClass.getTypeParameters(), |
| 195 | field.getGenericType()).orElse((Class<Object>) field.getType()); | |
| 196 | } | |
| 197 | } | |
| 198 | genericSuperclass = entityTypeOrSuperClass.getGenericSuperclass(); | |
| 199 | entityTypeOrSuperClass = entityTypeOrSuperClass.getSuperclass(); | |
| 200 | } | |
| 201 | throw new IllegalStateException("entityType without primary key annotation: entityType=" + entityType); | |
| 202 | } | |
| 203 | ||
| 204 | @SuppressWarnings("unchecked") | |
| 205 | private static <ID> Optional<Class<ID>> getActualIdType(@Nullable Type genericEntityType, TypeVariable<?>[] typeVariables, | |
| 206 | Type genericIdType) { | |
| 207 |
1
1. getActualIdType : negated conditional → KILLED |
if (genericEntityType instanceof ParameterizedType parameterizedType) { |
| 208 | assert parameterizedType.getActualTypeArguments().length == typeVariables.length | |
| 209 | : "Number of actual type arguments (" + parameterizedType.getActualTypeArguments().length | |
| 210 | + ") differs from number of type variables (" + typeVariables.length + ")"; | |
| 211 |
2
1. getActualIdType : changed conditional boundary → SURVIVED 2. getActualIdType : negated conditional → KILLED |
for (int i = 0; i < typeVariables.length; i++) { |
| 212 | TypeVariable<?> typeVariable = typeVariables[i]; | |
| 213 |
1
1. getActualIdType : negated conditional → KILLED |
if (!typeVariable.getName().equals(genericIdType.getTypeName())) { |
| 214 | continue; | |
| 215 | } | |
| 216 | Type actualType = parameterizedType.getActualTypeArguments()[i]; | |
| 217 | try { | |
| 218 |
1
1. getActualIdType : replaced return value with Optional.empty for com/reallifedeveloper/tools/test/database/JpaUtil::getActualIdType → KILLED |
return Optional.of((Class<ID>) Class.forName(actualType.getTypeName())); |
| 219 | } catch (ClassNotFoundException e) { | |
| 220 | throw new IllegalStateException("Unexpected problem looking up ID class", e); | |
| 221 | } | |
| 222 | } | |
| 223 | } | |
| 224 | return Optional.empty(); | |
| 225 | } | |
| 226 | ||
| 227 | /** | |
| 228 | * Calls the {@code add} method on the the {@code java.util.Collection} referenced by the given entity field to add the given value. | |
| 229 | * <p> | |
| 230 | * This method should only be called when you know that the field actually is a collection. | |
| 231 | * | |
| 232 | * @param field the field holding the collection | |
| 233 | * @param entity the entity where {@code field} lives | |
| 234 | * @param value the value to add, may be {@code null} | |
| 235 | */ | |
| 236 | @SuppressFBWarnings(value = "CRLF_INJECTION_LOGS", justification = "Only entity values being logged") | |
| 237 | public static void addObjectToCollectionField(Field field, Object entity, Object value) { | |
| 238 | assert Collection.class.isAssignableFrom(field.getType()) : "Expected field to be a Collection: field=" + field; | |
| 239 | try { | |
| 240 | Method add = field.getType().getMethod("add", Object.class); | |
| 241 | LOG.debug("Calling add method on field {} of entity {} to add entity {} to collection", field.getName(), entity, value); | |
| 242 | add.invoke(field.get(entity), value); | |
| 243 | } catch (NoSuchMethodException e) { | |
| 244 | throw new IllegalStateException( | |
| 245 | "Method 'add' not found -- field " + fieldNameForLogging(entity, field) + " should be a Collection", e); | |
| 246 | } catch (IllegalAccessException | InvocationTargetException e) { | |
| 247 | throw new IllegalStateException("Unexpected problem", e); | |
| 248 | } | |
| 249 | } | |
| 250 | ||
| 251 | /** | |
| 252 | * Calls the {@code put} method on the {@code java.util.Map} referenced by the given entity field to add the entities to map. | |
| 253 | * <p> | |
| 254 | * The key is found using the {@code MapKey} annotation of the field. | |
| 255 | * <p> | |
| 256 | * This method should only be called when you know that the field actually is a map. | |
| 257 | * | |
| 258 | * @param field the field holding the map | |
| 259 | * @param entity the entity where {@code field} lives | |
| 260 | * @param entitiesToMap a list of entities to map | |
| 261 | */ | |
| 262 | public static void addEntitiesToMapField(Field field, Object entity, List<?> entitiesToMap) { | |
| 263 | LOG.trace("addEntitiesToMapField: field={}, entity={}, entitiesToMap={}", field, entity, entitiesToMap); | |
| 264 | assert Map.class.isAssignableFrom(field.getType()) : "Expected field to be a Map: field=" + field; | |
| 265 | for (Object entityToMap : entitiesToMap) { | |
| 266 | MapKey mapKey = field.getAnnotation(MapKey.class); | |
| 267 |
1
1. addEntitiesToMapField : negated conditional → KILLED |
if (mapKey == null) { |
| 268 | throw new IllegalStateException( | |
| 269 | "Field " + fieldNameForLogging(entity, field) + " is a Map but is missing MapKey annotation"); | |
| 270 | } | |
| 271 | try { | |
| 272 | Method put = field.getType().getMethod("put", Object.class, Object.class); | |
| 273 | Object key = JpaUtil.getField(entityToMap, mapKey.name()).get(entityToMap); | |
| 274 | LOG.debug("Adding entity {} to map {} with key {}", entityToMap, fieldNameForLogging(entity, field), key); | |
| 275 | put.invoke(field.get(entity), key, entityToMap); | |
| 276 | } catch (NoSuchMethodException e) { | |
| 277 | throw new IllegalStateException( | |
| 278 | "Method 'put' not found -- field " + fieldNameForLogging(entity, field) + " should be a Map", e); | |
| 279 | } catch (NoSuchFieldException e) { | |
| 280 | throw new IllegalStateException( | |
| 281 | "Field " + mapKey.name() + " not found, it is used in MapKey annotation in " + fieldNameForLogging(entity, field), | |
| 282 | e); | |
| 283 | } catch (IllegalAccessException | InvocationTargetException e) { | |
| 284 | throw new IllegalStateException("Unexpected problem", e); | |
| 285 | } | |
| 286 | } | |
| 287 | } | |
| 288 | ||
| 289 | private static String fieldNameForLogging(Object entity, Field field) { | |
| 290 |
1
1. fieldNameForLogging : replaced return value with "" for com/reallifedeveloper/tools/test/database/JpaUtil::fieldNameForLogging → SURVIVED |
return entity.getClass().getName() + "." + field.getName(); |
| 291 | } | |
| 292 | } | |
Mutations | ||
| 51 |
1.1 |
|
| 52 |
1.1 |
|
| 54 |
1.1 |
|
| 72 |
1.1 |
|
| 74 |
1.1 |
|
| 75 |
1.1 |
|
| 76 |
1.1 |
|
| 94 |
1.1 |
|
| 96 |
1.1 |
|
| 97 |
1.1 |
|
| 116 |
1.1 |
|
| 118 |
1.1 |
|
| 140 |
1.1 |
|
| 145 |
1.1 |
|
| 146 |
1.1 |
|
| 149 |
1.1 |
|
| 153 |
1.1 |
|
| 159 |
1.1 2.2 |
|
| 161 |
1.1 2.2 |
|
| 162 |
1.1 2.2 |
|
| 164 |
1.1 2.2 |
|
| 167 |
1.1 2.2 |
|
| 180 |
1.1 |
|
| 185 |
1.1 |
|
| 186 |
1.1 |
|
| 187 |
1.1 |
|
| 190 |
1.1 |
|
| 191 |
1.1 |
|
| 193 |
1.1 |
|
| 194 |
1.1 |
|
| 207 |
1.1 |
|
| 211 |
1.1 2.2 |
|
| 213 |
1.1 |
|
| 218 |
1.1 |
|
| 267 |
1.1 |
|
| 290 |
1.1 |