class DownloaderController extends DataBridgeController {

    @SuppressFBWarnings(value = {"BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"}, justification =
        "manifestReader.readJsonManifest will always return an DownloaderInputManifestDto object. targetLocalDirectory.list().length will not" +
            " return a NullPointerException.")
    public void performDownload(RegServerAccessParamsDto regServerAccessParamsDto, File manifestPath,
        S3FileTransferRequestParamsDto s3FileTransferRequestParamsDto)
        throws InterruptedException, JAXBException, IOException, URISyntaxException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException
    {
        boolean cleanUpTargetLocalDirectoryOnFailure = false;
        File targetLocalDirectory = null;

        try
        {
            // Process manifest file.
            DownloaderInputManifestDto manifest = manifestReader.readJsonManifest(manifestPath);

            String storageName = getStorageNameFromManifest(manifest);

            // Get business object data from the herd registration server.
            downloaderWebClient.setRegServerAccessParamsDto(regServerAccessParamsDto);
            BusinessObjectData businessObjectData = downloaderWebClient.getBusinessObjectData(manifest);

            manifest.setBusinessObjectDataVersion(String.valueOf(businessObjectData.getVersion()));
            manifest.setBusinessObjectFormatVersion(String.valueOf(businessObjectData.getBusinessObjectFormatVersion()));
            s3FileTransferRequestParamsDto.getAdditionalAwsCredentialsProviders().add(new AutoRefreshCredentialProvider()
            {
                @Override
                public AwsCredential getNewAwsCredential() throws Exception
                {
                    return downloaderWebClient.getStorageUnitDownloadCredential(manifest, storageName).getAwsCredential();
                }
            });

            // Get a storage unit that belongs to the S3 storage.
            StorageUnit storageUnit = businessObjectDataHelper.getStorageUnitByStorageName(businessObjectData, storageName);

            // Get the expected S3 key prefix and S3 bucket name.
            S3KeyPrefixInformation s3KeyPrefixInformation = downloaderWebClient.getS3KeyPrefix(businessObjectData);

            // Check if the target folder (local directory + S3 key prefix) exists and try to create it if it does not.
            targetLocalDirectory = Paths.get(s3FileTransferRequestParamsDto.getLocalPath(), s3KeyPrefixInformation.getS3KeyPrefix()).toFile();
            if (!targetLocalDirectory.isDirectory())
            {
                // Create the local directory including any necessary but nonexistent parent directories.
                if (!targetLocalDirectory.mkdirs())
                {
                    throw new IllegalArgumentException(String.format("Failed to create target local directory \"%s\".", targetLocalDirectory.getPath()));
                }
            }
            else
            {
                // Check if the target local directory is empty.
                if (targetLocalDirectory.list().length > 0)
                {
                    throw new IllegalArgumentException(String.format("The target local directory \"%s\" is not empty.", targetLocalDirectory.getPath()));
                }
            }

            // Get S3 bucket information.
            Storage storage = downloaderWebClient.getStorage(storageName);

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

            // Get the list of S3 files matching the expected S3 key prefix.
            s3FileTransferRequestParamsDto.setS3BucketName(s3BucketName);
            // Since the S3 key prefix represents a directory, we add a trailing '/' character to it.
            s3FileTransferRequestParamsDto.setS3KeyPrefix(s3KeyPrefixInformation.getS3KeyPrefix() + "/");
            // When listing S3 files, we ignore 0 byte objects that represent S3 directories.
            List<String> actualS3Files = storageFileHelper.getFilePathsFromS3ObjectSummaries(s3Service.listDirectory(s3FileTransferRequestParamsDto, true));

            // Validate S3 files before we start the download.
            storageFileHelper.validateStorageUnitS3Files(storageUnit, actualS3Files, s3KeyPrefixInformation.getS3KeyPrefix());

            // Special handling for the maxThreads command line option.
            s3FileTransferRequestParamsDto.setMaxThreads(adjustIntegerValue(s3FileTransferRequestParamsDto.getMaxThreads(), MIN_THREADS, MAX_THREADS));

            // Download S3 files to the target local directory.
            s3FileTransferRequestParamsDto.setRecursive(true);
            cleanUpTargetLocalDirectoryOnFailure = true;
            s3Service.downloadDirectory(s3FileTransferRequestParamsDto);

            // Validate the downloaded files.
            storageFileHelper.validateDownloadedS3Files(s3FileTransferRequestParamsDto.getLocalPath(), s3KeyPrefixInformation.getS3KeyPrefix(), storageUnit);

            // Log a list of files downloaded to the target local directory.
            if (LOGGER.isInfoEnabled())
            {
                logLocalDirectoryContents(targetLocalDirectory);
            }

            // Create a downloader output manifest file.
            DownloaderOutputManifestDto downloaderOutputManifestDto =
                createDownloaderOutputManifestDto(businessObjectData, storageUnit, s3KeyPrefixInformation.getS3KeyPrefix());
            manifestWriter.writeJsonManifest(targetLocalDirectory, OUTPUT_MANIFEST_FILE_NAME, downloaderOutputManifestDto);
        }
        catch (InterruptedException | JAXBException | IOException | URISyntaxException e)
        {
            // If we got to the point of validating the target local directory being empty before this failure
            // occurred, let's rollback the data transfer by cleaning up the local target directory.
            if (cleanUpTargetLocalDirectoryOnFailure)
            {
                LOGGER.info(String.format("Rolling back the S3 data transfer by cleaning up \"%s\" target local directory.", targetLocalDirectory));
                HerdFileUtils.cleanDirectoryIgnoreException(targetLocalDirectory);
            }

            throw e;
        }
    }

}