All Downloads are FREE. Search and download functionalities are using the official Maven repository.

uk.gov.nationalarchives.dp.client.EntityClient.scala Maven / Gradle / Ivy

The newest version!
package uk.gov.nationalarchives.dp.client

import cats.{MonadError, Parallel}
import cats.effect.Async
import cats.implicits.*
import sttp.capabilities.Streams
import sttp.client3.*
import sttp.model.Method
import uk.gov.nationalarchives.dp.client.Client.*
import uk.gov.nationalarchives.dp.client.DataProcessor.EventAction
import uk.gov.nationalarchives.dp.client.Entities.{Entity, IdentifierResponse}
import uk.gov.nationalarchives.dp.client.EntityClient.EntityType.*
import uk.gov.nationalarchives.dp.client.EntityClient.*

import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.UUID
import scala.xml.{Elem, Node, NodeSeq}
import scala.xml.Utility.escape
import scala.util.Try

/** A client to create, get and update entities in Preservica
  *
  * @tparam F
  *   Type of the effect
  */
trait EntityClient[F[_], S] {

  /** Used to format dates.
    */
  val dateFormatter: DateTimeFormatter

  /** Returns metadata as an XML `Elem` for the provided entity
    *
    * @param entity
    *   The entity to return metadata for
    * @return
    *   An EntityMetadata object containing the metadata (Entity node, Identifiers and metadata) wrapped in the F effect
    */
  def metadataForEntity(entity: Entity): F[EntityMetadata]

  /** Returns a list of [[Client.BitStreamInfo]] representing the bitstreams for the content object reference
    *
    * @param contentRef
    *   The reference of the content object
    * @return
    *   A `Seq` of [[Client.BitStreamInfo]] containing the bitstream details
    */
  def getBitstreamInfo(contentRef: UUID): F[Seq[BitStreamInfo]]

  /** Returns an [[Entities.Entity]] for the given ref and type
    *
    * @param entityRef
    *   The reference of the entity
    * @param entityType
    *   The [[EntityClient.EntityType]] of the entity.
    * @return
    *   An [[Entities.Entity]] wrapped in the F effect
    */
  def getEntity(entityRef: UUID, entityType: EntityType): F[Entity]

  /** Returns a list of [[Entities.IdentifierResponse]] for a given entity
    *
    * @param entity
    *   The entity to find the identifiers for
    * @return
    *   A `Seq` of [[Entities.IdentifierResponse]] wrapped in the F effect
    */
  def getEntityIdentifiers(entity: Entity): F[Seq[IdentifierResponse]]

  /** Returns a String for the given ref and representationType
    *
    * @param ioEntityRef
    *   The reference of the Information Object
    * @param representationType
    *   The [[EntityClient.RepresentationType]] of the entity.
    * @return
    *   A String wrapped in the F effect
    */
  def getUrlsToIoRepresentations(
      ioEntityRef: UUID,
      representationType: Option[RepresentationType]
  ): F[Seq[String]]

  /** Returns a `Seq` of [[Entities.Entity]] for the given ref and representationType
    *
    * @param ioEntityRef
    *   The reference of the Information Object
    * @param representationType
    *   The [[EntityClient.RepresentationType]] of the entity.
    * @param repTypeIndex
    *   The index of the Representation.
    * @return
    *   A `Seq` of [[Entities.Entity]] wrapped in the F effect
    */
  def getContentObjectsFromRepresentation(
      ioEntityRef: UUID,
      representationType: RepresentationType,
      repTypeIndex: Int
  ): F[Seq[Entity]]

  /** Adds an entity to Preservica
    *
    * @param addEntityRequest
    *   An instance of [[EntityClient.AddEntityRequest]] with the details of the entity to add
    * @return
    *   The reference of the new entity wrapped in the F effect.
    */
  def addEntity(addEntityRequest: AddEntityRequest): F[UUID]

