org.finra.herd.service.impl.BusinessObjectDataServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.finra.herd.service.impl.BusinessObjectDataServiceImpl.java

Source

/*
 * 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.service.impl;

import java.io.File;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import org.finra.herd.core.helper.ConfigurationHelper;
import org.finra.herd.dao.BusinessObjectDataDao;
import org.finra.herd.dao.BusinessObjectFormatDao;
import org.finra.herd.dao.StorageUnitDao;
import org.finra.herd.dao.config.DaoSpringModuleConfig;
import org.finra.herd.dao.helper.JsonHelper;
import org.finra.herd.model.annotation.NamespacePermission;
import org.finra.herd.model.annotation.PublishNotificationMessages;
import org.finra.herd.model.api.xml.Attribute;
import org.finra.herd.model.api.xml.BusinessObjectData;
import org.finra.herd.model.api.xml.BusinessObjectDataAttributesUpdateRequest;
import org.finra.herd.model.api.xml.BusinessObjectDataAvailability;
import org.finra.herd.model.api.xml.BusinessObjectDataAvailabilityCollectionRequest;
import org.finra.herd.model.api.xml.BusinessObjectDataAvailabilityCollectionResponse;
import org.finra.herd.model.api.xml.BusinessObjectDataAvailabilityRequest;
import org.finra.herd.model.api.xml.BusinessObjectDataCreateRequest;
import org.finra.herd.model.api.xml.BusinessObjectDataDdl;
import org.finra.herd.model.api.xml.BusinessObjectDataDdlCollectionRequest;
import org.finra.herd.model.api.xml.BusinessObjectDataDdlCollectionResponse;
import org.finra.herd.model.api.xml.BusinessObjectDataDdlRequest;
import org.finra.herd.model.api.xml.BusinessObjectDataInvalidateUnregisteredRequest;
import org.finra.herd.model.api.xml.BusinessObjectDataInvalidateUnregisteredResponse;
import org.finra.herd.model.api.xml.BusinessObjectDataKey;
import org.finra.herd.model.api.xml.BusinessObjectDataKeys;
import org.finra.herd.model.api.xml.BusinessObjectDataParentsUpdateRequest;
import org.finra.herd.model.api.xml.BusinessObjectDataRetentionInformationUpdateRequest;
import org.finra.herd.model.api.xml.BusinessObjectDataRetryStoragePolicyTransitionRequest;
import org.finra.herd.model.api.xml.BusinessObjectDataSearchKey;
import org.finra.herd.model.api.xml.BusinessObjectDataSearchRequest;
import org.finra.herd.model.api.xml.BusinessObjectDataSearchResult;
import org.finra.herd.model.api.xml.BusinessObjectDataStatus;
import org.finra.herd.model.api.xml.BusinessObjectDataVersion;
import org.finra.herd.model.api.xml.BusinessObjectDataVersions;
import org.finra.herd.model.api.xml.BusinessObjectDefinitionKey;
import org.finra.herd.model.api.xml.BusinessObjectFormatKey;
import org.finra.herd.model.api.xml.CustomDdlKey;
import org.finra.herd.model.api.xml.NamespacePermissionEnum;
import org.finra.herd.model.api.xml.PartitionValueFilter;
import org.finra.herd.model.dto.BusinessObjectDataDestroyDto;
import org.finra.herd.model.dto.BusinessObjectDataRestoreDto;
import org.finra.herd.model.dto.BusinessObjectDataSearchResultPagingInfoDto;
import org.finra.herd.model.dto.ConfigurationValue;
import org.finra.herd.model.dto.S3FileTransferRequestParamsDto;
import org.finra.herd.model.dto.StorageUnitAvailabilityDto;
import org.finra.herd.model.jpa.BusinessObjectDataEntity;
import org.finra.herd.model.jpa.BusinessObjectDataStatusEntity;
import org.finra.herd.model.jpa.BusinessObjectDefinitionEntity;
import org.finra.herd.model.jpa.BusinessObjectFormatEntity;
import org.finra.herd.model.jpa.CustomDdlEntity;
import org.finra.herd.model.jpa.NotificationEventTypeEntity;
import org.finra.herd.model.jpa.RetentionTypeEntity;
import org.finra.herd.model.jpa.StorageEntity;
import org.finra.herd.model.jpa.StorageFileEntity;
import org.finra.herd.model.jpa.StoragePlatformEntity;
import org.finra.herd.model.jpa.StorageUnitEntity;
import org.finra.herd.service.BusinessObjectDataInitiateDestroyHelperService;
import org.finra.herd.service.BusinessObjectDataInitiateRestoreHelperService;
import org.finra.herd.service.BusinessObjectDataService;
import org.finra.herd.service.NotificationEventService;
import org.finra.herd.service.S3Service;
import org.finra.herd.service.helper.AttributeDaoHelper;
import org.finra.herd.service.helper.AttributeHelper;
import org.finra.herd.service.helper.BusinessObjectDataDaoHelper;
import org.finra.herd.service.helper.BusinessObjectDataHelper;
import org.finra.herd.service.helper.BusinessObjectDataInvalidateUnregisteredHelper;
import org.finra.herd.service.helper.BusinessObjectDataRetryStoragePolicyTransitionHelper;
import org.finra.herd.service.helper.BusinessObjectDataSearchHelper;
import org.finra.herd.service.helper.BusinessObjectDataStatusDaoHelper;
import org.finra.herd.service.helper.BusinessObjectDefinitionDaoHelper;
import org.finra.herd.service.helper.BusinessObjectDefinitionHelper;
import org.finra.herd.service.helper.BusinessObjectFormatDaoHelper;
import org.finra.herd.service.helper.BusinessObjectFormatHelper;
import org.finra.herd.service.helper.CustomDdlDaoHelper;
import org.finra.herd.service.helper.DdlGeneratorFactory;
import org.finra.herd.service.helper.S3KeyPrefixHelper;
import org.finra.herd.service.helper.StorageDaoHelper;
import org.finra.herd.service.helper.StorageHelper;
import org.finra.herd.service.helper.StorageUnitHelper;

/**
 * The business object data service implementation.
 */
@Service
@Transactional(value = DaoSpringModuleConfig.HERD_TRANSACTION_MANAGER_BEAN_NAME)
public class BusinessObjectDataServiceImpl implements BusinessObjectDataService {
    /**
     * The partition key value for business object data without partitioning.
     */
    public static final String NO_PARTITIONING_PARTITION_KEY = "partition";

    /**
     * The partition value for business object data without partitioning.
     */
    public static final String NO_PARTITIONING_PARTITION_VALUE = "none";

    /**
     * A status reason of "not registered".
     */
    public static final String REASON_NOT_REGISTERED = "NOT_REGISTERED";

    private static final Logger LOGGER = LoggerFactory.getLogger(BusinessObjectDataServiceImpl.class);

    @Autowired
    private AttributeDaoHelper attributeDaoHelper;

    @Autowired
    private AttributeHelper attributeHelper;

    @Autowired
    private BusinessObjectDataDao businessObjectDataDao;

    @Autowired
    private BusinessObjectDataDaoHelper businessObjectDataDaoHelper;

    @Autowired
    private BusinessObjectDataHelper businessObjectDataHelper;

    @Autowired
    private BusinessObjectDataInitiateDestroyHelperService businessObjectDataInitiateDestroyHelperService;

    @Autowired
    private BusinessObjectDataInitiateRestoreHelperService businessObjectDataInitiateRestoreHelperService;

    @Autowired
    private BusinessObjectDataInvalidateUnregisteredHelper businessObjectDataInvalidateUnregisteredHelper;

    @Autowired
    private BusinessObjectDataRetryStoragePolicyTransitionHelper businessObjectDataRetryStoragePolicyTransitionHelper;

    @Autowired
    private BusinessObjectDataSearchHelper businessObjectDataSearchHelper;

    @Autowired
    private BusinessObjectDataStatusDaoHelper businessObjectDataStatusDaoHelper;

    @Autowired
    private BusinessObjectDefinitionDaoHelper businessObjectDefinitionDaoHelper;

    @Autowired
    private BusinessObjectDefinitionHelper businessObjectDefinitionHelper;

    @Autowired
    private BusinessObjectFormatDao businessObjectFormatDao;

    @Autowired
    private BusinessObjectFormatDaoHelper businessObjectFormatDaoHelper;

    @Autowired
    private BusinessObjectFormatHelper businessObjectFormatHelper;

    @Autowired
    private ConfigurationHelper configurationHelper;

    @Autowired
    private CustomDdlDaoHelper customDdlDaoHelper;

    @Autowired
    private DdlGeneratorFactory ddlGeneratorFactory;

    @Autowired
    private JsonHelper jsonHelper;

    @Autowired
    private NotificationEventService notificationEventService;

    @Autowired
    private S3KeyPrefixHelper s3KeyPrefixHelper;

    @Autowired
    private S3Service s3Service;

    @Autowired
    private StorageDaoHelper storageDaoHelper;

    @Autowired
    private StorageHelper storageHelper;

    @Autowired
    private StorageUnitDao storageUnitDao;

    @Autowired
    private StorageUnitHelper storageUnitHelper;

    @NamespacePermission(fields = "#request.namespace", permissions = NamespacePermissionEnum.READ)
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public BusinessObjectDataAvailability checkBusinessObjectDataAvailability(
            BusinessObjectDataAvailabilityRequest request) {
        return checkBusinessObjectDataAvailabilityImpl(request);
    }

    @NamespacePermission(fields = "#request?.businessObjectDataAvailabilityRequests?.![namespace]", permissions = NamespacePermissionEnum.READ)
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public BusinessObjectDataAvailabilityCollectionResponse checkBusinessObjectDataAvailabilityCollection(
            BusinessObjectDataAvailabilityCollectionRequest request) {
        return checkBusinessObjectDataAvailabilityCollectionImpl(request);
    }

    @PublishNotificationMessages
    @NamespacePermission(fields = "#request.namespace", permissions = NamespacePermissionEnum.WRITE)
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public BusinessObjectData createBusinessObjectData(BusinessObjectDataCreateRequest request) {
        return businessObjectDataDaoHelper.createBusinessObjectData(request);
    }

