diff --git a/app/configuration/ApplicationConfig.scala b/app/configuration/ApplicationConfig.scala index 73f183d90..c170e895a 100644 --- a/app/configuration/ApplicationConfig.scala +++ b/app/configuration/ApplicationConfig.scala @@ -31,6 +31,8 @@ class ApplicationConfig @Inject() (configuration: Configuration) { val draftMetadataFileName: String = configuration.get[String]("draftMetadata.fileName") + val draftMetadataErrorFileName: String = configuration.get[String]("draftMetadata.errorFileName") + val notificationSnsTopicArn: String = get("notificationSnsTopicArn") val fileChecksTotalTimoutInSeconds: Int = configuration.get[Int]("fileChecksTotalTimoutInSeconds") diff --git a/app/controllers/DraftMetadataChecksResultsController.scala b/app/controllers/DraftMetadataChecksResultsController.scala index af4d2adc3..75a66ae75 100644 --- a/app/controllers/DraftMetadataChecksResultsController.scala +++ b/app/controllers/DraftMetadataChecksResultsController.scala @@ -4,10 +4,10 @@ import auth.TokenSecurity import configuration.{ApplicationConfig, KeycloakConfiguration} import graphql.codegen.GetConsignmentStatus.getConsignmentStatus.GetConsignment import org.pac4j.play.scala.SecurityComponents -import play.api.i18n.I18nSupport +import play.api.i18n.{I18nSupport, Messages} import play.api.mvc.{Action, AnyContent, Request} import services.Statuses._ -import services._ +import services.{FileError, _} import viewsapi.Caching.preventCaching import java.util.UUID @@ -20,7 +20,8 @@ class DraftMetadataChecksResultsController @Inject() ( val keycloakConfiguration: KeycloakConfiguration, val consignmentService: ConsignmentService, val applicationConfig: ApplicationConfig, - val consignmentStatusService: ConsignmentStatusService + val consignmentStatusService: ConsignmentStatusService, + val draftMetadataService: DraftMetadataService )(implicit val ec: ExecutionContext) extends TokenSecurity with I18nSupport { @@ -33,24 +34,74 @@ class DraftMetadataChecksResultsController @Inject() ( for { reference <- consignmentService.getConsignmentRef(consignmentId, request.token.bearerAccessToken) consignmentStatuses <- consignmentStatusService.getConsignmentStatuses(consignmentId, token) + errorType <- getErrorType(consignmentStatuses, consignmentId) } yield { - Ok( - views.html.draftmetadata - .draftMetadataChecksResults(consignmentId, reference, getValue(consignmentStatuses, DraftMetadataType), request.token.name) - ) - .uncache() + val resultsPage = { + // leaving original page for no errors + if (errorType == FileError.NONE) { + views.html.draftmetadata + .draftMetadataChecksResults(consignmentId, reference, DraftMetadataProgress("IMPORTED", "blue"), request.token.name) + } else { + if (isErrorReportAvailable(errorType)) { + views.html.draftmetadata + .draftMetadataChecksWithErrorDownload( + consignmentId, + reference, + request.token.name, + actionMessage(errorType), + detailsMessage(errorType) + ) + } else { + views.html.draftmetadata + .draftMetadataChecksErrorsNoDownload( + consignmentId, + reference, + request.token.name, + actionMessage(errorType), + detailsMessage(errorType) + ) + } + } + } + Ok(resultsPage).uncache() } } } - def getValue(statuses: List[GetConsignment.ConsignmentStatuses], statusType: StatusType): DraftMetadataProgress = { - val failed = DraftMetadataProgress("FAILED", "red") - statuses.find(_.statusType == statusType.id).map(_.value).map { - case FailedValue.value => failed - case CompletedWithIssuesValue.value => DraftMetadataProgress("ERRORS", "red") - case CompletedValue.value => DraftMetadataProgress("IMPORTED", "blue") - case _ => failed - } getOrElse failed + private def actionMessage(fileError: FileError.FileError)(implicit messages: Messages): String = { + val key = s"draftMetadata.validation.action.$fileError" + if (Messages.isDefinedAt(key)) + Messages(key) + else + s"Require action message for $key" + } + + private def detailsMessage(fileError: FileError.FileError)(implicit messages: Messages): String = { + val key = s"draftMetadata.validation.details.$fileError" + if (Messages.isDefinedAt(key)) + Messages(key) + else + s"Require details message for $key" + } + + private def isErrorReportAvailable(fileError: FileError.FileError): Boolean = { + fileError match { + case FileError.SCHEMA_VALIDATION => true + case _ => false + } + } + + private def getErrorType(consignmentStatuses: List[GetConsignment.ConsignmentStatuses], consignmentId: UUID): Future[FileError.Value] = { + val draftMetadataStatus = consignmentStatuses.find(_.statusType == DraftMetadataType.id).map(_.value) + if (draftMetadataStatus.isDefined) { + draftMetadataStatus.get match { + case CompletedValue.value => Future.successful(FileError.NONE) + case FailedValue.value => Future.successful(FileError.UNKNOWN) + case _ => draftMetadataService.getErrorTypeFromErrorJson(consignmentId) + } + } else { + Future.successful(FileError.UNKNOWN) + } } } diff --git a/app/services/DownloadService.scala b/app/services/DownloadService.scala new file mode 100644 index 000000000..3d19d37bf --- /dev/null +++ b/app/services/DownloadService.scala @@ -0,0 +1,25 @@ +package services + +import configuration.ApplicationConfig +import software.amazon.awssdk.core.ResponseBytes +import software.amazon.awssdk.core.async.AsyncResponseTransformer +import software.amazon.awssdk.services.s3.S3AsyncClient +import software.amazon.awssdk.services.s3.model.{GetObjectRequest, GetObjectResponse} +import uk.gov.nationalarchives.aws.utils.s3.S3Clients._ + +import javax.inject.Inject +import scala.concurrent.{ExecutionContext, Future} +import scala.jdk.FutureConverters.CompletionStageOps + +class DownloadService @Inject() (val applicationConfig: ApplicationConfig)(implicit val ec: ExecutionContext) { + private val s3Endpoint = applicationConfig.s3Endpoint + + def downloadFile(bucket: String, key: String): Future[ResponseBytes[GetObjectResponse]] = { + downloadFile(bucket, key, s3Async(s3Endpoint)) + } + + def downloadFile(bucket: String, key: String, s3AsyncClient: S3AsyncClient): Future[ResponseBytes[GetObjectResponse]] = { + val getObjectRequest = GetObjectRequest.builder.bucket(bucket).key(key).build() + s3AsyncClient.getObject(getObjectRequest, AsyncResponseTransformer.toBytes[GetObjectResponse]).asScala + } +} diff --git a/app/services/DraftMetadataService.scala b/app/services/DraftMetadataService.scala index 4f3c9fd8d..74d970ff6 100644 --- a/app/services/DraftMetadataService.scala +++ b/app/services/DraftMetadataService.scala @@ -1,13 +1,32 @@ package services import com.google.inject.Inject +import configuration.ApplicationConfig +import io.circe.Decoder +import io.circe.generic.auto._ +import io.circe.parser.decode import play.api.libs.ws.WSClient import play.api.{Configuration, Logging} +import software.amazon.awssdk.core.ResponseBytes +import software.amazon.awssdk.services.s3.model.GetObjectResponse +import uk.gov.nationalarchives.tdr.validation.Metadata +import java.nio.charset.StandardCharsets import java.util.UUID import scala.concurrent.{ExecutionContext, Future} -class DraftMetadataService @Inject() (val wsClient: WSClient, val configuration: Configuration)(implicit val executionContext: ExecutionContext) extends Logging { +object FileError extends Enumeration { + type FileError = Value + val UTF_8, INVALID_CSV, SCHEMA_REQUIRED, SCHEMA_VALIDATION, UNKNOWN, NONE = Value +} + +case class Error(validationProcess: String, property: String, errorKey: String, message: String) +case class ValidationErrors(assetId: String, errors: Set[Error], data: List[Metadata] = List.empty[Metadata]) +case class ErrorFileData(consignmentId: UUID, date: String, fileError: FileError.FileError, validationErrors: List[ValidationErrors]) + +class DraftMetadataService @Inject() (val wsClient: WSClient, val configuration: Configuration, val applicationConfig: ApplicationConfig, val downloadService: DownloadService)( + implicit val executionContext: ExecutionContext +) extends Logging { def triggerDraftMetadataValidator(consignmentId: UUID, uploadFileName: String, token: String): Future[Boolean] = { val url = s"${configuration.get[String]("metadatavalidation.baseUrl")}/draft-metadata/validate/$consignmentId/$uploadFileName" @@ -24,4 +43,17 @@ class DraftMetadataService @Inject() (val wsClient: WSClient, val configuration: } ) } + + def getErrorTypeFromErrorJson(consignmentId: UUID): Future[FileError.FileError] = { + implicit val FileErrorDecoder: Decoder[FileError.Value] = Decoder.decodeEnumeration(FileError) + val errorFile: Future[ResponseBytes[GetObjectResponse]] = + downloadService.downloadFile(applicationConfig.draft_metadata_s3_bucket_name, s"$consignmentId/${applicationConfig.draftMetadataErrorFileName}") + + errorFile + .map(responseBytes => { + val errorJson = new String(responseBytes.asByteArray(), StandardCharsets.UTF_8) + decode[ErrorFileData](errorJson).fold(_ => FileError.UNKNOWN, errorFileData => errorFileData.fileError) + }) + .recoverWith(_ => Future.successful(FileError.UNKNOWN)) + } } diff --git a/app/views/draftmetadata/draftMetadataChecksErrorsNoDownload.scala.html b/app/views/draftmetadata/draftMetadataChecksErrorsNoDownload.scala.html new file mode 100644 index 000000000..c3fdd85b6 --- /dev/null +++ b/app/views/draftmetadata/draftMetadataChecksErrorsNoDownload.scala.html @@ -0,0 +1,21 @@ +@import views.html.partials._ + +@import java.util.UUID +@(consignmentId: UUID, consignmentRef: String, name: String, actionMessage: String, detailsMessage: String)(implicit request: RequestHeader, messages: Messages) + +@main("Results of CSV Checks", name = name) { +
Once you have addressed this issue upload a revised metadata file.
+ + + +The report below contains details about issues found.
+ + + +Once you have addressed this issue upload a revised metadata file.
+ + + +There was an issue with your uploaded metadata file.
+ + + +