  /** Updates an entity in Preservice
    *
    * @param updateEntityRequest
    *   An instance of [[EntityClient.UpdateEntityRequest]] with the details of the entity to update
    * @return
    *   The string `"Entity was updated"`
    */
  def updateEntity(updateEntityRequest: UpdateEntityRequest): F[String]

  /** Updates identifiers for an entity
    *
    * @param entity
    *   The entity to update
    * @param identifiers
    *   A list of identifiers to update on the entity
    * @return
    *   The original identifiers argument.
    */
  def updateEntityIdentifiers(entity: Entity, identifiers: Seq[IdentifierResponse]): F[Seq[IdentifierResponse]]

  /** Streams the bitstream from the provided url into `streamFn`
    *
    * @param stream
    *   An instance of the sttp Stream type
    * @param url
    *   The url to stream the data from
    * @param streamFn
    *   The function to stream the data to
    * @tparam T
    *   The return type of the stream function, wrapped in the F effect
    * @return
    *   The return type of the stream function, wrapped in the F effect
    */
  def streamBitstreamContent[T](
      stream: Streams[S]
  )(url: String, streamFn: stream.BinaryStream => F[T]): F[T]

  /** Returns any entity updated since the provided dateTime
    *
    * @param dateTime
    *   The date and time to pass to the API
    * @param startEntry
    *   The entry to start from. Used for pagination
    * @param maxEntries
    *   The maximum number of entries to return. Defaults to 1000
    * @return
    *   A `Seq` of [[Entities.Entity]] wrapped in the F effect
    */
  def entitiesUpdatedSince(
      dateTime: ZonedDateTime,
      startEntry: Int,
      maxEntries: Int = 1000
  ): F[Seq[Entity]]

  /** Returns a list of event actions for an entity
    *
    * @param entity
    *   The entity to return the actions for
    * @param startEntry
    *   The entry to start from. Used for pagination
    * @param maxEntries
    *   The maximum number of entries to return. Defaults to 1000
    * @return
    *   A `Seq` of [[DataProcessor.EventAction]]
    */
  def entityEventActions(
      entity: Entity,
      startEntry: Int = 0,
      maxEntries: Int = 1000
  ): F[Seq[EventAction]]

  /** Find entities per identifier
    *
    * @param identifiers
    *   A Seq of identifiers
    * @return
    *   A Map of Identifier -> Seq of Entities
    */
  def entitiesPerIdentifier(identifiers: Seq[Identifier]): F[Map[Identifier, Seq[Entity]]]

  /** Adds an identifier for an entity
    *
    * @param entityRef
    *   The reference of the entity
    * @param entityType
    *   The type of the entity
    * @param identifier
    *   The identifier to add to the entity
    * @return
    *   The string `"The Identifier was added"`
    */
  def addIdentifierForEntity(
      entityRef: UUID,
      entityType: EntityType,
      identifier: Identifier
  ): F[String]

  /** Gets the version of Preservica in the namespace
    *
    * @param endpoint
    *   The Entity endpoint to be called (this should exclude the baseUrl and path)
    * @return
    *   The version of Preservica in the namespace as a Float
    */

  def getPreservicaNamespaceVersion(endpoint: String): F[Float]
}

/** An object containing a method which returns an implementation of the EntityClient trait
  */
object EntityClient {

