usage/app/controllers/UsageApi.scala (299 lines of code) (raw):

package controllers import java.net.URI import com.gu.contentapi.client.model.ItemQuery import com.gu.mediaservice.lib.argo.ArgoHelpers import com.gu.mediaservice.lib.argo.model.{EntityResponse, Link, Action => ArgoAction} import com.gu.mediaservice.lib.auth.{Authentication, Authorisation} import com.gu.mediaservice.lib.aws.UpdateMessage import com.gu.mediaservice.lib.logging.{LogMarker, MarkerMap} import com.gu.mediaservice.lib.play.RequestLoggingFilter import com.gu.mediaservice.lib.usage.UsageBuilder import com.gu.mediaservice.model.usage.{MediaUsage, SyndicatedUsageStatus, Usage, UsageNotice, UsageStatus} import com.gu.mediaservice.syntax.MessageSubjects import lib._ import model._ import play.api.libs.json.{JsArray, JsError, JsValue, Json} import play.api.mvc._ import play.utils.UriEncoding import rx.lang.scala.Subject import scala.concurrent.{ExecutionContext, Future} import scala.util.Try class UsageApi( auth: Authentication, authorisation: Authorisation, usageTable: UsageTable, usageGroupOps: UsageGroupOps, notifications: Notifications, config: UsageConfig, usageApiSubject: Subject[WithLogMarker[UsageGroup]], liveContentApi: LiveContentApi, override val controllerComponents: ControllerComponents, playBodyParsers: PlayBodyParsers )( implicit val ec: ExecutionContext ) extends BaseController with MessageSubjects with ArgoHelpers { private val AuthenticatedAndAuthorisedToDelete = auth andThen authorisation.CommonActionFilters.authorisedForDeleteCropsOrUsages private def wrapUsage(usage: Usage): EntityResponse[Usage] = { EntityResponse( uri = usageUri(usage.id), data = usage ) } private def usageUri(usageId: String): Option[URI] = { val encodedUsageId = UriEncoding.encodePathSegment(usageId, "UTF-8") Try { URI.create(s"${config.usageUri}/usages/$encodedUsageId") }.toOption } val indexResponse = { val indexData = Map("description" -> "This is the Usage Recording service") val indexLinks = List( Link("usages-by-media", s"${config.usageUri}/usages/media/{id}"), Link("usages-by-id", s"${config.usageUri}/usages/{id}") ) val printPostUri = URI.create(s"${config.usageUri}/usages/print") val actions = List( ArgoAction("print-usage", printPostUri, "POST") ) respond(indexData, indexLinks, actions) } def index = auth { indexResponse } def forUsage(usageId: String) = auth.async { req => implicit val logMarker: LogMarker = MarkerMap( "requestType" -> "get-usage", "requestId" -> RequestLoggingFilter.getRequestId(req), "usageId" -> usageId, ) logger.info(logMarker, s"Request for single usage $usageId") val usageFuture = usageTable.queryByUsageId(usageId) usageFuture.map[play.api.mvc.Result]((mediaUsageOption: Option[MediaUsage]) => { mediaUsageOption.foldLeft( respondNotFound("No usages found.") )((_, mediaUsage: MediaUsage) => { val usage = UsageBuilder.build(mediaUsage) val mediaId = mediaUsage.mediaId val uri = usageUri(usage.id) val links = List( Link("media", s"${config.services.apiBaseUri}/images/$mediaId"), Link("media-usage", s"${config.services.usageBaseUri}/usages/media/$mediaId") ) respond[Usage](data = usage, uri = uri, links = links) }) }).recover { case error: Exception => logger.error(logMarker, "UsageApi returned an error.", error) respondError(InternalServerError, "usage-retrieve-failed", error.getMessage) } } def reindexForContent(contentId: String) = auth.async { req => implicit val logMarker: LogMarker = MarkerMap( "requestType" -> "reindex-for-content", "requestId" -> RequestLoggingFilter.getRequestId(req), "contentId" -> contentId, ) val query = liveContentApi.usageQuery(contentId) liveContentApi.getResponse(query).map{response => response.content match { case Some(content) => ContentHelpers .getContentFirstPublished(content) .map(LiveContentItem(content, _)) .map(_.copy(isReindex = true)) .foreach(_.emitAsUsageGroup( usageApiSubject, usageGroupOps )) Accepted case _ => NotFound } }.recover { case error: Exception => logger.error(logMarker, s"UsageApi reindex for content ($contentId) failed!", error) InternalServerError } } def forMedia(mediaId: String) = auth.async { req => implicit val logMarker: LogMarker = MarkerMap( "requestType" -> "usages-for-media-id", "requestId" -> RequestLoggingFilter.getRequestId(req), "image-id" -> mediaId, ) val usagesFuture = usageTable.queryByImageId(mediaId) usagesFuture.map[play.api.mvc.Result]((mediaUsages: List[MediaUsage]) => { val usages = mediaUsages.map(UsageBuilder.build) usages match { case Nil => respondNotFound("No usages found.") case _ => val uri = Try { URI.create(s"${config.services.usageBaseUri}/usages/media/$mediaId") }.toOption val links = List( Link("media", s"${config.services.apiBaseUri}/images/$mediaId") ) respondCollection[EntityResponse[Usage]]( uri = uri, links = links, data = usages.map(wrapUsage) ) } }).recover { case error: BadInputException => logger.error(logMarker, "UsageApi returned an error.", error) respondError(BadRequest, "image-usage-retrieve-failed", error.getMessage) case error: Exception => logger.error(logMarker, "UsageApi returned an error.", error) respondError(InternalServerError, "image-usage-retrieve-failed", error.getMessage) } } val maxPrintRequestLength: Int = 1024 * config.maxPrintRequestLengthInKb val setPrintRequestBodyParser: BodyParser[JsValue] = playBodyParsers.json(maxLength = maxPrintRequestLength) def setPrintUsages = auth(setPrintRequestBodyParser) { req => { val printUsageRequestResult = req.body.validate[PrintUsageRequest] printUsageRequestResult.fold( e => { respondError(BadRequest, "print-usage-request-parse-failed", JsError.toJson(e).toString) }, printUsageRequest => { implicit val logMarker: LogMarker = MarkerMap( "requestType" -> "set-print-usages", "requestId" -> RequestLoggingFilter.getRequestId(req), ) val usageGroups = usageGroupOps.build(printUsageRequest.printUsageRecords) usageGroups.map(WithLogMarker.includeUsageGroup).foreach(usageApiSubject.onNext) Accepted } ) }} def setSyndicationUsages() = auth(parse.json) { req => { val syndicationUsageRequest = (req.body \ "data").validate[SyndicationUsageRequest] syndicationUsageRequest.fold( e => respondError( BadRequest, errorKey = "syndication-usage-parse-failed", errorMessage = JsError.toJson(e).toString ), sur => { implicit val logMarker: LogMarker = MarkerMap( "requestType" -> "set-syndication-usages", "requestId" -> RequestLoggingFilter.getRequestId(req), "image-id" -> sur.mediaId, ) ++ apiKeyMarkers(req.user.accessor) logger.info(logMarker, "recording syndication usage") val group = usageGroupOps.build(sur) usageApiSubject.onNext(WithLogMarker.includeUsageGroup(group)) Accepted } ) }} def setFrontUsages() = auth(parse.json) { req => { val request = (req.body \ "data").validate[FrontUsageRequest] request.fold( e => respondError( BadRequest, errorKey = "front-usage-parse-failed", errorMessage = JsError.toJson(e).toString ), fur => { implicit val logMarker: LogMarker = MarkerMap( "requestType" -> "set-front-usages", "requestId" -> RequestLoggingFilter.getRequestId(req), "image-id" -> fur.mediaId, ) ++ apiKeyMarkers(req.user.accessor) logger.info(logMarker, "recording front usage") val group = usageGroupOps.build(fur) usageApiSubject.onNext(WithLogMarker.includeUsageGroup(group)) Accepted } ) }} def setDownloadUsages() = auth(parse.json) { req => { val request = (req.body \ "data").validate[DownloadUsageRequest] request.fold( e => respondError( BadRequest, errorKey = "download-usage-parse-failed", errorMessage = JsError.toJson(e).toString ), usageRequest => { implicit val logMarker: LogMarker = MarkerMap( "requestType" -> "set-download-usages", "requestId" -> RequestLoggingFilter.getRequestId(req), "image-id" -> usageRequest.mediaId, ) ++ apiKeyMarkers(req.user.accessor) logger.info(logMarker, "recording download usage") val group = usageGroupOps.build(usageRequest) usageApiSubject.onNext(WithLogMarker.includeUsageGroup(group)) Accepted } ) }} def updateUsageStatus(mediaId: String, usageId: String) = auth.async(parse.json) {req => { val request = (req.body \ "data").validate[UsageStatus] request.fold( e => Future.successful( respondError( BadRequest, errorKey = "update-image-usage-status-failed", errorMessage = JsError.toJson(e).toString() ) ), usageStatus => { implicit val logMarker: LogMarker = MarkerMap( "requestType" -> "update-usage-status", "requestId" -> RequestLoggingFilter.getRequestId(req), "usageStatus" -> usageStatus.toString, "image-id" -> mediaId, "usage-id" -> usageId, ) ++ apiKeyMarkers(req.user.accessor) logger.info(logMarker, "recording usage status update") usageTable.queryByUsageId(usageId).map { case Some(mediaUsage) => val updatedStatusMediaUsage = mediaUsage.copy(status = usageStatus) usageTable.update(updatedStatusMediaUsage) val usageNotice = UsageNotice(mediaId, JsArray(Seq(Json.toJson(UsageBuilder.build(updatedStatusMediaUsage))))) val updateMessage = UpdateMessage( subject = UpdateUsageStatus, id = Some(mediaId), usageNotice = Some(usageNotice) ) notifications.publish(updateMessage) Ok case None => NotFound } } ) }} def deleteSingleUsage(mediaId: String, usageId: String) = AuthenticatedAndAuthorisedToDelete.async { req => implicit val logMarker: LogMarker = MarkerMap( "requestType" -> "delete-usage", "requestId" -> RequestLoggingFilter.getRequestId(req), "image-id" -> mediaId, "usage-id" -> usageId, ) usageTable.queryByUsageId(usageId).map { case Some(mediaUsage) => usageTable.deleteRecord(mediaUsage) val updateMessage = UpdateMessage(subject = DeleteSingleUsage, id = Some(mediaId), usageId = Some(usageId)) notifications.publish(updateMessage) Ok case None => NotFound } } def deleteUsages(mediaId: String) = AuthenticatedAndAuthorisedToDelete.async { req => implicit val logMarker: LogMarker = MarkerMap( "requestType" -> "delete-usages", "requestId" -> RequestLoggingFilter.getRequestId(req), "image-id" -> mediaId, ) usageTable.queryByImageId(mediaId).map(usages => { usages.foreach(usageTable.deleteRecord) }).recover { case error: BadInputException => logger.warn(logMarker, "UsageApi returned an error.", error) respondError(BadRequest, "image-usage-delete-failed", error.getMessage) case error: Exception => logger.error(logMarker, "UsageApi returned an error.", error) respondError(InternalServerError, "image-usage-delete-failed", error.getMessage) } val updateMessage = UpdateMessage(subject = DeleteUsages, id = Some(mediaId)) notifications.publish(updateMessage) Future.successful(Ok) } }