Java tutorial
/* * Copyright 2015 herd contributors * * 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.finra.herd.dao.impl; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.OneToMany; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.From; import javax.persistence.criteria.Join; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import javax.persistence.criteria.Subquery; import javax.persistence.metamodel.SingularAttribute; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.finra.herd.core.helper.ConfigurationHelper; import org.finra.herd.dao.helper.HerdDaoSecurityHelper; import org.finra.herd.model.api.xml.BusinessObjectDataKey; import org.finra.herd.model.api.xml.BusinessObjectFormatKey; import org.finra.herd.model.jpa.AuditableEntity; import org.finra.herd.model.jpa.BusinessObjectDataEntity; import org.finra.herd.model.jpa.BusinessObjectDataEntity_; import org.finra.herd.model.jpa.BusinessObjectDataStatusEntity; import org.finra.herd.model.jpa.BusinessObjectDataStatusEntity_; import org.finra.herd.model.jpa.BusinessObjectDefinitionEntity; import org.finra.herd.model.jpa.BusinessObjectDefinitionEntity_; import org.finra.herd.model.jpa.BusinessObjectFormatEntity; import org.finra.herd.model.jpa.BusinessObjectFormatEntity_; import org.finra.herd.model.jpa.FileTypeEntity; import org.finra.herd.model.jpa.FileTypeEntity_; import org.finra.herd.model.jpa.NamespaceEntity; import org.finra.herd.model.jpa.NamespaceEntity_; import org.finra.herd.model.jpa.StorageEntity; import org.finra.herd.model.jpa.StorageEntity_; import org.finra.herd.model.jpa.StoragePlatformEntity; import org.finra.herd.model.jpa.StoragePlatformEntity_; import org.finra.herd.model.jpa.StorageUnitEntity; import org.finra.herd.model.jpa.StorageUnitEntity_; import org.finra.herd.model.jpa.StorageUnitStatusEntity; import org.finra.herd.model.jpa.StorageUnitStatusEntity_; public abstract class AbstractHerdDao extends BaseJpaDaoImpl { /** * A default date mask for a single day formatted as yyyy-MM-dd. */ public static final String DEFAULT_SINGLE_DAY_DATE_MASK = "yyyy-MM-dd"; public static final int MAX_PARTITION_FILTERS_PER_REQUEST = 100; protected static final List<SingularAttribute<BusinessObjectDataEntity, String>> BUSINESS_OBJECT_DATA_PARTITIONS = Collections .unmodifiableList(Arrays.asList(BusinessObjectDataEntity_.partitionValue, BusinessObjectDataEntity_.partitionValue2, BusinessObjectDataEntity_.partitionValue3, BusinessObjectDataEntity_.partitionValue4, BusinessObjectDataEntity_.partitionValue5)); protected static final List<SingularAttribute<BusinessObjectDataEntity, String>> BUSINESS_OBJECT_DATA_SUBPARTITIONS = BUSINESS_OBJECT_DATA_PARTITIONS .subList(1, 1 + BusinessObjectDataEntity.MAX_SUBPARTITIONS); /** * Represents aggregate function. */ protected enum AggregateFunction { GREATEST, LEAST } @Autowired protected ConfigurationHelper configurationHelper; @Autowired private HerdDaoSecurityHelper herdDaoSecurityHelper; /** * TODO This method may be bdata specific. Consider creating new abstract class to group all bdata related DAO. Builds a query restriction predicate for the * specified entities as per business object data key values. * * @param builder the criteria builder * @param businessObjectDataEntity the business object data entity that appears in the from clause * @param businessObjectFormatEntity the business object format entity that appears in the from clause * @param fileTypeEntity the file type entity that appears in the from clause * @param businessObjectDefinitionEntity the business object definition entity that appears in the from clause * @param businessObjectDataKey the business object data key * * @return the query restriction predicate */ protected Predicate getQueryRestriction(CriteriaBuilder builder, From<?, BusinessObjectDataEntity> businessObjectDataEntity, From<?, BusinessObjectFormatEntity> businessObjectFormatEntity, From<?, FileTypeEntity> fileTypeEntity, From<?, BusinessObjectDefinitionEntity> businessObjectDefinitionEntity, BusinessObjectDataKey businessObjectDataKey) { // Create the standard restrictions based on the business object format key values that are part of the business object data key. // Please note that we specify not to ignore the business object format version. Predicate predicate = getQueryRestriction(builder, businessObjectFormatEntity, fileTypeEntity, businessObjectDefinitionEntity, getBusinessObjectFormatKey(businessObjectDataKey), false); // Create and append a restriction on partition values. predicate = builder.and(predicate, getQueryRestrictionOnPartitionValues(builder, businessObjectDataEntity, businessObjectDataKey)); // If it is specified, create and append a restriction on business object data version. if (businessObjectDataKey.getBusinessObjectDataVersion() != null) { predicate = builder.and(predicate, builder.equal(businessObjectDataEntity.get(BusinessObjectDataEntity_.version), businessObjectDataKey.getBusinessObjectDataVersion())); } return predicate; } /** * TODO This method may be bdata specific. Consider creating new abstract class to group all bdata related DAO. Builds a query restriction predicate for the * specified business object data entity as per primary and sub-partition values in the business object data key. * * @param builder the criteria builder * @param businessObjectDataEntity the business object data entity that appears in the from clause * @param businessObjectDataKey the business object data key * * @return the query restriction predicate */ protected Predicate getQueryRestrictionOnPartitionValues(CriteriaBuilder builder, From<?, BusinessObjectDataEntity> businessObjectDataEntity, BusinessObjectDataKey businessObjectDataKey) { return getQueryRestrictionOnPartitionValues(builder, businessObjectDataEntity, businessObjectDataKey.getPartitionValue(), businessObjectDataKey.getSubPartitionValues()); } /** * TODO This method may be bdata specific. Consider creating new abstract class to group all bdata related DAO. Builds a query restriction predicate for the * specified business object data entity as per primary and sub-partition values in the business object data key. * * @param builder the criteria builder * @param businessObjectDataEntity the business object data entity that appears in the from clause * @param primaryPartitionValue the primary partition value of the business object data * @param subPartitionValues the list of sub-partition values for the business object data * * @return the query restriction predicate */ protected Predicate getQueryRestrictionOnPartitionValues(CriteriaBuilder builder, From<?, BusinessObjectDataEntity> businessObjectDataEntity, String primaryPartitionValue, List<String> subPartitionValues) { // Create a standard restriction on primary partition value. Predicate predicate = builder.equal(businessObjectDataEntity.get(BusinessObjectDataEntity_.partitionValue), primaryPartitionValue); // Create and add standard restrictions on sub-partition values. Please note that the subpartition value columns are nullable. int subPartitionValuesCount = CollectionUtils.size(subPartitionValues); for (int i = 0; i < BusinessObjectDataEntity.MAX_SUBPARTITIONS; i++) { predicate = builder.and(predicate, i < subPartitionValuesCount ? builder.equal(businessObjectDataEntity.get(BUSINESS_OBJECT_DATA_SUBPARTITIONS.get(i)), subPartitionValues.get(i)) : builder.isNull(businessObjectDataEntity.get(BUSINESS_OBJECT_DATA_SUBPARTITIONS.get(i)))); } return predicate; } /** * TODO This method may be bformat specific. Consider creating new abstract class to group all bformat related DAO. Builds a query restriction predicate for * the specified business object format entity as per business object format key values. * * @param builder the criteria builder * @param businessObjectFormatEntity the business object format entity that appears in the from clause * @param fileTypeEntity the file type entity that appears in the from clause * @param businessObjectDefinitionEntity the business object definition entity that appears in the from clause * @param businessObjectFormatKey the business object format key * @param ignoreBusinessObjectFormatVersion specifies whether to ignore the business object format version when building the predicate * * @return the query restriction predicate */ protected Predicate getQueryRestriction(CriteriaBuilder builder, From<?, BusinessObjectFormatEntity> businessObjectFormatEntity, From<?, FileTypeEntity> fileTypeEntity, From<?, BusinessObjectDefinitionEntity> businessObjectDefinitionEntity, BusinessObjectFormatKey businessObjectFormatKey, boolean ignoreBusinessObjectFormatVersion) { // Join to the other tables we can filter on. Join<BusinessObjectDefinitionEntity, NamespaceEntity> namespaceEntity = businessObjectDefinitionEntity .join(BusinessObjectDefinitionEntity_.namespace); return getQueryRestriction(builder, businessObjectFormatEntity, fileTypeEntity, businessObjectDefinitionEntity, namespaceEntity, businessObjectFormatKey, ignoreBusinessObjectFormatVersion); } /** * Builds a query restriction predicate for the specified business object format entity as per business object format key values. * * @param builder the criteria builder * @param businessObjectFormatEntity the business object format entity that appears in the from clause * @param fileTypeEntity the file type entity that appears in the from clause * @param businessObjectDefinitionEntity the business object definition entity that appears in the from clause * @param namespaceEntity the namespace entity that appears in the from clause * @param businessObjectFormatKey the business object format key * @param ignoreBusinessObjectFormatVersion specifies whether to ignore the business object format version when building the predicate * * @return the query restriction predicate */ protected Predicate getQueryRestriction(CriteriaBuilder builder, From<?, BusinessObjectFormatEntity> businessObjectFormatEntity, From<?, FileTypeEntity> fileTypeEntity, From<?, BusinessObjectDefinitionEntity> businessObjectDefinitionEntity, From<?, NamespaceEntity> namespaceEntity, BusinessObjectFormatKey businessObjectFormatKey, boolean ignoreBusinessObjectFormatVersion) { // Create the standard restrictions based on the business object format key values (i.e. the standard where clauses). // Create a restriction on namespace code. Predicate predicate = builder.equal(builder.upper(namespaceEntity.get(NamespaceEntity_.code)), businessObjectFormatKey.getNamespace().toUpperCase()); // Create and append a restriction on business object definition name. predicate = builder.and(predicate, builder.equal( builder.upper(businessObjectDefinitionEntity.get(BusinessObjectDefinitionEntity_.name)), businessObjectFormatKey.getBusinessObjectDefinitionName().toUpperCase())); // Create and append a restriction on business object format usage. predicate = builder.and(predicate, builder.equal(builder.upper(businessObjectFormatEntity.get(BusinessObjectFormatEntity_.usage)), businessObjectFormatKey.getBusinessObjectFormatUsage().toUpperCase())); // Create and append a restriction on business object format file type. predicate = builder.and(predicate, builder.equal(builder.upper(fileTypeEntity.get(FileTypeEntity_.code)), businessObjectFormatKey.getBusinessObjectFormatFileType().toUpperCase())); // If specified, create and append a restriction on business object format version. if (!ignoreBusinessObjectFormatVersion && businessObjectFormatKey.getBusinessObjectFormatVersion() != null) { predicate = builder.and(predicate, builder.equal( businessObjectFormatEntity.get(BusinessObjectFormatEntity_.businessObjectFormatVersion), businessObjectFormatKey.getBusinessObjectFormatVersion())); } return predicate; } /** * TODO This method may be bformat specific. Consider creating new abstract class to group all bformat related DAO. Gets a business object format key from * the specified business object data key. * * @param businessObjectDataKey the business object data key * * @return the newly created business object format key */ private BusinessObjectFormatKey getBusinessObjectFormatKey(BusinessObjectDataKey businessObjectDataKey) { return new BusinessObjectFormatKey(businessObjectDataKey.getNamespace(), businessObjectDataKey.getBusinessObjectDefinitionName(), businessObjectDataKey.getBusinessObjectFormatUsage(), businessObjectDataKey.getBusinessObjectFormatFileType(), businessObjectDataKey.getBusinessObjectFormatVersion()); } /** * TODO This method may be bdata specific. Consider creating new abstract class to group all bdata related DAO. Builds a query restriction predicate for the * sub-query business object data entity as per partition values from the specified main query business object data entity. * * @param builder the criteria builder * @param businessObjectDataEntity the business object data entity that appears in the from clause * @param partitionFilters the list of partition filter to be used to select business object data instances. Each partition filter contains a list of * primary and sub-partition values in the right order up to the maximum partition levels allowed by business object data registration - with partition * values for the relative partitions not to be used for selection passed as nulls. * * @return the query restriction predicate */ protected Predicate getQueryRestrictionOnPartitionValues(CriteriaBuilder builder, From<?, BusinessObjectDataEntity> businessObjectDataEntity, List<List<String>> partitionFilters) { // Create a query restriction as per specified primary and/or sub-partition values. Predicate predicate = null; for (List<String> partitionFilter : partitionFilters) { // Add restriction for each partition level if the relative partition value is specified in the partition filter. Predicate partitionRestriction = null; for (int partitionLevel = 0; partitionLevel < BusinessObjectDataEntity.MAX_SUBPARTITIONS + 1; partitionLevel++) { String partitionValue = partitionFilter.get(partitionLevel); if (StringUtils.isNotBlank(partitionValue)) { Predicate partitionValueRestriction = builder.equal( businessObjectDataEntity.get(BUSINESS_OBJECT_DATA_PARTITIONS.get(partitionLevel)), partitionValue); partitionRestriction = (partitionRestriction == null ? partitionValueRestriction : builder.and(partitionRestriction, partitionValueRestriction)); } } predicate = (predicate == null ? partitionRestriction : builder.or(predicate, partitionRestriction)); } return predicate; } /** * TODO This method may be bdata specific. Consider creating new abstract class to group all bdata related DAO. Builds a query restriction predicate for the * sub-query business object data entity as per partition values from the specified main query business object data entity. * * @param builder the criteria builder * @param subBusinessObjectDataEntity the sub-query business object data entity that appears in the from clause * @param mainBusinessObjectDataEntity the main query business object data entity that appears in the from clause * * @return the query restriction predicate */ protected Predicate getQueryRestrictionOnPartitionValues(CriteriaBuilder builder, From<?, BusinessObjectDataEntity> subBusinessObjectDataEntity, From<?, BusinessObjectDataEntity> mainBusinessObjectDataEntity) { // Create a standard restriction on primary partition value. Predicate predicate = builder.equal( subBusinessObjectDataEntity.get(BusinessObjectDataEntity_.partitionValue), mainBusinessObjectDataEntity.get(BusinessObjectDataEntity_.partitionValue)); // Create and add standard restrictions on sub-partition values. Please note that the subpartition value columns are nullable. for (SingularAttribute<BusinessObjectDataEntity, String> businessObjectDataPartitionValueSingularAttribute : BUSINESS_OBJECT_DATA_SUBPARTITIONS) { predicate = builder.and(predicate, builder.or( builder.and( builder.isNull(subBusinessObjectDataEntity .get(businessObjectDataPartitionValueSingularAttribute)), builder.isNull(mainBusinessObjectDataEntity .get(businessObjectDataPartitionValueSingularAttribute))), builder.equal( subBusinessObjectDataEntity.get(businessObjectDataPartitionValueSingularAttribute), mainBusinessObjectDataEntity.get(businessObjectDataPartitionValueSingularAttribute)))); } return predicate; } /** * TODO This method may be bdata specific. Consider creating new abstract class to group all bdata related DAO. Builds query restriction predicates and adds * them to the query restriction as per specified business object data version and status. If a business object data version is specified, the business * object data status is ignored. When both business object data version and business object data status are not specified, the sub-query restriction is not * getting updated. * * @param builder the criteria builder * @param businessObjectDataEntity the business object data entity that appears in the from clause * @param businessObjectDataStatusEntity the business object data status entity that appears in the from clause * @param businessObjectDataVersion the business object data version * @param businessObjectDataStatus the business object data status. This parameter is ignored when the business object data version is specified. * * @return the query restriction predicate or null if both business object data version and business object data status are not specified */ protected Predicate getQueryRestrictionOnBusinessObjectDataVersionAndStatus(CriteriaBuilder builder, From<?, BusinessObjectDataEntity> businessObjectDataEntity, From<?, BusinessObjectDataStatusEntity> businessObjectDataStatusEntity, Integer businessObjectDataVersion, String businessObjectDataStatus) { Predicate predicate = null; // If specified, create a standard restriction on the business object data version. if (businessObjectDataVersion != null) { predicate = builder.equal(businessObjectDataEntity.get(BusinessObjectDataEntity_.version), businessObjectDataVersion); } // Only if a business object data version is not specified, check if we need to add a restriction on the business object data status. else if (businessObjectDataStatus != null) { predicate = builder.equal( builder.upper(businessObjectDataStatusEntity.get(BusinessObjectDataStatusEntity_.code)), businessObjectDataStatus.toUpperCase()); } return predicate; } /** * Builds a query restriction predicate for the storage. * * @param builder the criteria builder * @param storageEntity the storage entity that appears in the from clause * @param storagePlatformEntity the storage platform entity that appears in the from clause * @param storageNames the list of storage names where the business object data storage units should be looked for (case-insensitive) * @param storagePlatformType the optional storage platform type, e.g. S3 for Hive DDL. It is ignored when the list of storages is not empty * @param excludedStoragePlatformType the optional storage platform type to be excluded from search. It is ignored when the list of storages is not empty or * the storage platform type is specified * * @return the query restriction predicate */ protected Predicate getQueryRestrictionOnStorage(CriteriaBuilder builder, From<?, StorageEntity> storageEntity, From<?, StoragePlatformEntity> storagePlatformEntity, List<String> storageNames, String storagePlatformType, String excludedStoragePlatformType) { List<Predicate> predicates = new ArrayList<>(); // If specified, add restriction on storage. if (!CollectionUtils.isEmpty(storageNames)) { // Add a storage name restriction to the main query where clause. List<String> uppercaseStorageNames = new ArrayList<>(); for (String storageName : storageNames) { uppercaseStorageNames.add(storageName.toUpperCase()); } predicates.add(builder.upper(storageEntity.get(StorageEntity_.name)).in(uppercaseStorageNames)); } else if (StringUtils.isNotBlank(storagePlatformType)) { // Select storage units only from the storages of the specified storage platform type. predicates.add( builder.equal(storagePlatformEntity.get(StoragePlatformEntity_.name), storagePlatformType)); } else if (StringUtils.isNotBlank(excludedStoragePlatformType)) { // Ignore any storages of the excluded storage platform type. predicates.add(builder.notEqual(storagePlatformEntity.get(StoragePlatformEntity_.name), excludedStoragePlatformType)); } return builder.and(predicates.toArray(new Predicate[predicates.size()])); } /** * TODO This method may be bdata specific. Consider creating new abstract class to group all bdata related DAO. Builds a sub-query to select the maximum * business object data version. * * @param builder the criteria builder * @param criteria the criteria query * @param businessObjectDataEntity the business object data entity that appears in the from clause of the main query * @param businessObjectFormatEntity the business object format entity that appears in the from clause of the main query * @param businessObjectDataStatus the business object data status * @param storageNames the list of storage names where the business object data storage units should be looked for (case-insensitive) * @param storagePlatformType the optional storage platform type, e.g. S3 for Hive DDL. It is ignored when the list of storages is not empty * @param excludedStoragePlatformType the optional storage platform type to be excluded from search. It is ignored when the list of storages is not empty or * the storage platform type is specified * @param selectOnlyAvailableStorageUnits specifies if only available storage units will be selected or any storage units regardless of their status * * @return the sub-query to select the maximum business object data version */ protected Subquery<Integer> getMaximumBusinessObjectDataVersionSubQuery(CriteriaBuilder builder, CriteriaQuery<?> criteria, From<?, BusinessObjectDataEntity> businessObjectDataEntity, From<?, BusinessObjectFormatEntity> businessObjectFormatEntity, String businessObjectDataStatus, List<String> storageNames, String storagePlatformType, String excludedStoragePlatformType, boolean selectOnlyAvailableStorageUnits) { // Business object data version is not specified, so get the latest one in the specified storage. Subquery<Integer> subQuery = criteria.subquery(Integer.class); // The criteria root is the business object data. Root<BusinessObjectDataEntity> subBusinessObjectDataEntity = subQuery.from(BusinessObjectDataEntity.class); // Join to the other tables we can filter on. Join<BusinessObjectDataEntity, StorageUnitEntity> subStorageUnitEntity = subBusinessObjectDataEntity .join(BusinessObjectDataEntity_.storageUnits); Join<StorageUnitEntity, StorageEntity> subStorageEntity = subStorageUnitEntity .join(StorageUnitEntity_.storage); Join<StorageEntity, StoragePlatformEntity> subStoragePlatformEntity = subStorageEntity .join(StorageEntity_.storagePlatform); Join<BusinessObjectDataEntity, BusinessObjectFormatEntity> subBusinessObjectFormatEntity = subBusinessObjectDataEntity .join(BusinessObjectDataEntity_.businessObjectFormat); Join<StorageUnitEntity, StorageUnitStatusEntity> subStorageUnitStatusEntity = subStorageUnitEntity .join(StorageUnitEntity_.status); // Add a standard restriction on business object format. Predicate subQueryRestriction = builder.equal(subBusinessObjectFormatEntity, businessObjectFormatEntity); // Create and add standard restrictions on primary and sub-partition values. subQueryRestriction = builder.and(subQueryRestriction, getQueryRestrictionOnPartitionValues(builder, subBusinessObjectDataEntity, businessObjectDataEntity)); // If specified, create and add a standard restriction on business object data status. if (businessObjectDataStatus != null) { Join<BusinessObjectDataEntity, BusinessObjectDataStatusEntity> subBusinessObjectDataStatusEntity = subBusinessObjectDataEntity .join(BusinessObjectDataEntity_.status); subQueryRestriction = builder.and(subQueryRestriction, builder.equal( builder.upper( subBusinessObjectDataStatusEntity.get(BusinessObjectDataStatusEntity_.code)), businessObjectDataStatus.toUpperCase())); } // Create and add a standard restriction on storage. subQueryRestriction = builder.and(subQueryRestriction, getQueryRestrictionOnStorage(builder, subStorageEntity, subStoragePlatformEntity, storageNames, storagePlatformType, excludedStoragePlatformType)); // If specified, add a restriction on storage unit status availability flag. if (selectOnlyAvailableStorageUnits) { subQueryRestriction = builder.and(subQueryRestriction, builder.isTrue(subStorageUnitStatusEntity.get(StorageUnitStatusEntity_.available))); } subQuery.select(builder.max(subBusinessObjectDataEntity.get(BusinessObjectDataEntity_.version))) .where(subQueryRestriction); return subQuery; } /** * {@inheritDoc} * <p/> * This method overwrites the base class method by first updating an entity's audit fields if the entity is of type AuditableEntity. */ @Override public <T> T save(T entity) { updateAuditFields(entity); return super.save(entity); } /** * Updates the audit fields if the entity is of type AuditableEntity. * * @param entity the entity * @param <T> the type of entity */ @SuppressWarnings("rawtypes") private <T> void updateAuditFields(T entity) { if (entity instanceof AuditableEntity) { AuditableEntity auditableEntity = (AuditableEntity) entity; // Get the currently logged in username. String username = herdDaoSecurityHelper.getCurrentUsername(); // Always set the updated by field, but only set the created by field when it is null (i.e. this is a new record). if (auditableEntity.getCreatedBy() == null) { auditableEntity.setCreatedBy(username); } auditableEntity.setUpdatedBy(username); // Always set the updated on field to the current time, but only update the created on field when it is null (i.e. the first time). Timestamp currentTime = new Timestamp(System.currentTimeMillis()); auditableEntity.setUpdatedOn(currentTime); if (auditableEntity.getCreatedOn() == null) { auditableEntity.setCreatedOn(currentTime); } } // Try to update children one-to-many cascadable auditable entities. // Note that this assumes that OneToMany annotations are done on the field (as opposed to the method) and that all OneToMany fields are collections. // This approach also assumes that there are loops where children refer back to our entity (i.e. an infinite loop). // If there are other scenarios, we should modify this code to handle them. // Loop through all the fields of this entity. for (Field field : entity.getClass().getDeclaredFields()) { // Get all the annotations for the field. for (Annotation annotation : field.getDeclaredAnnotations()) { // Only look for OneToMany that cascade with "persist" or "merge". if (annotation instanceof OneToMany) { OneToMany oneToManyAnnotation = (OneToMany) annotation; List<CascadeType> cascadeTypes = new ArrayList<>(Arrays.asList(oneToManyAnnotation.cascade())); if ((cascadeTypes.contains(CascadeType.ALL)) || (cascadeTypes.contains(CascadeType.PERSIST)) || cascadeTypes.contains(CascadeType.MERGE)) { try { // Modify the accessibility to true so we can get the field (even if it's private) and get the value of the field for our entity. field.setAccessible(true); Object fieldValue = field.get(entity); // If the field is a collection (which OneToMany annotated fields should be), then iterate through the collection and look for // child auditable entities. if (fieldValue instanceof Collection) { Collection collection = (Collection) fieldValue; for (Object object : collection) { if (object instanceof AuditableEntity) { // We found a child auditable entity so recurse to update it's audit fields as well. updateAuditFields(object); } } } } catch (IllegalAccessException ex) { // Because we're setting accessible to true above, we shouldn't get here. throw new IllegalStateException("Unable to get field value for field \"" + field.getName() + "\" due to access restriction.", ex); } } } } } } }