    @NamespacePermission(fields = "#businessObjectDataKey.namespace", permissions = NamespacePermissionEnum.WRITE)
    @Override
    public BusinessObjectData deleteBusinessObjectData(BusinessObjectDataKey businessObjectDataKey,
            Boolean deleteFiles) {
        // Validate and trim the business object data key.
        businessObjectDataHelper.validateBusinessObjectDataKey(businessObjectDataKey, true, true);

        // Validate the mandatory deleteFiles flag.
        Assert.notNull(deleteFiles, "A delete files flag must be specified.");

        // Retrieve the business object data and ensure it exists.
        BusinessObjectDataEntity businessObjectDataEntity = businessObjectDataDaoHelper
                .getBusinessObjectDataEntity(businessObjectDataKey);

        // If the business object data has children, remove the parent children relationship.
        if (!businessObjectDataEntity.getBusinessObjectDataChildren().isEmpty()) {
            for (BusinessObjectDataEntity childBusinessObjectEntity : businessObjectDataEntity
                    .getBusinessObjectDataChildren()) {
                childBusinessObjectEntity.getBusinessObjectDataParents().remove(businessObjectDataEntity);
            }

            String businessObjectDataChildren = businessObjectDataEntity.getBusinessObjectDataChildren().stream()
                    .map(bData -> String.format("{%s}",
                            businessObjectDataHelper.businessObjectDataEntityAltKeyToString(bData)))
                    .collect(Collectors.joining(", "));
            businessObjectDataEntity.setBusinessObjectDataChildren(new ArrayList<>());
            businessObjectDataDao.save(businessObjectDataEntity);
            LOGGER.warn(String.format(
                    "Deleting business object data {%s} that has children associated with it. The parent relationship has been removed from: %s.",
                    businessObjectDataHelper.businessObjectDataEntityAltKeyToString(businessObjectDataEntity),
                    businessObjectDataChildren));
        }

        // If the flag is set, clean up the data files from all storages of S3 storage platform type.
        LOGGER.info("deleteFiles={}", deleteFiles);
        if (deleteFiles) {
            // Loop over all storage units for this business object data.
            for (StorageUnitEntity storageUnitEntity : businessObjectDataEntity.getStorageUnits()) {
                StorageEntity storageEntity = storageUnitEntity.getStorage();

                // Currently, we only support data file deletion from S3 platform type.
                if (storageEntity.getStoragePlatform().getName().equals(StoragePlatformEntity.S3)) {
                    LOGGER.info(
                            "Deleting business object data files from the storage... storageName=\"{}\" businessObjectDataKey={}",
                            storageEntity.getName(), jsonHelper.objectToJson(
                                    businessObjectDataHelper.getBusinessObjectDataKey(businessObjectDataEntity)));

                    // Get the S3 validation flags.
                    boolean validatePathPrefix = storageHelper.getBooleanStorageAttributeValueByName(
                            configurationHelper
                                    .getProperty(ConfigurationValue.S3_ATTRIBUTE_NAME_VALIDATE_PATH_PREFIX),
                            storageEntity, false, true);

                    // If this storage conforms to the path prefix validation, then delete all keys found under the S3 key prefix.
                    if (validatePathPrefix) {
                        // Retrieve S3 key prefix velocity template storage attribute value and store it in memory.
                        // Please note that it is not required, so we pass in a "false" flag.
                        String s3KeyPrefixVelocityTemplate = storageHelper.getStorageAttributeValueByName(
                                configurationHelper.getProperty(
                                        ConfigurationValue.S3_ATTRIBUTE_NAME_KEY_PREFIX_VELOCITY_TEMPLATE),
                                storageEntity, false);

                        // Validate that S3 key prefix velocity template is configured.
                        Assert.isTrue(StringUtils.isNotBlank(s3KeyPrefixVelocityTemplate), String.format(
                                "Storage \"%s\" has enabled path validation without S3 key prefix velocity template configured.",
                                storageEntity.getName()));

                        // Build the S3 key prefix as per S3 Naming Convention Wiki page.
                        String s3KeyPrefix = s3KeyPrefixHelper.buildS3KeyPrefix(s3KeyPrefixVelocityTemplate,
                                businessObjectDataEntity.getBusinessObjectFormat(), businessObjectDataKey,
                                storageEntity.getName());

                        // Get S3 bucket access parameters, such as bucket name, AWS access key ID, AWS secret access key, etc...
                        S3FileTransferRequestParamsDto params = storageHelper
                                .getS3BucketAccessParams(storageEntity);
                        // Since the S3 key prefix represents a directory, we add a trailing '/' character to it.
                        params.setS3KeyPrefix(s3KeyPrefix + "/");
                        // Delete a list of all keys/objects from S3 managed bucket matching the expected S3 key prefix.
                        // Please note that when deleting S3 files, we also delete all 0 byte objects that represent S3 directories.
                        s3Service.deleteDirectory(params);
                    }
                    // For a non S3 prefixed paths, delete the files explicitly or if only directory is registered, delete all files/subfolders found under it.
                    else {
                        // Get S3 bucket access parameters, such as bucket name, AWS access key ID, AWS secret access key, etc...
                        S3FileTransferRequestParamsDto params = storageHelper
                                .getS3BucketAccessParams(storageEntity);

                        // If only directory is registered delete all files/sub-folders found under it.
                        if (StringUtils.isNotBlank(storageUnitEntity.getDirectoryPath())
                                && storageUnitEntity.getStorageFiles().isEmpty()) {
                            // Since the directory path represents a directory, we add a trailing '/' character to it.
                            params.setS3KeyPrefix(storageUnitEntity.getDirectoryPath() + "/");
                            // Delete a list of all keys/objects from S3 bucket matching the directory path.
                            // Please note that when deleting S3 files, we also delete all 0 byte objects that represent S3 directories.
                            s3Service.deleteDirectory(params);
                        }
                        // Delete the files explicitly.
                        else {
                            // Create a list of files to delete.
                            List<File> files = new ArrayList<>();
                            for (StorageFileEntity storageFileEntity : storageUnitEntity.getStorageFiles()) {
                                files.add(new File(storageFileEntity.getPath()));
                            }
                            params.setFiles(files);
                            s3Service.deleteFileList(params);
                        }
                    }
                } else {
                    LOGGER.info(
                            "Skipping business object data file removal for a storage unit from the storage since it is not an S3 storage platform. "
                                    + " storageName=\"{}\" businessObjectDataKey={}",
                            storageEntity.getName(), jsonHelper.objectToJson(
                                    businessObjectDataHelper.getBusinessObjectDataKey(businessObjectDataEntity)));
                }
            }
        }

        // Create the business object data object from the entity.
        BusinessObjectData deletedBusinessObjectData = businessObjectDataHelper
                .createBusinessObjectDataFromEntity(businessObjectDataEntity);

        // Delete this business object data.
        businessObjectDataDao.delete(businessObjectDataEntity);

        // If this business object data version is the latest, set the latest flag on the previous version of this object data, if it exists.
        if (businessObjectDataEntity.getLatestVersion()) {
            // Get the maximum version for this business object data, if it exists.
            Integer maxBusinessObjectDataVersion = businessObjectDataDao
                    .getBusinessObjectDataMaxVersion(businessObjectDataKey);

            if (maxBusinessObjectDataVersion != null) {
                // Retrieve the previous version business object data entity. Since we successfully got the maximum
                // version for this business object data, the retrieved entity is not expected to be null.
                BusinessObjectDataEntity previousVersionBusinessObjectDataEntity = businessObjectDataDao
                        .getBusinessObjectDataByAltKey(new BusinessObjectDataKey(
                                businessObjectDataKey.getNamespace(),
                                businessObjectDataKey.getBusinessObjectDefinitionName(),
                                businessObjectDataKey.getBusinessObjectFormatUsage(),
                                businessObjectDataKey.getBusinessObjectFormatFileType(),
                                businessObjectDataKey.getBusinessObjectFormatVersion(),
                                businessObjectDataKey.getPartitionValue(),
                                businessObjectDataKey.getSubPartitionValues(), maxBusinessObjectDataVersion));

                // Update the previous version business object data entity.
                previousVersionBusinessObjectDataEntity.setLatestVersion(true);
                businessObjectDataDao.saveAndRefresh(previousVersionBusinessObjectDataEntity);
            }
        }

        // Return the deleted business object data.
        return deletedBusinessObjectData;
    }

    /**
     * {@inheritDoc}
     * <p/>
     * This implementation executes non-transactionally, suspends the current transaction if one exists.
     */
    @NamespacePermission(fields = "#businessObjectDataKey.namespace", permissions = NamespacePermissionEnum.WRITE)
    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public BusinessObjectData destroyBusinessObjectData(BusinessObjectDataKey businessObjectDataKey) {
        return destroyBusinessObjectDataImpl(businessObjectDataKey);
    }

    @NamespacePermission(fields = "#request.namespace", permissions = NamespacePermissionEnum.READ)
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public BusinessObjectDataDdl generateBusinessObjectDataDdl(BusinessObjectDataDdlRequest request) {
        return generateBusinessObjectDataDdlImpl(request, false);
    }

    @NamespacePermission(fields = "#request?.businessObjectDataDdlRequests?.![namespace]", permissions = NamespacePermissionEnum.READ)
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public BusinessObjectDataDdlCollectionResponse generateBusinessObjectDataDdlCollection(
            BusinessObjectDataDdlCollectionRequest request) {
        return generateBusinessObjectDataDdlCollectionImpl(request);
    }

    @NamespacePermission(fields = "#businessObjectDefinitionKey.namespace", permissions = NamespacePermissionEnum.READ)
    @Override
    public BusinessObjectDataKeys getAllBusinessObjectDataByBusinessObjectDefinition(
            BusinessObjectDefinitionKey businessObjectDefinitionKey) {
        // Perform validation and trim.
        businessObjectDefinitionHelper.validateBusinessObjectDefinitionKey(businessObjectDefinitionKey);

        // Ensure that a business object definition already exists with the specified name.
        BusinessObjectDefinitionEntity businessObjectDefinitionEntity = businessObjectDefinitionDaoHelper
                .getBusinessObjectDefinitionEntity(businessObjectDefinitionKey);

        // Get the maximum number of records to return.
        Integer maxResults = configurationHelper
                .getProperty(ConfigurationValue.BUSINESS_OBJECT_DATA_GET_ALL_MAX_RESULT_COUNT, Integer.class);

        // Gets the list of keys and return them.
        BusinessObjectDataKeys businessObjectDataKeys = new BusinessObjectDataKeys();
        businessObjectDataKeys.getBusinessObjectDataKeys().addAll(businessObjectDataDao
                .getBusinessObjectDataByBusinessObjectDefinition(businessObjectDefinitionEntity, maxResults));
        return businessObjectDataKeys;
    }