  /** Creates a new `EntityClient` instance.
    *
    * @param clientConfig
    *   Configuration parameters needed to create the client
    * @param me
    *   An implicit instance of cats.MonadError
    * @param sync
    *   An implicit instance of cats.Sync
    * @tparam F
    *   The type of the effect
    * @tparam S
    *   The type of the Stream to be used for the streaming methods.
    * @return
    */
  def createEntityClient[F[_]: Async: Parallel, S](clientConfig: ClientConfig[F, S])(using
      me: MonadError[F, Throwable]
  ): EntityClient[F, S] = new EntityClient[F, S] {
    val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
    private val apiBaseUrl: String = clientConfig.apiBaseUrl
    private val apiVersion = 7.0f
    private val apiUrl = s"$apiBaseUrl/api/entity/v$apiVersion"
    private val namespaceUrl = s"http://preservica.com/XIP/v$apiVersion"

    private val missingPathExceptionMessage: UUID => String = ref =>
      s"No path found for entity id $ref. Could this entity have been deleted?"

    private val client: Client[F, S] = Client(clientConfig)

    import client.*

    override def getEntity(entityRef: UUID, entityType: EntityType): F[Entity] =
      for {
        token <- getAuthenticationToken
        entity <- getEntityWithRef(entityRef, entityType, token)
      } yield entity

    override def getUrlsToIoRepresentations(
        entityRef: UUID,
        representationType: Option[RepresentationType]
    ): F[Seq[String]] =
      for {
        token <- getAuthenticationToken
        urlsOfReps <- urlsToIoRepresentations(entityRef, representationType, token)
      } yield urlsOfReps

    override def getContentObjectsFromRepresentation(
        ioEntityRef: UUID,
        representationType: RepresentationType,
        repTypeIndex: Int
    ): F[Seq[Entity]] =
      for {
        token <- getAuthenticationToken
        representationsResponse <- ioRepresentations(ioEntityRef, representationType, repTypeIndex, token)
        contentObjects <- dataProcessor.getContentObjectsFromRepresentation(
          representationsResponse,
          representationType,
          ioEntityRef
        )
      } yield contentObjects

    override def getEntityIdentifiers(entity: Entity): F[Seq[IdentifierResponse]] = {
      for {
        path <- me.fromOption(
          entity.path,
          PreservicaClientException(missingPathExceptionMessage(entity.ref))
        )
        url = uri"$apiUrl/$path/${entity.ref}/identifiers"
        token <- getAuthenticationToken
        identifiers <- entityIdentifiers(url.toString.some, token, Nil)
      } yield identifiers
    }

    override def addEntity(addEntityRequest: AddEntityRequest): F[UUID] = {
      val path = addEntityRequest.entityType.entityPath
      for {
        _ <-
          if (path == ContentObject.entityPath)
            me.raiseError(
              PreservicaClientException("You currently cannot create a content object via the API.")
            )
          else me.unit

        nodeNameAndToken <- validateEntityUpdateInputs(
          addEntityRequest.entityType,
          addEntityRequest.parentRef
        )
        (nodeName, token) = nodeNameAndToken
        addXipTag = path == InformationObject.entityPath
        addRequestBody = createUpdateRequestBody(
          addEntityRequest.ref,
          addEntityRequest.title,
          addEntityRequest.description,
          addEntityRequest.parentRef,
          addEntityRequest.securityTag,
          nodeName,
          addXipTag
        )
        // "Representations" can be appended to an 'information-objects' request; for now, we'll exclude it and instead, just close the tag
        fullRequestBody = if (addXipTag) addRequestBody + "\n            " else addRequestBody
        url = uri"$apiUrl/$path"
        addEntityResponse <- sendXMLApiRequest(url.toString, token, Method.POST, Some(fullRequestBody))
        ref <- dataProcessor.childNodeFromEntity(addEntityResponse, nodeName, "Ref")
      } yield UUID.fromString(ref.trim)
    }

    override def updateEntity(updateEntityRequest: UpdateEntityRequest): F[String] = {
      for {
        nodeNameAndToken <- validateEntityUpdateInputs(
          updateEntityRequest.entityType,
          updateEntityRequest.parentRef
        )

        (nodeName, token) = nodeNameAndToken
        updateRequestBody = createUpdateRequestBody(
          Some(updateEntityRequest.ref),
          updateEntityRequest.title,
          updateEntityRequest.descriptionToChange,
          updateEntityRequest.parentRef,
          updateEntityRequest.securityTag,
          nodeName
        )
        path = updateEntityRequest.entityType.entityPath
        url = uri"$apiUrl/$path/${updateEntityRequest.ref}"
        _ <- sendXMLApiRequest(url.toString, token, Method.PUT, Some(updateRequestBody))
        response = "Entity was updated"
      } yield response
    }

    override def getBitstreamInfo(
        contentObjectRef: UUID
    ): F[Seq[BitStreamInfo]] =
      for {
        token <- getAuthenticationToken
        contentObjectElement <- sendXMLApiRequest(
          s"$apiUrl/${ContentObject.entityPath}/$contentObjectRef",
          token,
          Method.GET
        )

        generationsEndpointUrl <- dataProcessor.generationUrlFromEntity(contentObjectElement)
        allGenerationElements <- generationElements(generationsEndpointUrl, contentObjectRef, token)
        allBitstreamInfo <- allGenerationElements.map { generationElement =>
          for {
            generationType <- dataProcessor.generationType(generationElement, contentObjectRef)
            bitstreamElements <- bitstreamElements(generationElement, token)
            contentObject <- dataProcessor.getEntity(contentObjectRef, contentObjectElement, ContentObject)
            allBitstreamInfo <- dataProcessor.allBitstreamInfo(bitstreamElements, generationType, contentObject)
          } yield allBitstreamInfo
        }.flatSequence

      } yield allBitstreamInfo

    override def metadataForEntity(entity: Entity): F[EntityMetadata] =
      for {
        token <- getAuthenticationToken
        queryParams = Map("max" -> 1000, "start" -> 0)
        path <- me.fromOption(
          entity.path,
          PreservicaClientException(missingPathExceptionMessage(entity.ref))
        )
        entityUrl = s"$apiUrl/$path/${entity.ref}"
        entityType <- getEntityType(entity)

        entityInfo <- sendXMLApiRequest(entityUrl, token, Method.GET)
        entityNode <- dataProcessor.getEntityXml(entity.ref, entityInfo, entityType)

        identifiers <- entityIdentifiersXml(Some(s"$entityUrl/identifiers"), token, Nil)

        entityLinks <- entityLinksXml(entity.ref, uri"$entityUrl/links?$queryParams".toString.some, token, Nil)

        fragmentUrls <- dataProcessor.fragmentUrls(entityInfo)
        fragmentResponses <- fragmentUrls.map(url => sendXMLApiRequest(url, token, Method.GET)).sequence
        fragments <- dataProcessor.fragments(fragmentResponses)
        fragmentsWithMetadataLabel = fragments.map { node =>
          new Elem(node.prefix, "Metadata", node.attributes, node.scope, false, node.child*)
        }

        eventActionResponseXmls <- eventActionsXml(uri"$entityUrl/event-actions?$queryParams".toString.some, token, Nil)
        eventActions <- eventActionResponseXmls.map(dataProcessor.getEventActionElements).flatSequence
        entityMetadata <-
          if (entityType.entityTypeShort == "CO")
            for {
              generationsEndpointUrl <- me.pure(s"$entityUrl/generations")
              allGenerationsResponseElements <- generationElements(generationsEndpointUrl, entity.ref, token)
              allGenerationElements <- allGenerationsResponseElements
                .map(dataProcessor.getGenerationElement)
                .flatSequence
              bitstreamElements <- allGenerationsResponseElements.map(bitstreamElements(_, token)).flatSequence
            } yield CoMetadata(
              entityNode,
              allGenerationElements,
              bitstreamElements.flatMap(_ \ "Bitstream"),
              identifiers,
              entityLinks,
              fragmentsWithMetadataLabel,
              eventActions
            )
          else if (entityType.entityTypeShort == "IO")
            for {
              urlsToIoReps <- urlsToIoRepresentations(entity.ref, None, token)
              representations <- urlsToIoReps.map { urlToIoRepresentation =>
                val urlSplitOnForwardSlash = urlToIoRepresentation.split('/').reverse
                val generationVersion = urlSplitOnForwardSlash.head.toInt
                val representationType = RepresentationType.valueOf(urlSplitOnForwardSlash(1))

                ioRepresentations(entity.ref, representationType, generationVersion, token).flatMap {
                  dataProcessor.getRepresentationElement
                }
              }.flatSequence
            } yield IoMetadata(
              entityNode,
              representations,
              identifiers,
              entityLinks,
              fragmentsWithMetadataLabel,
              eventActions
            )
          else
            me.pure(
              StandardEntityMetadata(entityNode, identifiers, entityLinks, fragmentsWithMetadataLabel, eventActions)
            )
      } yield entityMetadata

    override def entitiesUpdatedSince(
        dateTime: ZonedDateTime,
        startEntry: Int,
        maxEntries: Int = 1000
    ): F[Seq[Entity]] = {
      val dateString = dateTime.format(dateFormatter)
      val queryParams = Map("date" -> dateString, "max" -> maxEntries, "start" -> startEntry)
      val url = uri"$apiUrl/entities/updated-since?$queryParams"
      for {
        token <- getAuthenticationToken
        updatedEntities <- getEntities(url.toString, token)
      } yield updatedEntities
    }

    override def entityEventActions(
        entity: Entity,
        startEntry: Int = 0,
        maxEntries: Int = 1000
    ): F[Seq[EventAction]] = {
      val queryParams = Map("max" -> maxEntries, "start" -> startEntry)
      for {
        path <- me.fromOption(
          entity.path,
          PreservicaClientException(missingPathExceptionMessage(entity.ref))
        )
        url = uri"$apiUrl/$path/${entity.ref}/event-actions?$queryParams"
        token <- getAuthenticationToken
        allEventActionsResponseXml <- eventActionsXml(url.toString.some, token, Nil)
        eventActions <- allEventActionsResponseXml
          .map(eventActionsResponseXml => dataProcessor.getEventActions(eventActionsResponseXml))
          .flatSequence
      } yield eventActions.reverse // most recent event first
    }

    override def entitiesPerIdentifier(identifiers: Seq[Identifier]): F[Map[Identifier, Seq[Entity]]] =
      for {
        token <- getAuthenticationToken
        entities <- identifiers.distinct.parTraverse(identifier => entitiesForIdentifier(identifier, token))
      } yield entities.toMap

    override def addIdentifierForEntity(
        entityRef: UUID,
        entityType: EntityType,
        identifier: Identifier
    ): F[String] =
      for {
        token <- getAuthenticationToken
        _ <- sendXMLApiRequest(
          s"$apiUrl/${entityType.entityPath}/$entityRef/identifiers",
          token,
          Method.POST,
          Some(requestBodyForIdentifier(identifier.identifierName, identifier.value))
        )
        response = s"The Identifier was added"
      } yield response

    def streamBitstreamContent[T](
        stream: Streams[S]
    )(url: String, streamFn: stream.BinaryStream => F[T]): F[T] = {
      val apiUri = uri"$url"

      def request(token: String) = basicRequest
        .get(apiUri)
        .headers(Map("Preservica-Access-Token" -> token))
        .response(asStream(stream)(streamFn))

      for {
        token <- getAuthenticationToken
        res <- backend.send(request(token))
        body <- me.fromEither {
          res.body.left.map(err => PreservicaClientException(Method.GET, apiUri, res.code, err))
        }
      } yield body
    }

    override def updateEntityIdentifiers(
        entity: Entity,
        identifiers: Seq[IdentifierResponse]
    ): F[Seq[IdentifierResponse]] =
      for {
        token <- getAuthenticationToken
        updateResponse <- identifiers.map { identifier =>
          val requestBody = requestBodyForIdentifier(identifier.identifierName, identifier.value).some
          for {
            path <- me.fromOption(
              entity.path,
              PreservicaClientException(missingPathExceptionMessage(entity.ref))
            )
            url = uri"$apiUrl/$path/${entity.ref}/identifiers/${identifier.id}"
            _ <- sendXMLApiRequest(url.toString, token, Method.PUT, requestBody)
          } yield identifier
        }.sequence
      } yield updateResponse

    override def getPreservicaNamespaceVersion(endpoint: String): F[Float] = {
      for {
        token <- getAuthenticationToken
        resXml <- sendXMLApiRequest(s"$apiBaseUrl/api/entity/$endpoint", token, Method.GET)
        version <- dataProcessor.getPreservicaNamespaceVersion(resXml)
      } yield version
    }

    private def entitiesForIdentifier(
        identifier: Identifier,
        token: String
    ): F[(Identifier, Seq[Entity])] = {
      val queryParams = Map("type" -> identifier.identifierName, "value" -> identifier.value)
      val url = uri"$apiUrl/entities/by-identifier?$queryParams"
      for {
        entitiesWithIdentifier <- getEntities(url.toString, token)
        entities <- entitiesWithIdentifier.map { entityWithId =>
          for {
            entityType <- getEntityType(entityWithId)
            entity <- getEntityWithRef(
              entityWithId.ref,
              entityType,
              token
            ) // This is necessary to get the full entity information as by-identifier doesn't return it
          } yield entity
        }.sequence
      } yield identifier -> entities
    }

    private def getEntityWithRef(entityRef: UUID, entityType: EntityType, token: String) =
      for {
        url <- me.pure(uri"$apiUrl/${entityType.entityPath}/$entityRef")
        entityResponse <- sendXMLApiRequest(url.toString(), token, Method.GET)
        entity <- dataProcessor.getEntity(entityRef, entityResponse, entityType)
      } yield entity

    private def urlsToIoRepresentations(
        entityRef: UUID,
        representationType: Option[RepresentationType],
        token: String
    ) =
      for {
        url <- me.pure(uri"$apiUrl/information-objects/$entityRef/representations")
        representationsResponse <- sendXMLApiRequest(url.toString(), token, Method.GET)
        urlsOfRepresentations <- dataProcessor.getUrlsToEntityRepresentations(
          representationsResponse,
          representationType
        )
      } yield urlsOfRepresentations

    private def getEntityType(entity: Entity): F[EntityType] =
      me.fromOption(
        entity.entityType,
        PreservicaClientException(s"No entity type found for entity ${entity.ref}")
      )

    private def getEntities(url: String, token: String): F[Seq[Entity]] =
      for {
        entitiesResponseXml <- sendXMLApiRequest(url, token, Method.GET)
        entitiesWithUpdates <- dataProcessor.getEntities(entitiesResponseXml)
      } yield entitiesWithUpdates

    private def eventActionsXml(
        url: Option[String],
        token: String,
        currentCollectionOfEventActionsXml: Seq[Elem]
    ): F[Seq[Elem]] = {
      if (url.isEmpty) {
        me.pure(currentCollectionOfEventActionsXml)
      } else {
        for {
          eventActionsResponseXml <- sendXMLApiRequest(url.get, token, Method.GET)
          nextPageUrl <- dataProcessor.nextPage(eventActionsResponseXml)
          allEventActionsXml <- eventActionsXml(
            nextPageUrl,
            token,
            currentCollectionOfEventActionsXml :+ eventActionsResponseXml
          )
        } yield allEventActionsXml
      }
    }

    private def requestBodyForIdentifier(identifierName: String, identifierValue: String): String = {
      val identifierAsXml =
        
          
            {identifierName}
          
          
            {identifierValue}
          
        

      s"""\n$identifierAsXml"""
    }

    private def ioRepresentations(
        ioEntityRef: UUID,
        representationType: RepresentationType,
        repTypeIndex: Int,
        token: String
    ): F[Elem] =
      for {
        url <- me.pure(uri"$apiUrl/information-objects/$ioEntityRef/representations/$representationType/$repTypeIndex")
        representationsResponse <- sendXMLApiRequest(url.toString(), token, Method.GET)
      } yield representationsResponse

    private def entityIdentifiers(
        url: Option[String],
        token: String,
        currentCollectionOfIdentifiers: Seq[IdentifierResponse]
    ): F[Seq[IdentifierResponse]] = {
      if (url.isEmpty) {
        me.pure(currentCollectionOfIdentifiers)
      } else {
        for {
          identifiersResponseXml <- sendXMLApiRequest(url.get, token, Method.GET)
          identifiersBatch <- dataProcessor.getIdentifiers(identifiersResponseXml)
          nextPageUrl <- dataProcessor.nextPage(identifiersResponseXml)
          allIdentifiers <- entityIdentifiers(
            nextPageUrl,
            token,
            currentCollectionOfIdentifiers ++ identifiersBatch
          )
        } yield allIdentifiers
      }
    }

    private def validateEntityUpdateInputs(entityType: EntityType, parentRef: Option[UUID]): F[(String, String)] =
      for {
        _ <-
          if (entityType.entityPath != StructuralObject.entityPath && parentRef.isEmpty)
            me.raiseError(
              PreservicaClientException(
                "You must pass in the parent ref if you would like to add/update a non-structural object."
              )
            )
          else me.unit
        token <- getAuthenticationToken
      } yield (entityType.toString, token)

    private def createUpdateRequestBody(
        ref: Option[UUID],
        title: String,
        descriptionToChange: Option[String],
        parentRef: Option[UUID],
        securityTag: SecurityTag,
        nodeName: String,
        addOpeningXipTag: Boolean = false
    ): String = {
      s"""
            ${if (addOpeningXipTag) s"""""" else ""}
            <$nodeName xmlns="http://preservica.com/XIP/v$apiVersion">
              ${ref.map(r => s"$r").getOrElse("")}
              ${escape(title)}
              ${descriptionToChange
          .map(description => s"${escape(description)}")
          .getOrElse("")}
              $securityTag
              ${parentRef.map(parent => s"$parent").getOrElse("")}
            """
    }

    private def generationElements(
        generationsEndpointUrl: String,
        contentObjectRef: UUID,
        token: String
    ): F[Seq[Elem]] =
      for {
        generationsElement <- sendXMLApiRequest(generationsEndpointUrl, token, Method.GET)
        allGenerationUrls <- dataProcessor.allGenerationUrls(generationsElement, contentObjectRef)

        allGenerationElements <- allGenerationUrls.map { url =>
          sendXMLApiRequest(url, token, Method.GET)
        }.sequence
      } yield allGenerationElements

    private def bitstreamElements(generationResponseElement: Elem, token: String) =
      for {
        allBitstreamUrls <- dataProcessor.allBitstreamUrls(generationResponseElement)
        bitstreamElements <- allBitstreamUrls.map { url =>
          sendXMLApiRequest(url, token, Method.GET)
        }.sequence
      } yield bitstreamElements

    private def entityIdentifiersXml(
        url: Option[String],
        token: String,
        currentCollectionOfIdentifiers: Seq[Node]
    ): F[Seq[Node]] =
      if (url.isEmpty)
        me.pure(currentCollectionOfIdentifiers)
      else
        for {
          identifiersResponseXml <- sendXMLApiRequest(url.get, token, Method.GET)
          identifiersBatch <- dataProcessor.getIdentifiersXml(identifiersResponseXml)
          nextPageUrl <- dataProcessor.nextPage(identifiersResponseXml)
          allIdentifiers <- entityIdentifiersXml(
            nextPageUrl,
            token,
            currentCollectionOfIdentifiers ++ identifiersBatch
          )
        } yield allIdentifiers

    private def entityLinksXml(
        ref: UUID,
        url: Option[String],
        token: String,
        currentCollectionOfEntityLinks: Seq[Node]
    ): F[Seq[Node]] =
      if (url.isEmpty) me.pure(currentCollectionOfEntityLinks)
      else
        for {
          entityLinksResponseXml <- sendXMLApiRequest(url.get, token, Method.GET)
          entityLinksXmlBatch <- dataProcessor.getEntityLinksXml(ref, entityLinksResponseXml)
          nextPageUrl <- dataProcessor.nextPage(entityLinksResponseXml)
          allEntityLinksXml <- entityLinksXml(
            ref,
            nextPageUrl,
            token,
            currentCollectionOfEntityLinks ++ entityLinksXmlBatch
          )
        } yield allEntityLinksXml
  }

  sealed trait EntityMetadata:
    val entityNode: Node
    val identifiers: Seq[Node]
    val links: Seq[Node]
    val metadataNodes: Seq[Node]
    val eventActions: Seq[Node]

  /** Represents a Preservica security tag
    */
  enum SecurityTag:
    override def toString: String = this match
      case Open   => "open"
      case Closed => "closed"

    case Open, Closed

  /** Represents an entity type
    */
  enum EntityType(val entityPath: String, val entityTypeShort: String):
    case StructuralObject extends EntityType("structural-objects", "SO")
    case InformationObject extends EntityType("information-objects", "IO")
    case ContentObject extends EntityType("content-objects", "CO")

  /** Represents a Preservica identifier
    */
  case class Identifier(identifierName: String, value: String)

  /** Represents a Preservica representation tag
    */

  enum RepresentationType:
    case Access, Preservation

  /** Represents an entity to add to Preservica
    *
    * @param ref
    *   An optional ref. If one is not provided, one will be generated
    * @param title
    *   The title of the new entity
    * @param description
    *   The optional description of the new entity
    * @param entityType
    *   The type of the new entity
    * @param securityTag
    *   The security tag of the new entity
    * @param parentRef
    *   An optional parent reference
    */
  case class AddEntityRequest(
      ref: Option[UUID],
      title: String,
      description: Option[String],
      entityType: EntityType,
      securityTag: SecurityTag,
      parentRef: Option[UUID]
  )