    @NamespacePermission(fields = "#businessObjectFormatKey.namespace", permissions = NamespacePermissionEnum.READ)
    @Override
    public BusinessObjectDataKeys getAllBusinessObjectDataByBusinessObjectFormat(
            BusinessObjectFormatKey businessObjectFormatKey) {
        // Perform validation and trim. Please note that we specify business object format version parameter to be required.
        businessObjectFormatHelper.validateBusinessObjectFormatKey(businessObjectFormatKey, true);

        // Ensure that a business object definition already exists with the specified name.
        BusinessObjectFormatEntity businessObjectFormatEntity = businessObjectFormatDaoHelper
                .getBusinessObjectFormatEntity(businessObjectFormatKey);

        // Get the maximum number of records to return.
        Integer maxResults = configurationHelper
                .getProperty(ConfigurationValue.BUSINESS_OBJECT_DATA_GET_ALL_MAX_RESULT_COUNT, Integer.class);

        // Gets the list of business object data keys and return them.
        BusinessObjectDataKeys businessObjectDataKeys = new BusinessObjectDataKeys();
        businessObjectDataKeys.getBusinessObjectDataKeys().addAll(businessObjectDataDao
                .getBusinessObjectDataByBusinessObjectFormat(businessObjectFormatEntity, maxResults));
        return businessObjectDataKeys;
    }

    @NamespacePermission(fields = "#businessObjectDataKey.namespace", permissions = NamespacePermissionEnum.READ)
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public BusinessObjectData getBusinessObjectData(BusinessObjectDataKey businessObjectDataKey,
            String businessObjectFormatPartitionKey, String businessObjectDataStatus,
            Boolean includeBusinessObjectDataStatusHistory, Boolean includeStorageUnitStatusHistory) {
        return getBusinessObjectDataImpl(businessObjectDataKey, businessObjectFormatPartitionKey,
                businessObjectDataStatus, includeBusinessObjectDataStatusHistory, includeStorageUnitStatusHistory);
    }

    @NamespacePermission(fields = "#businessObjectDataKey.namespace", permissions = NamespacePermissionEnum.READ)
    @Override
    public BusinessObjectDataVersions getBusinessObjectDataVersions(BusinessObjectDataKey businessObjectDataKey) {
        // Validate and trim the business object data key.
        businessObjectDataHelper.validateBusinessObjectDataKey(businessObjectDataKey, false, false);

        // Get the business object data versions based on the specified parameters.
        List<BusinessObjectDataEntity> businessObjectDataEntities = businessObjectDataDao
                .getBusinessObjectDataEntities(businessObjectDataKey);

        // Create the response.
        BusinessObjectDataVersions businessObjectDataVersions = new BusinessObjectDataVersions();
        for (BusinessObjectDataEntity businessObjectDataEntity : businessObjectDataEntities) {
            BusinessObjectDataVersion businessObjectDataVersion = new BusinessObjectDataVersion();
            BusinessObjectDataKey businessObjectDataVersionKey = businessObjectDataHelper
                    .getBusinessObjectDataKey(businessObjectDataEntity);
            businessObjectDataVersion.setBusinessObjectDataKey(businessObjectDataVersionKey);
            businessObjectDataVersion.setStatus(businessObjectDataEntity.getStatus().getCode());
            businessObjectDataVersions.getBusinessObjectDataVersions().add(businessObjectDataVersion);
        }

        return businessObjectDataVersions;
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Delegates implementation to {@link org.finra.herd.service.helper.BusinessObjectDataInvalidateUnregisteredHelper}. Starts a new transaction. Meant for
     * Activiti wrapper usage.
     */
    @PublishNotificationMessages
    @NamespacePermission(fields = "#businessObjectDataInvalidateUnregisteredRequest.namespace", permissions = NamespacePermissionEnum.WRITE)
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public BusinessObjectDataInvalidateUnregisteredResponse invalidateUnregisteredBusinessObjectData(
            BusinessObjectDataInvalidateUnregisteredRequest businessObjectDataInvalidateUnregisteredRequest) {
        return invalidateUnregisteredBusinessObjectDataImpl(businessObjectDataInvalidateUnregisteredRequest);
    }

    /**
     * {@inheritDoc}
     * <p/>
     * This implementation executes non-transactionally, suspends the current transaction if one exists.
     */
    @NamespacePermission(fields = "#businessObjectDataKey.namespace", permissions = NamespacePermissionEnum.WRITE)
    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public BusinessObjectData restoreBusinessObjectData(BusinessObjectDataKey businessObjectDataKey,
            Integer expirationInDays, String archiveRetrievalOption) {
        return restoreBusinessObjectDataImpl(businessObjectDataKey, expirationInDays, archiveRetrievalOption);
    }

    @NamespacePermission(fields = "#businessObjectDataKey.namespace", permissions = NamespacePermissionEnum.WRITE)
    @Override
    public BusinessObjectData retryStoragePolicyTransition(BusinessObjectDataKey businessObjectDataKey,
            BusinessObjectDataRetryStoragePolicyTransitionRequest request) {
        return businessObjectDataRetryStoragePolicyTransitionHelper
                .retryStoragePolicyTransition(businessObjectDataKey, request);
    }

    @NamespacePermission(fields = "#businessObjectDataSearchRequest.businessObjectDataSearchFilters[0].BusinessObjectDataSearchKeys[0].namespace", permissions = NamespacePermissionEnum.READ)
    @Override
    public BusinessObjectDataSearchResultPagingInfoDto searchBusinessObjectData(Integer pageNum, Integer pageSize,
            BusinessObjectDataSearchRequest businessObjectDataSearchRequest) {
        // TODO: Check name space permission for all entries in the request.
        // Validate the business object data search request.
        businessObjectDataSearchHelper.validateBusinessObjectDataSearchRequest(businessObjectDataSearchRequest);

        // Get the maximum number of results that can be returned on any page of data. The "pageSize" query parameter should not be greater than
        // this value or an HTTP status of 400 (Bad Request) error would be returned.
        int maxResultsPerPage = configurationHelper
                .getProperty(ConfigurationValue.BUSINESS_OBJECT_DATA_SEARCH_MAX_PAGE_SIZE, Integer.class);

        // Validate the page number and page size
        // Set the defaults if pageNum and pageSize are null
        // Page number must be greater than 0
        // Page size must be greater than 0 and less than maximum page size
        pageNum = businessObjectDataSearchHelper.validatePagingParameter("pageNum", pageNum, 1, Integer.MAX_VALUE);
        pageSize = businessObjectDataSearchHelper.validatePagingParameter("pageSize", pageSize, maxResultsPerPage,
                maxResultsPerPage);

        // Get the maximum record count that is configured in the system.
        Integer businessObjectDataSearchMaxResultCount = configurationHelper
                .getProperty(ConfigurationValue.BUSINESS_OBJECT_DATA_SEARCH_MAX_RESULT_COUNT, Integer.class);

        // Get the business object data search key.
        // We assume that the input list contains only one filter with a single search key, since validation should be passed by now.
        BusinessObjectDataSearchKey businessObjectDataSearchKey = businessObjectDataSearchRequest
                .getBusinessObjectDataSearchFilters().get(0).getBusinessObjectDataSearchKeys().get(0);

        // Validate partition keys in partition value filters.
        if (CollectionUtils.isNotEmpty(businessObjectDataSearchKey.getPartitionValueFilters())) {
            // Get a count of business object formats that match the business object data search key parameters without the list of partition keys.
            Long businessObjectFormatRecordCount = businessObjectFormatDao
                    .getBusinessObjectFormatCountByPartitionKeys(businessObjectDataSearchKey.getNamespace(),
                            businessObjectDataSearchKey.getBusinessObjectDefinitionName(),
                            businessObjectDataSearchKey.getBusinessObjectFormatUsage(),
                            businessObjectDataSearchKey.getBusinessObjectFormatFileType(),
                            businessObjectDataSearchKey.getBusinessObjectFormatVersion(), null);

            // If business object format record count is zero, we return an empty result list.
            if (businessObjectFormatRecordCount == 0) {
                return new BusinessObjectDataSearchResultPagingInfoDto(pageNum.longValue(), pageSize.longValue(),
                        0L, 0L, 0L, (long) maxResultsPerPage,
                        new BusinessObjectDataSearchResult(new ArrayList<>()));
            }

            // Get partition keys from the list of partition value filters.
            List<String> partitionKeys = new ArrayList<>();
            for (PartitionValueFilter partitionValueFilter : businessObjectDataSearchKey
                    .getPartitionValueFilters()) {
                // Get partition key from the partition value filter. Partition key should not be empty, since validation is passed by now.
                partitionKeys.add(partitionValueFilter.getPartitionKey());
            }

            // Get a count of business object formats that match the business object data search key parameters and the list of partition keys.
            businessObjectFormatRecordCount = businessObjectFormatDao.getBusinessObjectFormatCountByPartitionKeys(
                    businessObjectDataSearchKey.getNamespace(),
                    businessObjectDataSearchKey.getBusinessObjectDefinitionName(),
                    businessObjectDataSearchKey.getBusinessObjectFormatUsage(),
                    businessObjectDataSearchKey.getBusinessObjectFormatFileType(),
                    businessObjectDataSearchKey.getBusinessObjectFormatVersion(), partitionKeys);

            // Fail if business object formats found that contain specified partition keys in their schema.
            Assert.isTrue(businessObjectFormatRecordCount > 0, String.format(
                    "There are no registered business object formats with \"%s\" namespace, \"%s\" business object definition name",
                    businessObjectDataSearchKey.getNamespace(),
                    businessObjectDataSearchKey.getBusinessObjectDefinitionName())
                    + (StringUtils.isNotBlank(businessObjectDataSearchKey.getBusinessObjectFormatUsage())
                            ? String.format(", \"%s\" business object format usage",
                                    businessObjectDataSearchKey.getBusinessObjectFormatUsage())
                            : "")
                    + (StringUtils.isNotBlank(businessObjectDataSearchKey.getBusinessObjectFormatFileType())
                            ? String.format(", \"%s\" business object format file type",
                                    businessObjectDataSearchKey.getBusinessObjectFormatFileType())
                            : "")
                    + (businessObjectDataSearchKey.getBusinessObjectFormatVersion() != null
                            ? String.format(", \"%d\" business object format version",
                                    businessObjectDataSearchKey.getBusinessObjectFormatVersion())
                            : "")
                    + String.format(" that have schema with partition columns matching \"%s\" partition key(s).",
                            String.join(", ", partitionKeys)));
        }

        // Get the total record count up to to the maximum allowed record count that is configured in the system plus one more record.
        Integer totalRecordCount = businessObjectDataDao.getBusinessObjectDataLimitedCountBySearchKey(
                businessObjectDataSearchKey, businessObjectDataSearchMaxResultCount + 1);

        // Validate the total record count.
        if (totalRecordCount > businessObjectDataSearchMaxResultCount) {
            throw new IllegalArgumentException(
                    String.format("Result limit of %d exceeded. Modify filters to further limit results.",
                            businessObjectDataSearchMaxResultCount));
        }

        // If total record count is zero, we return an empty result list. Otherwise, execute the search.
        List<BusinessObjectData> businessObjectDataList = totalRecordCount == 0 ? new ArrayList<>()
                : businessObjectDataDao.searchBusinessObjectData(businessObjectDataSearchKey, pageNum, pageSize);

        // Get the page count.
        Integer pageCount = totalRecordCount / pageSize + (totalRecordCount % pageSize > 0 ? 1 : 0);

        // Build and return the business object data search result with the paging information.
        return new BusinessObjectDataSearchResultPagingInfoDto(pageNum.longValue(), pageSize.longValue(),
                pageCount.longValue(), (long) businessObjectDataList.size(), totalRecordCount.longValue(),
                (long) maxResultsPerPage, new BusinessObjectDataSearchResult(businessObjectDataList));
    }

    @NamespacePermission(fields = "#businessObjectDataKey.namespace", permissions = { NamespacePermissionEnum.WRITE,
            NamespacePermissionEnum.WRITE_ATTRIBUTE })
    @Override
    public BusinessObjectData updateBusinessObjectDataAttributes(BusinessObjectDataKey businessObjectDataKey,
            BusinessObjectDataAttributesUpdateRequest businessObjectDataAttributesUpdateRequest) {
        // Validate and trim the business object data key.
        businessObjectDataHelper.validateBusinessObjectDataKey(businessObjectDataKey, true, true);

        // Validate the update request.
        Assert.notNull(businessObjectDataAttributesUpdateRequest,
                "A business object data attributes update request must be specified.");
        Assert.notNull(businessObjectDataAttributesUpdateRequest.getAttributes(),
                "A list of business object data attributes must be specified.");
        List<Attribute> attributes = businessObjectDataAttributesUpdateRequest.getAttributes();

        // Validate attributes. This is also going to trim the attribute names.
        attributeHelper.validateAttributes(attributes);

        // Retrieve the business object data and ensure it exists.
        BusinessObjectDataEntity businessObjectDataEntity = businessObjectDataDaoHelper
                .getBusinessObjectDataEntity(businessObjectDataKey);

        // Validate attributes against attribute definitions.
        attributeDaoHelper.validateAttributesAgainstBusinessObjectDataAttributeDefinitions(attributes,
                businessObjectDataEntity.getBusinessObjectFormat().getAttributeDefinitions());

        // Update the attributes.
        attributeDaoHelper.updateBusinessObjectDataAttributes(businessObjectDataEntity, attributes);

        // Persist and refresh the entity.
        businessObjectDataEntity = businessObjectDataDao.saveAndRefresh(businessObjectDataEntity);

        // Create and return the business object data object from the persisted entity.
        return businessObjectDataHelper.createBusinessObjectDataFromEntity(businessObjectDataEntity);
    }

    @NamespacePermission(fields = "#businessObjectDataKey.namespace", permissions = NamespacePermissionEnum.WRITE)
    @Override
    public BusinessObjectData updateBusinessObjectDataParents(BusinessObjectDataKey businessObjectDataKey,
            BusinessObjectDataParentsUpdateRequest businessObjectDataParentsUpdateRequest) {
        // Validate and trim the business object data key.
        businessObjectDataHelper.validateBusinessObjectDataKey(businessObjectDataKey, true, true);

        // Validate the update request.
        Assert.notNull(businessObjectDataParentsUpdateRequest,
                "A business object data parents update request must be specified.");

        // Validate and trim the parents' keys.
        businessObjectDataDaoHelper.validateBusinessObjectDataKeys(
                businessObjectDataParentsUpdateRequest.getBusinessObjectDataParents());

        // Retrieve the business object data and ensure it exists.
        BusinessObjectDataEntity businessObjectDataEntity = businessObjectDataDaoHelper
                .getBusinessObjectDataEntity(businessObjectDataKey);

        // Fail if this business object data is not in a pre-registration status.
        if (BooleanUtils.isNotTrue(businessObjectDataEntity.getStatus().getPreRegistrationStatus())) {
            throw new IllegalArgumentException(String.format(
                    "Unable to update parents for business object data because it has \"%s\" status, which is not one of pre-registration statuses.",
                    businessObjectDataEntity.getStatus().getCode()));
        }

        // Update parents.
        List<BusinessObjectDataEntity> businessObjectDataParents = businessObjectDataEntity
                .getBusinessObjectDataParents();

        // Remove all existing parents.
        businessObjectDataParents.clear();

        // Loop through all business object data parents specified in the request and add them one by one.
        if (CollectionUtils.isNotEmpty(businessObjectDataParentsUpdateRequest.getBusinessObjectDataParents())) {
            for (BusinessObjectDataKey businessObjectDataParentKey : businessObjectDataParentsUpdateRequest
                    .getBusinessObjectDataParents()) {
                // Look up parent business object data.
                BusinessObjectDataEntity businessObjectDataParentEntity = businessObjectDataDaoHelper
                        .getBusinessObjectDataEntity(businessObjectDataParentKey);

                // Add business object data entity being updated as a dependent (i.e. child) of the looked up parent.
                businessObjectDataParentEntity.getBusinessObjectDataChildren().add(businessObjectDataEntity);

                // Add the looked up parent as a parent of the business object data entity being updated.
                businessObjectDataParents.add(businessObjectDataParentEntity);
            }
        }

        // Persist and refresh the entity.
        businessObjectDataEntity = businessObjectDataDao.saveAndRefresh(businessObjectDataEntity);

        // Create and return the business object data object from the persisted entity.
        return businessObjectDataHelper.createBusinessObjectDataFromEntity(businessObjectDataEntity);
    }

    @NamespacePermission(fields = "#businessObjectDataKey.namespace", permissions = NamespacePermissionEnum.WRITE)
    @Override
    public BusinessObjectData updateBusinessObjectDataRetentionInformation(
            BusinessObjectDataKey businessObjectDataKey,
            BusinessObjectDataRetentionInformationUpdateRequest businessObjectDataRetentionInformationUpdateRequest) {
        // Validate and trim the business object data key.
        businessObjectDataHelper.validateBusinessObjectDataKey(businessObjectDataKey, true, true);

        // Validate the update request.
        Assert.notNull(businessObjectDataRetentionInformationUpdateRequest,
                "A business object data retention information update request must be specified.");

        // Retrieve the business object data and ensure it exists.
        BusinessObjectDataEntity businessObjectDataEntity = businessObjectDataDaoHelper
                .getBusinessObjectDataEntity(businessObjectDataKey);

        // Get the latest version of the business object format for this business object data.
        BusinessObjectFormatKey businessObjectFormatKey = new BusinessObjectFormatKey(
                businessObjectDataKey.getNamespace(), businessObjectDataKey.getBusinessObjectDefinitionName(),
                businessObjectDataKey.getBusinessObjectFormatUsage(),
                businessObjectDataKey.getBusinessObjectFormatFileType(), null);
        BusinessObjectFormatEntity businessObjectFormatEntity = businessObjectFormatDaoHelper
                .getBusinessObjectFormatEntity(businessObjectFormatKey);

        // Fail if business object format for this business object data does not have
        // retention information configured with BDATA_RETENTION_DATE retention type.
        if (businessObjectFormatEntity.getRetentionType() == null || !businessObjectFormatEntity.getRetentionType()
                .getCode().equals(RetentionTypeEntity.BDATA_RETENTION_DATE)) {
            throw new IllegalArgumentException(String.format(
                    "Retention information with %s retention type must be configured for business object format. Business object format: {%s}",
                    RetentionTypeEntity.BDATA_RETENTION_DATE,
                    businessObjectFormatHelper.businessObjectFormatKeyToString(businessObjectFormatKey)));
        }

        // Update the retention information.
        businessObjectDataEntity
                .setRetentionExpiration(
                        businessObjectDataRetentionInformationUpdateRequest.getRetentionExpirationDate() != null
                                ? new Timestamp(businessObjectDataRetentionInformationUpdateRequest
                                        .getRetentionExpirationDate().toGregorianCalendar().getTimeInMillis())
                                : null);

        // Persist and refresh the entity.
        businessObjectDataEntity = businessObjectDataDao.saveAndRefresh(businessObjectDataEntity);

        // Create and return the business object data object from the persisted entity.
        return businessObjectDataHelper.createBusinessObjectDataFromEntity(businessObjectDataEntity);
    }

    /**
     * Performs an availability check for a collection of business object data.
     *
     * @param businessObjectDataAvailabilityCollectionRequest the business object data availability collection requests
     *
     * @return the business object data availability information
     */
    BusinessObjectDataAvailabilityCollectionResponse checkBusinessObjectDataAvailabilityCollectionImpl(
            BusinessObjectDataAvailabilityCollectionRequest businessObjectDataAvailabilityCollectionRequest) {
        // Perform the validation of the entire request, before we start processing the individual requests that requires the database access.
        validateBusinessObjectDataAvailabilityCollectionRequest(businessObjectDataAvailabilityCollectionRequest);

        // Process the individual requests and build the response.
        BusinessObjectDataAvailabilityCollectionResponse businessObjectDataAvailabilityCollectionResponse = new BusinessObjectDataAvailabilityCollectionResponse();
        List<BusinessObjectDataAvailability> businessObjectDataAvailabilityResponses = new ArrayList<>();
        businessObjectDataAvailabilityCollectionResponse
                .setBusinessObjectDataAvailabilityResponses(businessObjectDataAvailabilityResponses);
        boolean isAllDataAvailable = true;
        boolean isAllDataNotAvailable = true;
        for (BusinessObjectDataAvailabilityRequest request : businessObjectDataAvailabilityCollectionRequest
                .getBusinessObjectDataAvailabilityRequests()) {
            // Please note that when calling to process individual availability requests, we ask to skip the request validation and trimming step.
            BusinessObjectDataAvailability businessObjectDataAvailability = checkBusinessObjectDataAvailabilityImpl(
                    request, true);
            businessObjectDataAvailabilityResponses.add(businessObjectDataAvailability);
            isAllDataAvailable = isAllDataAvailable
                    && businessObjectDataAvailability.getNotAvailableStatuses().isEmpty();
            isAllDataNotAvailable = isAllDataNotAvailable
                    && businessObjectDataAvailability.getAvailableStatuses().isEmpty();
        }
        businessObjectDataAvailabilityCollectionResponse.setIsAllDataAvailable(isAllDataAvailable);
        businessObjectDataAvailabilityCollectionResponse.setIsAllDataNotAvailable(isAllDataNotAvailable);

        return businessObjectDataAvailabilityCollectionResponse;
    }

    /**
     * Performs a search and returns a list of business object data key values and relative statuses for a range of requested business object data.
     *
     * @param request the business object data availability request
     *
     * @return the business object data availability information
     */
    BusinessObjectDataAvailability checkBusinessObjectDataAvailabilityImpl(
            BusinessObjectDataAvailabilityRequest request) {
        // By default, validate and trim the request.
        return checkBusinessObjectDataAvailabilityImpl(request, false);
    }

    /**
     * Initiates destruction process for an existing business object data by using S3 tagging to mark the relative S3 files for deletion and updating statuses
     * of the business object data and its storage unit. The S3 data then gets deleted by S3 bucket lifecycle policy that is based on S3 tagging.
     *
     * @param businessObjectDataKey the business object data key
     *
     * @return the business object data information
     */
    BusinessObjectData destroyBusinessObjectDataImpl(BusinessObjectDataKey businessObjectDataKey) {
        // Create a business object data destroy parameters DTO.
        BusinessObjectDataDestroyDto businessObjectDataDestroyDto = new BusinessObjectDataDestroyDto();

        // Prepare to initiate a business object data destroy request.
        businessObjectDataInitiateDestroyHelperService.prepareToInitiateDestroy(businessObjectDataDestroyDto,
                businessObjectDataKey);

        // Create a storage unit notification for the storage unit status change event.
        notificationEventService.processStorageUnitNotificationEventAsync(
                NotificationEventTypeEntity.EventTypesStorageUnit.STRGE_UNIT_STTS_CHG,
                businessObjectDataDestroyDto.getBusinessObjectDataKey(),
                businessObjectDataDestroyDto.getStorageName(),
                businessObjectDataDestroyDto.getNewStorageUnitStatus(),
                businessObjectDataDestroyDto.getOldStorageUnitStatus());

        // Create a business object data notification for the business object data status change event.
        notificationEventService.processBusinessObjectDataNotificationEventAsync(
                NotificationEventTypeEntity.EventTypesBdata.BUS_OBJCT_DATA_STTS_CHG,
                businessObjectDataDestroyDto.getBusinessObjectDataKey(),
                businessObjectDataDestroyDto.getNewBusinessObjectDataStatus(),
                businessObjectDataDestroyDto.getOldBusinessObjectDataStatus());

        // Execute S3 specific steps.
        businessObjectDataInitiateDestroyHelperService.executeS3SpecificSteps(businessObjectDataDestroyDto);

        // Complete the initiation of a business object data destroy request.
        BusinessObjectData businessObjectData = businessObjectDataInitiateDestroyHelperService
                .executeInitiateDestroyAfterStep(businessObjectDataDestroyDto);

        // Create a storage unit notification for the storage unit status change event.
        notificationEventService.processStorageUnitNotificationEventAsync(
                NotificationEventTypeEntity.EventTypesStorageUnit.STRGE_UNIT_STTS_CHG,
                businessObjectDataDestroyDto.getBusinessObjectDataKey(),
                businessObjectDataDestroyDto.getStorageName(),
                businessObjectDataDestroyDto.getNewStorageUnitStatus(),
                businessObjectDataDestroyDto.getOldStorageUnitStatus());

        return businessObjectData;
    }

    /**
     * Retrieves the DDL to initialize the specified type of the database system to perform queries for a collection of business object data in the specified
     * storages.
     *
     * @param businessObjectDataDdlCollectionRequest the business object data DDL collection request
     *
     * @return the business object data DDL information
     */
    BusinessObjectDataDdlCollectionResponse generateBusinessObjectDataDdlCollectionImpl(
            BusinessObjectDataDdlCollectionRequest businessObjectDataDdlCollectionRequest) {
        // Perform the validation of the entire request, before we start processing the individual requests that requires the database access.
        validateBusinessObjectDataDdlCollectionRequest(businessObjectDataDdlCollectionRequest);

        // Process the individual requests and build the response.
        BusinessObjectDataDdlCollectionResponse businessObjectDataDdlCollectionResponse = new BusinessObjectDataDdlCollectionResponse();
        List<BusinessObjectDataDdl> businessObjectDataDdlResponses = new ArrayList<>();
        businessObjectDataDdlCollectionResponse.setBusinessObjectDataDdlResponses(businessObjectDataDdlResponses);
        List<String> ddls = new ArrayList<>();
        for (BusinessObjectDataDdlRequest request : businessObjectDataDdlCollectionRequest
                .getBusinessObjectDataDdlRequests()) {
            // Please note that when calling to process individual ddl requests, we ask to skip the request validation and trimming step.
            BusinessObjectDataDdl businessObjectDataDdl = generateBusinessObjectDataDdlImpl(request, true);
            businessObjectDataDdlResponses.add(businessObjectDataDdl);
            ddls.add(businessObjectDataDdl.getDdl());
        }
        businessObjectDataDdlCollectionResponse.setDdlCollection(StringUtils.join(ddls, "\n\n"));

        return businessObjectDataDdlCollectionResponse;
    }

    /**
     * Retrieves the DDL to initialize the specified type of the database system to perform queries for a range of requested business object data in the
     * specified storage.
     *
     * @param request the business object data DDL request
     * @param skipRequestValidation specifies whether to skip the request validation and trimming
     *
     * @return the business object data DDL information
     */
    BusinessObjectDataDdl generateBusinessObjectDataDdlImpl(BusinessObjectDataDdlRequest request,
            boolean skipRequestValidation) {
        // Perform the validation.
        if (!skipRequestValidation) {
            validateBusinessObjectDataDdlRequest(request);
        }

        // Get the business object format entity for the specified parameters and make sure it exists.
        // Please note that when format version is not specified, we should get back the latest format version.
        BusinessObjectFormatEntity businessObjectFormatEntity = businessObjectFormatDaoHelper
                .getBusinessObjectFormatEntity(new BusinessObjectFormatKey(request.getNamespace(),
                        request.getBusinessObjectDefinitionName(), request.getBusinessObjectFormatUsage(),
                        request.getBusinessObjectFormatFileType(), request.getBusinessObjectFormatVersion()));

        // Validate that format has schema information.
        Assert.notEmpty(businessObjectFormatEntity.getSchemaColumns(), String.format(
                "Business object format with namespace \"%s\", business object definition name \"%s\", format usage \"%s\", format file type \"%s\","
                        + " and format version \"%s\" doesn't have schema information.",
                businessObjectFormatEntity.getBusinessObjectDefinition().getNamespace().getCode(),
                businessObjectFormatEntity.getBusinessObjectDefinition().getName(),
                businessObjectFormatEntity.getUsage(), businessObjectFormatEntity.getFileType().getCode(),
                businessObjectFormatEntity.getBusinessObjectFormatVersion()));

        // If it was specified, retrieve the custom DDL and ensure it exists.
        CustomDdlEntity customDdlEntity = null;
        if (StringUtils.isNotBlank(request.getCustomDdlName())) {
            CustomDdlKey customDdlKey = new CustomDdlKey(
                    businessObjectFormatEntity.getBusinessObjectDefinition().getNamespace().getCode(),
                    businessObjectFormatEntity.getBusinessObjectDefinition().getName(),
                    businessObjectFormatEntity.getUsage(), businessObjectFormatEntity.getFileType().getCode(),
                    businessObjectFormatEntity.getBusinessObjectFormatVersion(), request.getCustomDdlName());
            customDdlEntity = customDdlDaoHelper.getCustomDdlEntity(customDdlKey);
        }

        // Build a list of storage names specified in the request.
        List<String> storageNames = new ArrayList<>();
        if (StringUtils.isNotBlank(request.getStorageName())) {
            storageNames.add(request.getStorageName());
        }
        if (!CollectionUtils.isEmpty(request.getStorageNames())) {
            storageNames.addAll(request.getStorageNames());
        }

        // Validate that storage entities, specified in the request, exist, of a proper storage platform type, and have S3 bucket name configured.
        Map<String, StorageEntity> storageEntities = new HashMap<>();
        Map<String, String> s3BucketNames = new HashMap<>();
        for (String storageName : storageNames) {
            StorageEntity storageEntity = storageDaoHelper.getStorageEntity(storageName);

            // Only S3 storage platform is currently supported.
            Assert.isTrue(storageEntity.getStoragePlatform().getName().equals(StoragePlatformEntity.S3),
                    String.format("Cannot generate DDL for \"%s\" storage platform.",
                            storageEntity.getStoragePlatform().getName()));

            // Validate that storage have S3 bucket name configured. Please note that since S3 bucket name attribute value is required we pass a "true" flag.
            String s3BucketName = storageHelper.getStorageAttributeValueByName(
                    configurationHelper.getProperty(ConfigurationValue.S3_ATTRIBUTE_NAME_BUCKET_NAME),
                    storageEntity, true);

            // Memorize retrieved values for faster processing.
            String upperCaseStorageName = storageName.toUpperCase();
            storageEntities.put(upperCaseStorageName, storageEntity);
            s3BucketNames.put(upperCaseStorageName, s3BucketName);
        }

        // Create and initialize a business object data DDL object instance.
        BusinessObjectDataDdl businessObjectDataDdl = createBusinessObjectDataDdl(request);
        businessObjectDataDdl.setDdl(
                ddlGeneratorFactory.getDdlGenerator(request.getOutputFormat()).generateCreateTableDdl(request,
                        businessObjectFormatEntity, customDdlEntity, storageNames, storageEntities, s3BucketNames));

        return businessObjectDataDdl;
    }

    /**
     * Retrieves existing business object data entry information. This method does not start a new transaction and instead continues with existing transaction,
     * if any.
     *
     * @param businessObjectDataKey the business object data key
     * @param businessObjectFormatPartitionKey the business object format partition key, may be null
     * @param businessObjectDataStatus the business object data status, may be null
     * @param includeBusinessObjectDataStatusHistory specifies to include business object data status history in the response
     * @param includeStorageUnitStatusHistory specifies to include storage unit status history for each storage unit in the response
     *
     * @return the retrieved business object data information
     */
    BusinessObjectData getBusinessObjectDataImpl(BusinessObjectDataKey businessObjectDataKey,
            String businessObjectFormatPartitionKey, String businessObjectDataStatus,
            Boolean includeBusinessObjectDataStatusHistory, Boolean includeStorageUnitStatusHistory) {
        // Validate and trim the business object data key.
        businessObjectDataHelper.validateBusinessObjectDataKey(businessObjectDataKey, false, false);

        // If specified, trim the partition key parameter.
        String businessObjectFormatPartitionKeyLocal = businessObjectFormatPartitionKey != null
                ? businessObjectFormatPartitionKey.trim()
                : null;

        // If specified, trim the business object data status parameter; otherwise default to VALID status.
        String businessObjectDataStatusLocal = businessObjectDataStatus != null ? businessObjectDataStatus.trim()
                : BusinessObjectDataStatusEntity.VALID;

        // Validate the business object data status.
        BusinessObjectDataStatusEntity businessObjectDataStatusEntity = businessObjectDataStatusDaoHelper
                .getBusinessObjectDataStatusEntity(businessObjectDataStatusLocal);

        // Get the business object data based on the specified parameters. If a business object data version isn't specified,
        // the latest version of business object data of the specified business object data status is returned.
        BusinessObjectDataEntity businessObjectDataEntity = businessObjectDataDaoHelper
                .getBusinessObjectDataEntityByKeyAndStatus(businessObjectDataKey,
                        businessObjectDataStatusEntity.getCode());

        // If specified, ensure the partition key matches what's configured within the business object format.
        if (StringUtils.isNotBlank(businessObjectFormatPartitionKeyLocal)) {
            String configuredPartitionKey = businessObjectDataEntity.getBusinessObjectFormat().getPartitionKey();
            Assert.isTrue(configuredPartitionKey.equalsIgnoreCase(businessObjectFormatPartitionKeyLocal),
                    String.format(
                            "Partition key \"%s\" doesn't match configured business object format partition key \"%s\".",
                            businessObjectFormatPartitionKeyLocal, configuredPartitionKey));
        }

        // Create and return the business object definition object from the persisted entity.
        return businessObjectDataHelper.createBusinessObjectDataFromEntity(businessObjectDataEntity,
                includeBusinessObjectDataStatusHistory, includeStorageUnitStatusHistory);
    }

    /**
     * Delegates implementation to {@link org.finra.herd.service.helper.BusinessObjectDataInvalidateUnregisteredHelper}. Keeps current transaction context.
     *
     * @param businessObjectDataInvalidateUnregisteredRequest {@link org.finra.herd.model.api.xml.BusinessObjectDataInvalidateUnregisteredRequest}
     *
     * @return {@link BusinessObjectDataInvalidateUnregisteredResponse}
     */
    BusinessObjectDataInvalidateUnregisteredResponse invalidateUnregisteredBusinessObjectDataImpl(
            BusinessObjectDataInvalidateUnregisteredRequest businessObjectDataInvalidateUnregisteredRequest) {
        return businessObjectDataInvalidateUnregisteredHelper
                .invalidateUnregisteredBusinessObjectData(businessObjectDataInvalidateUnregisteredRequest);
    }

    /**
     * Initiates a restore request for a currently archived business object data. Keeps current transaction context.
     *
     * @param businessObjectDataKey the business object data key
     * @param expirationInDays the the time, in days, between when the business object data is restored to the S3 bucket and when it expires
     * @param archiveRetrievalOption the archive retrieval option when restoring an archived object. Currently three options are supported: Expedited, Standard,
     * and Bulk
     *
     * @return the business object data information
     */
    BusinessObjectData restoreBusinessObjectDataImpl(BusinessObjectDataKey businessObjectDataKey,
            Integer expirationInDays, String archiveRetrievalOption) {
        // Execute the initiate a restore request before step.
        BusinessObjectDataRestoreDto businessObjectDataRestoreDto = businessObjectDataInitiateRestoreHelperService
                .prepareToInitiateRestore(businessObjectDataKey, expirationInDays, archiveRetrievalOption);

        // Create storage unit notification for the origin storage unit.
        notificationEventService.processStorageUnitNotificationEventAsync(
                NotificationEventTypeEntity.EventTypesStorageUnit.STRGE_UNIT_STTS_CHG,
                businessObjectDataRestoreDto.getBusinessObjectDataKey(),
                businessObjectDataRestoreDto.getStorageName(),
                businessObjectDataRestoreDto.getNewStorageUnitStatus(),
                businessObjectDataRestoreDto.getOldStorageUnitStatus());

        // Initiate the restore request.
        businessObjectDataInitiateRestoreHelperService.executeS3SpecificSteps(businessObjectDataRestoreDto);

        // On failure of the above step, execute the "after" step, and re-throw the exception.
        if (businessObjectDataRestoreDto.getException() != null) {
            // On failure, execute the after step that updates the storage unit status back to ARCHIVED.
            businessObjectDataInitiateRestoreHelperService
                    .executeInitiateRestoreAfterStep(businessObjectDataRestoreDto);

            // Create storage unit notification for the origin storage unit.
            notificationEventService.processStorageUnitNotificationEventAsync(
                    NotificationEventTypeEntity.EventTypesStorageUnit.STRGE_UNIT_STTS_CHG,
                    businessObjectDataRestoreDto.getBusinessObjectDataKey(),
                    businessObjectDataRestoreDto.getStorageName(),
                    businessObjectDataRestoreDto.getNewStorageUnitStatus(),
                    businessObjectDataRestoreDto.getOldStorageUnitStatus());

            // Re-throw the original exception.
            throw businessObjectDataRestoreDto.getException();
        } else {
            // Execute the after step for the initiate a business object data restore request
            // and return the business object data information.
            return businessObjectDataInitiateRestoreHelperService
                    .executeInitiateRestoreAfterStep(businessObjectDataRestoreDto);
        }
    }

    /**
     * Updates the list of not-available statuses by adding business object data status instances created from discovered "non-available" registered
     * sub-partitions as per list of "matched" partition filters to the specified list of not-available statuses.
     *
     * @param notAvailableStatuses the list of not-available statuses to be updated
     * @param businessObjectFormatKey the business object format key
     * @param matchedAvailablePartitionFilters the list of "matched" partition filters
     * @param availablePartitions the list of already discovered "available" partitions, where each partition consists of primary and optional sub-partition
     * values
     * @param storageNames the list of storage names
     */
    private void addNotAvailableBusinessObjectDataStatuses(List<BusinessObjectDataStatus> notAvailableStatuses,
            BusinessObjectFormatKey businessObjectFormatKey, List<List<String>> matchedAvailablePartitionFilters,
            List<List<String>> availablePartitions, List<String> storageNames) {
        // Now try to retrieve latest business object data per list of matched filters regardless of business object data and/or storage unit statuses.
        // This is done to include all registered sub-partitions in the response.
        // Business object data availability works across all storage platform types, so the storage platform type is not specified in the herdDao call.
        // We want to select any existing storage units regardless of their status, so we pass "false" for selectOnlyAvailableStorageUnits parameter.
        List<StorageUnitAvailabilityDto> matchedNotAvailableStorageUnitEntities = storageUnitDao
                .getStorageUnitsByPartitionFilters(businessObjectFormatKey, matchedAvailablePartitionFilters, null,
                        null, storageNames, null, null, false);

        // Exclude all storage units with business object data having "DELETED" status.
        matchedNotAvailableStorageUnitEntities = storageUnitHelper.excludeBusinessObjectDataStatus(
                matchedNotAvailableStorageUnitEntities, BusinessObjectDataStatusEntity.DELETED);

        // Exclude all already discovered "available" partitions. Please note that, since we got here, the list of matched partitions can not be empty.
        matchedNotAvailableStorageUnitEntities = storageUnitHelper
                .excludePartitions(matchedNotAvailableStorageUnitEntities, availablePartitions);

        // Keep processing the matched "not available" storage units only when the list is not empty.
        if (!CollectionUtils.isEmpty(matchedNotAvailableStorageUnitEntities)) {
            // Populate the "not available" statuses with all found "not available" registered sub-partitions.
            addNotAvailableBusinessObjectDataStatuses(notAvailableStatuses, matchedNotAvailableStorageUnitEntities);
        }
    }

    /**
     * Adds business object data status instances created from the list of storage unit availability DTOs to the specified list of not-available statuses.
     *
     * @param notAvailableStatuses the list of not-available statuses
     * @param storageUnitAvailabilityDtos the list of storage unit availability DTOs
     */
    private void addNotAvailableBusinessObjectDataStatuses(List<BusinessObjectDataStatus> notAvailableStatuses,
            List<StorageUnitAvailabilityDto> storageUnitAvailabilityDtos) {
        for (StorageUnitAvailabilityDto storageUnitAvailabilityDto : storageUnitAvailabilityDtos) {
            notAvailableStatuses.add(createNotAvailableBusinessObjectDataStatus(storageUnitAvailabilityDto));
        }
    }

    /**
     * Performs a search and returns a list of business object data key values and relative statuses for a range of requested business object data.
     *
     * @param request the business object data availability request
     * @param skipRequestValidation specifies whether to skip the request validation and trimming
     *
     * @return the business object data availability information
     */
    private BusinessObjectDataAvailability checkBusinessObjectDataAvailabilityImpl(
            BusinessObjectDataAvailabilityRequest request, boolean skipRequestValidation) {
        // Perform the validation.
        if (!skipRequestValidation) {
            validateBusinessObjectDataAvailabilityRequest(request);
        }

        // Get business object format key from the request.
        BusinessObjectFormatKey businessObjectFormatKey = getBusinessObjectFormatKey(request);

        // Make sure that specified business object format exists.
        BusinessObjectFormatEntity businessObjectFormatEntity = businessObjectFormatDaoHelper
                .getBusinessObjectFormatEntity(businessObjectFormatKey);

        // Get the list of storage names from the request and validate that specified storage entities exist.
        List<String> storageNames = getStorageNames(request);
        storageDaoHelper.validateStorageExistence(storageNames);

        // Build partition filters based on the specified partition value filters.
        // Business object data availability works across all storage platform types, so the storage platform type is not specified in the call.
        List<List<String>> partitionFilters = businessObjectDataDaoHelper.buildPartitionFilters(
                request.getPartitionValueFilters(), request.getPartitionValueFilter(), businessObjectFormatKey,
                request.getBusinessObjectDataVersion(), storageNames, null, null, businessObjectFormatEntity);

        // Retrieve a list of storage unit availability DTOs for the specified partition values. The list will be sorted by partition value that is identified
        // by partition column position. If a business object data version isn't specified, the latest VALID business object data version is returned.
        // Business object data availability works across all storage platform types, so the storage platform type is not specified in the herdDao call.
        // We want to select only "available" storage units, so we pass "true" for selectOnlyAvailableStorageUnits parameter.
        List<StorageUnitAvailabilityDto> availableStorageUnitAvailabilityDtos = storageUnitDao
                .getStorageUnitsByPartitionFilters(businessObjectFormatKey, partitionFilters,
                        request.getBusinessObjectDataVersion(), BusinessObjectDataStatusEntity.VALID, storageNames,
                        null, null, true);

        // Create business object data availability object instance and initialise it with request field values.
        BusinessObjectDataAvailability businessObjectDataAvailability = createBusinessObjectDataAvailability(
                request);

        // Create "available" and "not available" business object data status lists.
        List<BusinessObjectDataStatus> availableStatuses = new ArrayList<>();
        businessObjectDataAvailability.setAvailableStatuses(availableStatuses);
        List<BusinessObjectDataStatus> notAvailableStatuses = new ArrayList<>();
        businessObjectDataAvailability.setNotAvailableStatuses(notAvailableStatuses);

        // Build a list of matched available partition filters and populate the available statuses list. Please note that each request partition filter
        // might result in multiple available business object data entities. If storage names are not specified, fail on "duplicate" business object data
        // (same business object data instance registered with multiple storages). Otherwise, remove possible "duplicates".
        List<List<String>> matchedAvailablePartitionFilters = new ArrayList<>();
        List<List<String>> availablePartitions = new ArrayList<>();
        Map<BusinessObjectDataKey, StorageUnitAvailabilityDto> businessObjectDataToStorageUnitMap = new HashMap<>();
        for (StorageUnitAvailabilityDto storageUnitAvailabilityDto : availableStorageUnitAvailabilityDtos) {
            BusinessObjectDataKey businessObjectDataKey = storageUnitAvailabilityDto.getBusinessObjectDataKey();

            if (businessObjectDataToStorageUnitMap.containsKey(businessObjectDataKey)) {
                // If storage names are not specified, fail on a business object data registered in multiple storage. Otherwise, ignore that storage unit.
                if (CollectionUtils.isEmpty(storageNames)) {
                    throw new IllegalArgumentException(String.format(
                            "Found business object data registered in more than one storage. "
                                    + "Please specify storage(s) in the request to resolve this. Business object data {%s}",
                            businessObjectDataHelper.businessObjectDataKeyToString(businessObjectDataKey)));
                }
            } else {
                matchedAvailablePartitionFilters.add(businessObjectDataHelper
                        .getPartitionFilter(businessObjectDataKey, partitionFilters.get(0)));
                availablePartitions
                        .add(businessObjectDataHelper.getPrimaryAndSubPartitionValues(businessObjectDataKey));
                availableStatuses.add(createAvailableBusinessObjectDataStatus(storageUnitAvailabilityDto));
                businessObjectDataToStorageUnitMap.put(businessObjectDataKey, storageUnitAvailabilityDto);
            }
        }

        // Check if request specifies to include all registered sub-partitions in the response.
        boolean includeAllRegisteredSubPartitions = request.getBusinessObjectDataVersion() == null
                && BooleanUtils.isTrue(request.isIncludeAllRegisteredSubPartitions());

        // If request specifies to include all registered sub-partitions in the response, query all
        // matched partition filters one more time to discover any non-available registered sub-partitions.
        if (includeAllRegisteredSubPartitions && !CollectionUtils.isEmpty(matchedAvailablePartitionFilters)) {
            addNotAvailableBusinessObjectDataStatuses(notAvailableStatuses, businessObjectFormatKey,
                    matchedAvailablePartitionFilters, availablePartitions, storageNames);
        }

        // Get a list of unmatched partition filters.
        List<List<String>> unmatchedPartitionFilters = new ArrayList<>(partitionFilters);
        unmatchedPartitionFilters.removeAll(matchedAvailablePartitionFilters);

        // We still need to try to retrieve business object data per list of unmatched filters regardless of business object data and/or storage unit statuses.
        // This is done to populate not-available statuses with legitimate reasons.
        // Business object data availability works across all storage platform types, so the storage platform type is not specified in the herdDao call.
        // We want to select any existing storage units regardless of their status, so we pass "false" for selectOnlyAvailableStorageUnits parameter.
        List<StorageUnitAvailabilityDto> notAvailableStorageUnitAvailabilityDtos = storageUnitDao
                .getStorageUnitsByPartitionFilters(businessObjectFormatKey, unmatchedPartitionFilters,
                        request.getBusinessObjectDataVersion(), null, storageNames, null, null, false);

        // Populate the not-available statuses list.
        addNotAvailableBusinessObjectDataStatuses(notAvailableStatuses, notAvailableStorageUnitAvailabilityDtos);

        // Build a list of matched "not-available" partition filters.
        // Please note that each request partition filter might result in multiple available business object data entities.
        List<List<String>> matchedNotAvailablePartitionFilters = getPartitionFilters(
                notAvailableStorageUnitAvailabilityDtos, partitionFilters.get(0));

        // Update the list of unmatched partition filters.
        unmatchedPartitionFilters.removeAll(matchedNotAvailablePartitionFilters);

        // Populate the "not available" statuses per remaining unmatched filters.
        for (List<String> unmatchedPartitionFilter : unmatchedPartitionFilters) {
            notAvailableStatuses.add(createNotAvailableBusinessObjectDataStatus(request, unmatchedPartitionFilter,
                    REASON_NOT_REGISTERED));
        }

        return businessObjectDataAvailability;
    }

    /**
     * Creates a business object data status instance from the storage unit availability DTO.
     *
     * @param storageUnitAvailabilityDto the storage unit availability DTO
     *
     * @return the business object data status instance
     */
    private BusinessObjectDataStatus createAvailableBusinessObjectDataStatus(
            StorageUnitAvailabilityDto storageUnitAvailabilityDto) {
        BusinessObjectDataStatus businessObjectDataStatus = new BusinessObjectDataStatus();

        businessObjectDataStatus.setBusinessObjectFormatVersion(
                storageUnitAvailabilityDto.getBusinessObjectDataKey().getBusinessObjectFormatVersion());
        businessObjectDataStatus
                .setPartitionValue(storageUnitAvailabilityDto.getBusinessObjectDataKey().getPartitionValue());
        businessObjectDataStatus.setSubPartitionValues(
                storageUnitAvailabilityDto.getBusinessObjectDataKey().getSubPartitionValues());
        businessObjectDataStatus.setBusinessObjectDataVersion(
                storageUnitAvailabilityDto.getBusinessObjectDataKey().getBusinessObjectDataVersion());
        businessObjectDataStatus.setReason(storageUnitAvailabilityDto.getBusinessObjectDataStatus());

        return businessObjectDataStatus;
    }

    /**
     * Creates business object data availability object instance and initialise it with the business object data availability request field values.
     *
     * @param request the business object data availability request
     *
     * @return the newly created BusinessObjectDataAvailability object instance
     */
    private BusinessObjectDataAvailability createBusinessObjectDataAvailability(
            BusinessObjectDataAvailabilityRequest request) {
        BusinessObjectDataAvailability businessObjectDataAvailability = new BusinessObjectDataAvailability();

        businessObjectDataAvailability.setNamespace(request.getNamespace());
        businessObjectDataAvailability.setBusinessObjectDefinitionName(request.getBusinessObjectDefinitionName());
        businessObjectDataAvailability.setBusinessObjectFormatUsage(request.getBusinessObjectFormatUsage());
        businessObjectDataAvailability.setBusinessObjectFormatFileType(request.getBusinessObjectFormatFileType());
        businessObjectDataAvailability.setBusinessObjectFormatVersion(request.getBusinessObjectFormatVersion());

        businessObjectDataAvailability.setPartitionValueFilters(request.getPartitionValueFilters());
        businessObjectDataAvailability.setPartitionValueFilter(request.getPartitionValueFilter());

        businessObjectDataAvailability.setBusinessObjectDataVersion(request.getBusinessObjectDataVersion());

        businessObjectDataAvailability.setStorageNames(request.getStorageNames());
        businessObjectDataAvailability.setStorageName(request.getStorageName());

        return businessObjectDataAvailability;
    }

    /**
     * Creates business object data ddl object instance and initialise it with the business object data ddl request field values.
     *
     * @param request the business object data ddl request
     *
     * @return the newly created BusinessObjectDataDdl object instance
     */
    private BusinessObjectDataDdl createBusinessObjectDataDdl(BusinessObjectDataDdlRequest request) {
        BusinessObjectDataDdl businessObjectDataDdl = new BusinessObjectDataDdl();

        businessObjectDataDdl.setNamespace(request.getNamespace());
        businessObjectDataDdl.setBusinessObjectDefinitionName(request.getBusinessObjectDefinitionName());
        businessObjectDataDdl.setBusinessObjectFormatUsage(request.getBusinessObjectFormatUsage());
        businessObjectDataDdl.setBusinessObjectFormatFileType(request.getBusinessObjectFormatFileType());
        businessObjectDataDdl.setBusinessObjectFormatVersion(request.getBusinessObjectFormatVersion());

        businessObjectDataDdl.setPartitionValueFilters(request.getPartitionValueFilters());
        businessObjectDataDdl.setPartitionValueFilter(request.getPartitionValueFilter());

        businessObjectDataDdl.setBusinessObjectDataVersion(request.getBusinessObjectDataVersion());

        businessObjectDataDdl.setStorageNames(request.getStorageNames());
        businessObjectDataDdl.setStorageName(request.getStorageName());

        businessObjectDataDdl.setOutputFormat(request.getOutputFormat());
        businessObjectDataDdl.setTableName(request.getTableName());
        businessObjectDataDdl.setCustomDdlName(request.getCustomDdlName());

        return businessObjectDataDdl;
    }

    /**
     * Creates a business object data status instance from the storage unit availability DTO.
     *
     * @param storageUnitAvailabilityDto the storage unit availability DTO
     *
     * @return the business object data status instance
     */
    private BusinessObjectDataStatus createNotAvailableBusinessObjectDataStatus(
            StorageUnitAvailabilityDto storageUnitAvailabilityDto) {
        BusinessObjectDataStatus businessObjectDataStatus = new BusinessObjectDataStatus();

        businessObjectDataStatus.setBusinessObjectFormatVersion(
                storageUnitAvailabilityDto.getBusinessObjectDataKey().getBusinessObjectFormatVersion());
        businessObjectDataStatus
                .setPartitionValue(storageUnitAvailabilityDto.getBusinessObjectDataKey().getPartitionValue());
        businessObjectDataStatus.setSubPartitionValues(
                storageUnitAvailabilityDto.getBusinessObjectDataKey().getSubPartitionValues());
        businessObjectDataStatus.setBusinessObjectDataVersion(
                storageUnitAvailabilityDto.getBusinessObjectDataKey().getBusinessObjectDataVersion());

        // If storage unit is "available", the business object data is selected as "non-available" due to its business object data status.
        if (storageUnitAvailabilityDto.isStorageUnitAvailable()) {
            businessObjectDataStatus.setReason(storageUnitAvailabilityDto.getBusinessObjectDataStatus());
        }
        // Otherwise, report the storage unit status as a reason for the business object data not being available.
        else {
            businessObjectDataStatus.setReason(storageUnitAvailabilityDto.getStorageUnitStatus());
        }

        return businessObjectDataStatus;
    }

    /**
     * Creates the business object data status.
     *
     * @param businessObjectDataAvailabilityRequest the business object data availability request
     * @param unmatchedPartitionFilter the partition filter that got no matched business object data instances
     * @param reason the reason for the business object data not being available
     *
     * @return the business object data status
     */
    private BusinessObjectDataStatus createNotAvailableBusinessObjectDataStatus(
            BusinessObjectDataAvailabilityRequest businessObjectDataAvailabilityRequest,
            List<String> unmatchedPartitionFilter, String reason) {
        BusinessObjectDataStatus businessObjectDataStatus = new BusinessObjectDataStatus();

        // Populate business object data status values using the business object data availability request.
        businessObjectDataStatus.setBusinessObjectFormatVersion(
                businessObjectDataAvailabilityRequest.getBusinessObjectFormatVersion());

        // When list of partition value filters is used, we populate primary and/or sub-partition values.
        if (businessObjectDataAvailabilityRequest.getPartitionValueFilters() != null) {
            // Replace all null partition values with an empty string.
            replaceAllNullsWithEmptyString(unmatchedPartitionFilter);

            // Populate primary and sub-partition values from the unmatched partition filter.
            businessObjectDataStatus.setPartitionValue(unmatchedPartitionFilter.get(0));
            businessObjectDataStatus
                    .setSubPartitionValues(unmatchedPartitionFilter.subList(1, unmatchedPartitionFilter.size()));
        }
        // Otherwise, for backwards compatibility, populate primary partition value only per expected single partition value from the unmatched filter.
        else {
            // Since the availability request contains a standalone partition value filter,
            // the unmatched partition filter is expected to contain only a single partition value.
            for (String partitionValue : unmatchedPartitionFilter) {
                if (partitionValue != null) {
                    businessObjectDataStatus.setPartitionValue(partitionValue);
                    break;
                }
            }
        }
        businessObjectDataStatus
                .setBusinessObjectDataVersion(businessObjectDataAvailabilityRequest.getBusinessObjectDataVersion());
        businessObjectDataStatus.setReason(reason);

        return businessObjectDataStatus;
    }

    /**
     * Gets business object format key from the business object data availability request.
     *
     * @param request the business object data availability request
     *
     * @return the business object format key
     */
    private BusinessObjectFormatKey getBusinessObjectFormatKey(BusinessObjectDataAvailabilityRequest request) {
        return new BusinessObjectFormatKey(request.getNamespace(), request.getBusinessObjectDefinitionName(),
                request.getBusinessObjectFormatUsage(), request.getBusinessObjectFormatFileType(),
                request.getBusinessObjectFormatVersion());
    }

    /**
     * Gets a list of matched partition filters per specified list of storage unit entities and a sample partition filter.
     *
     * @param storageUnitAvailabilityDtos the list of storage unit availability DTOs
     * @param samplePartitionFilter the sample partition filter
     *
     * @return the list of partition filters
     */
    private List<List<String>> getPartitionFilters(List<StorageUnitAvailabilityDto> storageUnitAvailabilityDtos,
            List<String> samplePartitionFilter) {
        List<List<String>> partitionFilters = new ArrayList<>();

        for (StorageUnitAvailabilityDto storageUnitAvailabilityDto : storageUnitAvailabilityDtos) {
            partitionFilters.add(businessObjectDataHelper.getPartitionFilter(
                    storageUnitAvailabilityDto.getBusinessObjectDataKey(), samplePartitionFilter));
        }

        return partitionFilters;
    }

    /**
     * Gets storage names from the business object data availability request.
     *
     * @param request the business object data availability request
     *
     * @return the list of storage names
     */
    private List<String> getStorageNames(BusinessObjectDataAvailabilityRequest request) {
        List<String> storageNames = new ArrayList<>();

        if (StringUtils.isNotBlank(request.getStorageName())) {
            storageNames.add(request.getStorageName());
        }

        if (!CollectionUtils.isEmpty(request.getStorageNames())) {
            storageNames.addAll(request.getStorageNames());
        }

        return storageNames;
    }

    /**
     * Replaces all null values in the specified list with empty strings.
     *
     * @param list the list of strings
     */
    private void replaceAllNullsWithEmptyString(List<String> list) {
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i) == null) {
                list.set(i, "");
            }
        }
    }

    /**
     * Validates a business object data availability collection request. This method also trims appropriate request parameters.
     *
     * @param businessObjectDataAvailabilityCollectionRequest the request
     *
     * @throws IllegalArgumentException if any validation errors were found
     */
    private void validateBusinessObjectDataAvailabilityCollectionRequest(
            BusinessObjectDataAvailabilityCollectionRequest businessObjectDataAvailabilityCollectionRequest) {
        Assert.notNull(businessObjectDataAvailabilityCollectionRequest,
                "A business object data availability collection request must be specified.");

        Assert.isTrue(
                !CollectionUtils.isEmpty(businessObjectDataAvailabilityCollectionRequest
                        .getBusinessObjectDataAvailabilityRequests()),
                "At least one business object data availability request must be specified.");

        for (BusinessObjectDataAvailabilityRequest request : businessObjectDataAvailabilityCollectionRequest
                .getBusinessObjectDataAvailabilityRequests()) {
            validateBusinessObjectDataAvailabilityRequest(request);
        }
    }

    /**
     * Validates the business object data availability request. This method also trims appropriate request parameters.
     *
     * @param request the request
     *
     * @throws IllegalArgumentException if any validation errors were found
     */
    private void validateBusinessObjectDataAvailabilityRequest(BusinessObjectDataAvailabilityRequest request) {
        Assert.notNull(request, "A business object data availability request must be specified.");

        // Validate and trim the request parameters.
        Assert.hasText(request.getNamespace(), "A namespace must be specified.");
        request.setNamespace(request.getNamespace().trim());

        Assert.hasText(request.getBusinessObjectDefinitionName(),
                "A business object definition name must be specified.");
        request.setBusinessObjectDefinitionName(request.getBusinessObjectDefinitionName().trim());

        Assert.hasText(request.getBusinessObjectFormatUsage(), "A business object format usage must be specified.");
        request.setBusinessObjectFormatUsage(request.getBusinessObjectFormatUsage().trim());

        Assert.hasText(request.getBusinessObjectFormatFileType(),
                "A business object format file type must be specified.");
        request.setBusinessObjectFormatFileType(request.getBusinessObjectFormatFileType().trim());

        // Validate the partition value filters. Allow partition value tokens to be specified.
        businessObjectDataHelper.validatePartitionValueFilters(request.getPartitionValueFilters(),
                request.getPartitionValueFilter(), true);

        // Make sure that request does not contain both a list of storage names and a standalone storage name.
        Assert.isTrue(request.getStorageNames() == null || request.getStorageName() == null,
                "A list of storage names and a standalone storage name cannot be both specified.");

        // Trim the standalone storage name, if specified.
        if (request.getStorageName() != null) {
            Assert.hasText(request.getStorageName(), "A storage name must be specified.");
            request.setStorageName(request.getStorageName().trim());
        }

        // Validate and trim the list of storage names.
        if (!CollectionUtils.isEmpty(request.getStorageNames())) {
            for (int i = 0; i < request.getStorageNames().size(); i++) {
                Assert.hasText(request.getStorageNames().get(i), "A storage name must be specified.");
                request.getStorageNames().set(i, request.getStorageNames().get(i).trim());
            }
        }
    }

    /**
     * Validates a business object data DDL collection request. This method also trims appropriate request parameters.
     *
     * @param businessObjectDataDdlCollectionRequest the request
     *
     * @throws IllegalArgumentException if any validation errors were found
     */
    private void validateBusinessObjectDataDdlCollectionRequest(
            BusinessObjectDataDdlCollectionRequest businessObjectDataDdlCollectionRequest) {
        Assert.notNull(businessObjectDataDdlCollectionRequest,
                "A business object data DDL collection request must be specified.");

        Assert.isTrue(
                !CollectionUtils.isEmpty(businessObjectDataDdlCollectionRequest.getBusinessObjectDataDdlRequests()),
                "At least one business object data DDL request must be specified.");

        for (BusinessObjectDataDdlRequest request : businessObjectDataDdlCollectionRequest
                .getBusinessObjectDataDdlRequests()) {
            validateBusinessObjectDataDdlRequest(request);
        }
    }

    /**
     * Validates the business object data DDL request. This method also trims appropriate request parameters.
     *
     * @param request the request
     *
     * @throws IllegalArgumentException if any validation errors were found
     */
    private void validateBusinessObjectDataDdlRequest(BusinessObjectDataDdlRequest request) {
        Assert.notNull(request, "A business object data DDL request must be specified.");

        // Validate and trim the request parameters.
        Assert.hasText(request.getNamespace(), "A namespace must be specified.");
        request.setNamespace(request.getNamespace().trim());

        Assert.hasText(request.getBusinessObjectDefinitionName(),
                "A business object definition name must be specified.");
        request.setBusinessObjectDefinitionName(request.getBusinessObjectDefinitionName().trim());

        Assert.hasText(request.getBusinessObjectFormatUsage(), "A business object format usage must be specified.");
        request.setBusinessObjectFormatUsage(request.getBusinessObjectFormatUsage().trim());

        Assert.hasText(request.getBusinessObjectFormatFileType(),
                "A business object format file type must be specified.");
        request.setBusinessObjectFormatFileType(request.getBusinessObjectFormatFileType().trim());

        // Validate the partition value filters. Do not allow partition value tokens to be specified.
        businessObjectDataHelper.validatePartitionValueFilters(request.getPartitionValueFilters(),
                request.getPartitionValueFilter(), false);

        // Make sure that request does not contain both a list of storage names and a standalone storage name.
        Assert.isTrue(request.getStorageNames() == null || request.getStorageName() == null,
                "A list of storage names and a standalone storage name cannot be both specified.");

        // Trim the standalone storage name, if specified.
        if (request.getStorageName() != null) {
            Assert.hasText(request.getStorageName(), "A storage name must be specified.");
            request.setStorageName(request.getStorageName().trim());
        }

        // Validate and trim the list of storage names.
        if (!CollectionUtils.isEmpty(request.getStorageNames())) {
            for (int i = 0; i < request.getStorageNames().size(); i++) {
                Assert.hasText(request.getStorageNames().get(i), "A storage name must be specified.");
                request.getStorageNames().set(i, request.getStorageNames().get(i).trim());
            }
        }

        Assert.notNull(request.getOutputFormat(), "An output format must be specified.");

        Assert.hasText(request.getTableName(), "A table name must be specified.");
        request.setTableName(request.getTableName().trim());

        if (StringUtils.isNotBlank(request.getCustomDdlName())) {
            request.setCustomDdlName(request.getCustomDdlName().trim());
        }
    }
}