  /** Represents an entity to update in Preservica
    *
    * @param ref
    *   The ref of the entity to be updated
    * @param title
    *   The title of the updated entity
    * @param descriptionToChange
    *   The optional description of the updated entity
    * @param entityType
    *   The type of the updated entity
    * @param securityTag
    *   The security tag of the updated entity
    * @param parentRef
    *   An optional parent reference
    */
  case class UpdateEntityRequest(
      ref: UUID,
      title: String,
      descriptionToChange: Option[String],
      entityType: EntityType,
      securityTag: SecurityTag,
      parentRef: Option[UUID]
  )

  enum GenerationType:
    case Original, Derived

  /* The non-specific (generic) metadata that is common for all Entity types; default for current/future Entities
  that don't have an EntityMetadata implementation. */
  case class StandardEntityMetadata(
      entityNode: Node,
      identifiers: Seq[Node],
      links: Seq[Node],
      metadataNodes: Seq[Node],
      eventActions: Seq[Node]
  ) extends EntityMetadata

  case class IoMetadata(
      entityNode: Node,
      representations: Seq[Node],
      identifiers: Seq[Node],
      links: Seq[Node],
      metadataNodes: Seq[Node],
      eventActions: Seq[Node]
  ) extends EntityMetadata

  case class CoMetadata(
      entityNode: Node,
      generationNodes: Seq[Node],
      bitstreamNodes: Seq[Node],
      identifiers: Seq[Node],
      links: Seq[Node],
      metadataNodes: Seq[Node],
      eventActions: Seq[Node]
  ) extends EntityMetadata

  object SecurityTag:
    def fromString(securityTagString: String): Option[SecurityTag] = Try(
      SecurityTag.valueOf(securityTagString.capitalize)
    ).toOption
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy