diff --git a/dsp-shared/src/main/scala/dsp/errors/Errors.scala b/dsp-shared/src/main/scala/dsp/errors/Errors.scala index 812a0372cb..847b3fe629 100644 --- a/dsp-shared/src/main/scala/dsp/errors/Errors.scala +++ b/dsp-shared/src/main/scala/dsp/errors/Errors.scala @@ -106,6 +106,9 @@ case class ForbiddenException(message: String) extends RequestRejectedException( * @param message a description of the error. */ case class NotFoundException(message: String) extends RequestRejectedException(message) +object NotFoundException { + val notFound = NotFoundException("The requested data was not found") +} /** * An exception indicating that a requested update is not allowed because it would create a duplicate value. @@ -271,91 +274,6 @@ object AssertionException { AssertionException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) } -/** - * An abstract class for exceptions indicating that something went wrong with the triplestore. - * - * @param message a description of the error. - * @param cause the original exception representing the cause of the error, if any. - */ -abstract class TriplestoreException(message: String, cause: Option[Throwable] = None) - extends InternalServerException(message, cause) - -/** - * Indicates that the network connection to the triplestore failed. - * - * @param message a description of the error. - * @param cause the original exception representing the cause of the error, if any. - */ -case class TriplestoreConnectionException(message: String, cause: Option[Throwable] = None) - extends TriplestoreException(message, cause) - -object TriplestoreConnectionException { - def apply(message: String, e: Throwable, log: Logger): TriplestoreConnectionException = - TriplestoreConnectionException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) -} - -/** - * Indicates that a read timeout occurred while waiting for data from the triplestore. - * - * @param message a description of the error. - * @param cause the original exception representing the cause of the error, if any. - */ -final case class TriplestoreTimeoutException(message: String, cause: Option[Throwable] = None) - extends TriplestoreException(message, cause) - -object TriplestoreTimeoutException { - def apply(message: String, e: Throwable, log: Logger): TriplestoreTimeoutException = - TriplestoreTimeoutException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) - - def apply(message: String, cause: Throwable): TriplestoreTimeoutException = - TriplestoreTimeoutException(message, Some(cause)) -} - -/** - * Indicates that we tried using a feature which is unsuported by the selected triplestore. - * - * @param message a description of the error. - * @param cause the original exception representing the cause of the error, if any. - */ -case class TriplestoreUnsupportedFeatureException(message: String, cause: Option[Throwable] = None) - extends TriplestoreException(message, cause) - -object TriplestoreUnsupportedFeatureException { - def apply(message: String, e: Throwable, log: Logger): TriplestoreUnsupportedFeatureException = - TriplestoreUnsupportedFeatureException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) -} - -/** - * Indicates that something inside the Triplestore package went wrong. More details can be given in the message parameter. - * - * @param message a description of the error. - * @param cause the original exception representing the cause of the error, if any. - */ -case class TriplestoreInternalException(message: String, cause: Option[Throwable] = None) - extends TriplestoreException(message, cause) - -object TriplestoreInternalException { - def apply(message: String, e: Throwable, log: Logger): TriplestoreInternalException = - TriplestoreInternalException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) -} - -/** - * Indicates that the triplestore returned an error message, or a response that could not be parsed. - * - * @param message a description of the error. - * @param cause the original exception representing the cause of the error, if any. - */ -final case class TriplestoreResponseException(message: String, cause: Option[Throwable] = None) - extends TriplestoreException(message, cause) - -object TriplestoreResponseException { - def apply(message: String, e: Throwable, log: Logger): TriplestoreResponseException = - TriplestoreResponseException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) - - def apply(message: String): TriplestoreResponseException = - TriplestoreResponseException(message) -} - /** * Indicates an inconsistency in repository data. * @@ -452,22 +370,6 @@ case class FileWriteException(message: String) extends InternalServerException(m */ case class NotImplementedException(message: String) extends InternalServerException(message) -/** - * Indicates that an error occurred with Sipi not relating to the user's request (it is not the user's fault). - * - * @param message a description of the error. - */ -case class SipiException(message: String, cause: Option[Throwable] = None) - extends InternalServerException(message, cause) - -object SipiException { - def apply(message: String, e: Throwable, log: Logger): SipiException = - SipiException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) - - def apply(message: String, e: Throwable): SipiException = - SipiException(message, Some(e)) -} - /** * An abstract base class for exceptions indicating that something about a configuration made it impossible to start. * diff --git a/webapi/src/main/resources/application.conf b/webapi/src/main/resources/application.conf index db9adbc7c1..727e1890ba 100644 --- a/webapi/src/main/resources/application.conf +++ b/webapi/src/main/resources/application.conf @@ -149,39 +149,6 @@ akka { } } -akka.actor.deployment { - - /applicationManager/storeManager/triplestoreManager/httpTriplestoreRouter { - router = balancing-pool - nr-of-instances = 10 - nr-of-instances = ${?KNORA_WEBAPI_DB_CONNECTIONS} - pool-dispatcher { - executor = "thread-pool-executor" - - # allocate exactly 10 threads for this pool - thread-pool-executor { - core-pool-size-min = 2 - core-pool-size-min = ${?KNORA_WEBAPI_DB_CONNECTIONS} - core-pool-size-max = 2 - core-pool-size-max = ${?KNORA_WEBAPI_DB_CONNECTIONS} - } - } - } - - /applicationManager/storeManager/iiifManager/sipiConnector { - router = balancing-pool - nr-of-instances = 10 - pool-dispatcher { - executor = "thread-pool-executor" - - # allocate exactly 10 threads for this pool - thread-pool-executor { - core-pool-size-min = 10 - core-pool-size-max = 10 - } - } - } -} // all responder actors should run on this dispatcher knora-actor-dispatcher { diff --git a/webapi/src/main/scala/org/knora/webapi/app/ApplicationActor.scala b/webapi/src/main/scala/org/knora/webapi/app/ApplicationActor.scala index f715d8cbbf..f9b5524252 100644 --- a/webapi/src/main/scala/org/knora/webapi/app/ApplicationActor.scala +++ b/webapi/src/main/scala/org/knora/webapi/app/ApplicationActor.scala @@ -27,7 +27,6 @@ import org.knora.webapi.config.AppConfig import org.knora.webapi.core.LiveActorMaker import dsp.errors.InconsistentRepositoryDataException import dsp.errors.MissingLastModificationDateOntologyException -import dsp.errors.SipiException import dsp.errors.UnexpectedMessageException import dsp.errors.UnsupportedValueException import org.knora.webapi.http.directives.DSPApiDirectives @@ -58,6 +57,7 @@ import org.knora.webapi.settings._ import org.knora.webapi.store.cache.CacheServiceManager import org.knora.webapi.store.cache.settings.CacheServiceSettings import org.knora.webapi.store.iiif.IIIFServiceManager +import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.util.ActorUtil.future2Message import org.knora.webapi.util.cache.CacheUtil import redis.clients.jedis.exceptions.JedisConnectionException @@ -71,6 +71,8 @@ import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceRequest import org.knora.webapi.messages.store.sipimessages.IIIFRequest import org.knora.webapi.util.ActorUtil import org.knora.webapi.store.triplestore.TriplestoreServiceManager +import org.knora.webapi.messages.ResponderRequest +import akka.routing.RoundRobinPool /** * This is the first actor in the application. All other actors are children @@ -138,6 +140,22 @@ class ApplicationActor( appActor = self ) + val routerActor = + context.actorOf( + RoundRobinPool(1000).props( + Props( + new ApplicationRouterActor( + responderManager, + cacheServiceManager, + iiifServiceManager, + triplestoreManager, + appConfig + ) + ).withDispatcher(KnoraDispatchers.KnoraActorDispatcher) + ), + "RouterActor" + ) + /** * This actor acts as the supervisor for its child actors. * Here we can override the default supervisor strategy. @@ -194,6 +212,7 @@ class ApplicationActor( } def ready(): Receive = { + /* Usually only called from tests */ case AppStop() => appStop() @@ -373,12 +392,10 @@ class ApplicationActor( timers.startSingleTimer("CheckCacheService", CheckCacheService, 5.seconds) // Forward messages to the responder manager and the different store managers. - case msg: KnoraRequestV1 => future2Message(sender(), responderManager.receive(msg), log) - case msg: KnoraRequestV2 => future2Message(sender(), responderManager.receive(msg), log) - case msg: KnoraRequestADM => future2Message(sender(), responderManager.receive(msg), log) - case msg: CacheServiceRequest => ActorUtil.zio2Message(sender(), cacheServiceManager.receive(msg), appConfig) - case msg: IIIFRequest => ActorUtil.zio2Message(sender(), iiifServiceManager.receive(msg), appConfig) - case msg: TriplestoreRequest => ActorUtil.zio2Message(sender(), triplestoreManager.receive(msg), appConfig) + case msg: ResponderRequest => routerActor.forward(msg) + case msg: CacheServiceRequest => routerActor.forward(msg) + case msg: IIIFRequest => routerActor.forward(msg) + case msg: TriplestoreRequest => routerActor.forward(msg) case akka.actor.Status.Failure(ex: Exception) => ex match { diff --git a/webapi/src/main/scala/org/knora/webapi/app/ApplicationRouterActor.scala b/webapi/src/main/scala/org/knora/webapi/app/ApplicationRouterActor.scala new file mode 100644 index 0000000000..f7a69155fb --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/app/ApplicationRouterActor.scala @@ -0,0 +1,34 @@ +package org.knora.webapi.app + +import akka.actor.Actor +import org.knora.webapi.messages.ResponderRequest +import com.typesafe.scalalogging.Logger +import org.knora.webapi.responders.ResponderManager +import org.knora.webapi.util.ActorUtil +import scala.concurrent.ExecutionContext +import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceRequest +import org.knora.webapi.messages.store.sipimessages.IIIFRequest +import org.knora.webapi.messages.store.triplestoremessages.TriplestoreRequest +import org.knora.webapi.store.iiif.IIIFServiceManager +import org.knora.webapi.store.triplestore.TriplestoreServiceManager +import org.knora.webapi.store.cache.CacheServiceManager +import org.knora.webapi.config.AppConfig + +class ApplicationRouterActor( + responderManager: ResponderManager, + cacheServiceManager: CacheServiceManager, + iiifServiceManager: IIIFServiceManager, + triplestoreManager: TriplestoreServiceManager, + appConfig: AppConfig +) extends Actor { + val log: Logger = Logger(this.getClass()) + implicit val ec: ExecutionContext = context.dispatcher + def receive: Receive = { + case msg: ResponderRequest.KnoraRequestV1 => ActorUtil.future2Message(sender(), responderManager.receive(msg), log) + case msg: ResponderRequest.KnoraRequestV2 => ActorUtil.future2Message(sender(), responderManager.receive(msg), log) + case msg: ResponderRequest.KnoraRequestADM => ActorUtil.future2Message(sender(), responderManager.receive(msg), log) + case msg: CacheServiceRequest => ActorUtil.zio2Message(sender(), cacheServiceManager.receive(msg), appConfig, log) + case msg: IIIFRequest => ActorUtil.zio2Message(sender(), iiifServiceManager.receive(msg), appConfig, log) + case msg: TriplestoreRequest => ActorUtil.zio2Message(sender(), triplestoreManager.receive(msg), appConfig, log) + } +} diff --git a/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesV2.scala b/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesV2.scala index 1b999c7ba5..88a69c4df1 100644 --- a/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesV2.scala @@ -8,6 +8,7 @@ package org.knora.webapi.http.status import akka.http.scaladsl.model.StatusCode import akka.http.scaladsl.model.StatusCodes import dsp.errors._ +import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException /** * The possible values for the HTTP status code that is returned as part of each Knora API v2 response. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index 6ea5b95b43..68e8093d38 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -13,7 +13,6 @@ import com.typesafe.scalalogging.Logger import dsp.errors.AssertionException import dsp.errors.BadRequestException import dsp.errors.NotImplementedException -import dsp.errors.SipiException import dsp.valueobjects.IriErrorMessages import org.knora.webapi._ import org.knora.webapi.messages.IriConversions._ @@ -36,6 +35,7 @@ import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 import org.knora.webapi.messages.v2.responder.standoffmessages._ import org.knora.webapi.responders.ResponderManager import org.knora.webapi.settings.KnoraSettingsImpl +import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.util._ import java.time.Instant diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/StandoffResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/StandoffResponderV1.scala index 0b6d810e48..1cb8907152 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/StandoffResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/StandoffResponderV1.scala @@ -8,7 +8,6 @@ package org.knora.webapi.responders.v1 import akka.pattern._ import org.knora.webapi._ import dsp.errors.NotFoundException -import dsp.errors.SipiException import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.StringFormatter @@ -20,6 +19,7 @@ import org.knora.webapi.messages.v1.responder.standoffmessages._ import org.knora.webapi.messages.v2.responder.standoffmessages._ import org.knora.webapi.responders.Responder import org.knora.webapi.responders.Responder.handleUnexpectedMessage +import org.knora.webapi.store.iiif.errors.SipiException import java.util.UUID import scala.concurrent.Future diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourceUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourceUtilV2.scala index f00b7fe4da..b915ae72df 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourceUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourceUtilV2.scala @@ -215,7 +215,10 @@ object ResourceUtilV2 { ) // If Sipi succeeds, return the future we were given. Otherwise, return a failed future. - appActor.ask(sipiRequest).mapTo[SuccessResponseV2].flatMap(_ => updateFuture) + appActor + .ask(sipiRequest) + .mapTo[SuccessResponseV2] + .flatMap(_ => updateFuture) case Failure(_) => // The file value update failed. Ask Sipi to delete the temporary file. @@ -224,11 +227,14 @@ object ResourceUtilV2 { requestingUser = requestingUser ) - val sipiResponseFuture: Future[SuccessResponseV2] = appActor.ask(sipiRequest).mapTo[SuccessResponseV2] + val sipiResponseFuture: Future[SuccessResponseV2] = + appActor + .ask(sipiRequest) + .mapTo[SuccessResponseV2] // Did Sipi successfully delete the temporary file? sipiResponseFuture.transformWith { - case Success(_) => + case Success(value) => // Yes. Return the future we were given. updateFuture diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index a0a91955be..a8a44e966b 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -52,6 +52,7 @@ import org.knora.webapi.messages.v2.responder.standoffmessages.GetXSLTransformat import org.knora.webapi.messages.v2.responder.valuemessages._ import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.Responder.handleUnexpectedMessage +import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.util._ import java.time.Instant diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResponderWithStandoffV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResponderWithStandoffV2.scala index d334d037cc..ab050f4946 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResponderWithStandoffV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResponderWithStandoffV2.scala @@ -9,7 +9,6 @@ import akka.http.scaladsl.util.FastFuture import akka.pattern._ import org.knora.webapi.IRI import dsp.errors.NotFoundException -import dsp.errors.SipiException import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.util.ConstructResponseUtilV2 @@ -21,6 +20,7 @@ import org.knora.webapi.messages.v2.responder.standoffmessages.GetMappingRespons import org.knora.webapi.messages.v2.responder.standoffmessages.GetXSLTransformationRequestV2 import org.knora.webapi.messages.v2.responder.standoffmessages.GetXSLTransformationResponseV2 import org.knora.webapi.responders.Responder +import org.knora.webapi.store.iiif.errors.SipiException import scala.concurrent.Future diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala index ad4c264642..5fb199226e 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala @@ -12,7 +12,6 @@ import dsp.errors.AssertionException import dsp.errors.BadRequestException import dsp.errors.GravsearchException import dsp.errors.InconsistentRepositoryDataException -import dsp.errors.TriplestoreTimeoutException import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants @@ -42,6 +41,7 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.ReadPropertyInfoV import org.knora.webapi.messages.v2.responder.resourcemessages._ import org.knora.webapi.messages.v2.responder.searchmessages._ import org.knora.webapi.responders.Responder.handleUnexpectedMessage +import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException import org.knora.webapi.util.ApacheLuceneSupport._ import scala.concurrent.Future diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index 83a56f1e51..e4ea5ee47d 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -916,19 +916,22 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde // Get ontology information about the submitted property. - propertyInfoRequestForSubmittedProperty = PropertiesGetRequestV2( - propertyIris = Set(submittedInternalPropertyIri), - allLanguages = false, - requestingUser = updateValueRequest.requestingUser - ) + propertyInfoRequestForSubmittedProperty = + PropertiesGetRequestV2( + propertyIris = Set(submittedInternalPropertyIri), + allLanguages = false, + requestingUser = updateValueRequest.requestingUser + ) propertyInfoResponseForSubmittedProperty: ReadOntologyV2 <- appActor .ask(propertyInfoRequestForSubmittedProperty) .mapTo[ReadOntologyV2] - propertyInfoForSubmittedProperty: ReadPropertyInfoV2 = propertyInfoResponseForSubmittedProperty.properties( - submittedInternalPropertyIri - ) + + propertyInfoForSubmittedProperty: ReadPropertyInfoV2 = + propertyInfoResponseForSubmittedProperty.properties( + submittedInternalPropertyIri + ) // Don't accept link properties. _ = if (propertyInfoForSubmittedProperty.isLinkProp) { @@ -946,23 +949,23 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde // corresponding link property, whose objects we will need to query. Get ontology information about the // adjusted property. - adjustedInternalPropertyInfo: ReadPropertyInfoV2 <- getAdjustedInternalPropertyInfo( - submittedPropertyIri = submittedExternalPropertyIri, - maybeSubmittedValueType = - Some(submittedExternalValueType), - propertyInfoForSubmittedProperty = - propertyInfoForSubmittedProperty, - requestingUser = updateValueRequest.requestingUser - ) + adjustedInternalPropertyInfo: ReadPropertyInfoV2 <- + getAdjustedInternalPropertyInfo( + submittedPropertyIri = submittedExternalPropertyIri, + maybeSubmittedValueType = Some(submittedExternalValueType), + propertyInfoForSubmittedProperty = propertyInfoForSubmittedProperty, + requestingUser = updateValueRequest.requestingUser + ) // Get the resource's metadata and relevant property objects, using the adjusted property. Do this as the system user, // so we can see objects that the user doesn't have permission to see. - resourceInfo: ReadResourceV2 <- getResourceWithPropertyValues( - resourceIri = resourceIri, - propertyInfo = adjustedInternalPropertyInfo, - requestingUser = KnoraSystemInstances.Users.SystemUser - ) + resourceInfo: ReadResourceV2 <- + getResourceWithPropertyValues( + resourceIri = resourceIri, + propertyInfo = adjustedInternalPropertyInfo, + requestingUser = KnoraSystemInstances.Users.SystemUser + ) _ = if (resourceInfo.resourceClassIri != submittedExternalResourceClassIri.toOntologySchema(InternalSchema)) { throw BadRequestException( @@ -971,14 +974,15 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } // Check that the resource has the value that the user wants to update, as an object of the submitted property. - currentValue: ReadValueV2 = resourceInfo.values - .get(submittedInternalPropertyIri) - .flatMap(_.find(_.valueIri == valueIri)) - .getOrElse { - throw NotFoundException( - s"Resource <$resourceIri> does not have value <$valueIri> as an object of property <$submittedExternalPropertyIri>" - ) - } + currentValue: ReadValueV2 = + resourceInfo.values + .get(submittedInternalPropertyIri) + .flatMap(_.find(_.valueIri == valueIri)) + .getOrElse { + throw NotFoundException( + s"Resource <$resourceIri> does not have value <$valueIri> as an object of property <$submittedExternalPropertyIri>" + ) + } // Check that the current value has the submitted value type. _ = if (currentValue.valueContent.valueType != submittedExternalValueType.toOntologySchema(InternalSchema)) { @@ -1011,16 +1015,14 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde ): Future[UpdateValueResponseV2] = for { // Do the initial checks, and get information about the resource, the property, and the value. - resourcePropertyValue: ResourcePropertyValue <- getResourcePropertyValue( - resourceIri = updateValuePermissionsV2.resourceIri, - submittedExternalResourceClassIri = - updateValuePermissionsV2.resourceClassIri, - submittedExternalPropertyIri = - updateValuePermissionsV2.propertyIri, - valueIri = updateValuePermissionsV2.valueIri, - submittedExternalValueType = - updateValuePermissionsV2.valueType - ) + resourcePropertyValue: ResourcePropertyValue <- + getResourcePropertyValue( + resourceIri = updateValuePermissionsV2.resourceIri, + submittedExternalResourceClassIri = updateValuePermissionsV2.resourceClassIri, + submittedExternalPropertyIri = updateValuePermissionsV2.propertyIri, + valueIri = updateValuePermissionsV2.valueIri, + submittedExternalValueType = updateValuePermissionsV2.valueType + ) resourceInfo: ReadResourceV2 = resourcePropertyValue.resource submittedInternalPropertyIri: SmartIri = resourcePropertyValue.submittedInternalPropertyIri @@ -1028,17 +1030,20 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde // Validate and reformat the submitted permissions. - newValuePermissionLiteral: String <- PermissionUtilADM.validatePermissions( - permissionLiteral = updateValuePermissionsV2.permissions, - appActor = appActor - ) + newValuePermissionLiteral: String <- + PermissionUtilADM.validatePermissions( + permissionLiteral = updateValuePermissionsV2.permissions, + appActor = appActor + ) // Check that the user has ChangeRightsPermission on the value, and that the new permissions are // different from the current ones. - currentPermissionsParsed: Map[EntityPermission, Set[IRI]] = PermissionUtilADM.parsePermissions( - currentValue.permissions - ) + currentPermissionsParsed: Map[EntityPermission, Set[IRI]] = + PermissionUtilADM.parsePermissions( + currentValue.permissions + ) + newPermissionsParsed: Map[EntityPermission, Set[IRI]] = PermissionUtilADM.parsePermissions( updateValuePermissionsV2.permissions, @@ -1066,39 +1071,43 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde stringFormatter.makeRandomValueIri(resourceInfo.resourceIri) ) - currentTime: Instant = updateValuePermissionsV2.valueCreationDate.getOrElse(Instant.now) - - sparqlUpdate = org.knora.webapi.messages.twirl.queries.sparql.v2.txt - .changeValuePermissions( - dataNamedGraph = dataNamedGraph, - resourceIri = resourceInfo.resourceIri, - propertyIri = submittedInternalPropertyIri, - currentValueIri = currentValue.valueIri, - valueTypeIri = currentValue.valueContent.valueType, - newValueIri = newValueIri, - newPermissions = newValuePermissionLiteral, - currentTime = currentTime - ) - .toString() + currentTime: Instant = + updateValuePermissionsV2.valueCreationDate.getOrElse(Instant.now) + + sparqlUpdate = + org.knora.webapi.messages.twirl.queries.sparql.v2.txt + .changeValuePermissions( + dataNamedGraph = dataNamedGraph, + resourceIri = resourceInfo.resourceIri, + propertyIri = submittedInternalPropertyIri, + currentValueIri = currentValue.valueIri, + valueTypeIri = currentValue.valueContent.valueType, + newValueIri = newValueIri, + newPermissions = newValuePermissionLiteral, + currentTime = currentTime + ) + .toString() _ <- appActor.ask(SparqlUpdateRequest(sparqlUpdate)).mapTo[SparqlUpdateResponse] // Check that the value was written correctly to the triplestore. - unverifiedValue = UnverifiedValueV2( - newValueIri = newValueIri, - newValueUUID = currentValue.valueHasUUID, - valueContent = currentValue.valueContent, - permissions = newValuePermissionLiteral, - creationDate = currentTime - ) + unverifiedValue = + UnverifiedValueV2( + newValueIri = newValueIri, + newValueUUID = currentValue.valueHasUUID, + valueContent = currentValue.valueContent, + permissions = newValuePermissionLiteral, + creationDate = currentTime + ) - verifiedValue: VerifiedValueV2 <- verifyValue( - resourceIri = resourceInfo.resourceIri, - propertyIri = submittedInternalPropertyIri, - unverifiedValue = unverifiedValue, - requestingUser = updateValueRequest.requestingUser - ) + verifiedValue: VerifiedValueV2 <- + verifyValue( + resourceIri = resourceInfo.resourceIri, + propertyIri = submittedInternalPropertyIri, + unverifiedValue = unverifiedValue, + requestingUser = updateValueRequest.requestingUser + ) } yield UpdateValueResponseV2( valueIri = verifiedValue.newValueIri, valueType = unverifiedValue.valueContent.valueType, @@ -1117,16 +1126,14 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde ): Future[UpdateValueResponseV2] = { for { // Do the initial checks, and get information about the resource, the property, and the value. - resourcePropertyValue: ResourcePropertyValue <- getResourcePropertyValue( - resourceIri = updateValueContentV2.resourceIri, - submittedExternalResourceClassIri = - updateValueContentV2.resourceClassIri, - submittedExternalPropertyIri = - updateValueContentV2.propertyIri, - valueIri = updateValueContentV2.valueIri, - submittedExternalValueType = - updateValueContentV2.valueContent.valueType - ) + resourcePropertyValue: ResourcePropertyValue <- + getResourcePropertyValue( + resourceIri = updateValueContentV2.resourceIri, + submittedExternalResourceClassIri = updateValueContentV2.resourceClassIri, + submittedExternalPropertyIri = updateValueContentV2.propertyIri, + valueIri = updateValueContentV2.valueIri, + submittedExternalValueType = updateValueContentV2.valueContent.valueType + ) resourceInfo: ReadResourceV2 = resourcePropertyValue.resource submittedInternalPropertyIri: SmartIri = resourcePropertyValue.submittedInternalPropertyIri @@ -1134,25 +1141,28 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde currentValue: ReadValueV2 = resourcePropertyValue.value // Did the user submit permissions for the new value? - newValueVersionPermissionLiteral <- updateValueContentV2.permissions match { - case Some(permissions) => - // Yes. Validate them. - PermissionUtilADM.validatePermissions( - permissionLiteral = permissions, - appActor = appActor - ) + newValueVersionPermissionLiteral <- + updateValueContentV2.permissions match { + case Some(permissions) => + // Yes. Validate them. + PermissionUtilADM.validatePermissions( + permissionLiteral = permissions, + appActor = appActor + ) - case None => - // No. Use the permissions on the current version of the value. - FastFuture.successful(currentValue.permissions) - } + case None => + // No. Use the permissions on the current version of the value. + FastFuture.successful(currentValue.permissions) + } // Check that the user has permission to do the update. If they want to change the permissions // on the value, they need ChangeRightsPermission, otherwise they need ModifyPermission. - currentPermissionsParsed: Map[EntityPermission, Set[IRI]] = PermissionUtilADM.parsePermissions( - currentValue.permissions - ) + currentPermissionsParsed: Map[EntityPermission, Set[IRI]] = + PermissionUtilADM.parsePermissions( + currentValue.permissions + ) + newPermissionsParsed: Map[EntityPermission, Set[IRI]] = PermissionUtilADM.parsePermissions( newValueVersionPermissionLiteral, @@ -1176,9 +1186,10 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde ) // Convert the submitted value content to the internal schema. - submittedInternalValueContent: ValueContentV2 = updateValueContentV2.valueContent.toOntologySchema( - InternalSchema - ) + submittedInternalValueContent: ValueContentV2 = + updateValueContentV2.valueContent.toOntologySchema( + InternalSchema + ) // Check that the object of the adjusted property (the value to be created, or the target of the link to be created) will have // the correct type for the adjusted property's knora-base:objectClassConstraint. @@ -1217,7 +1228,8 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde // Check that the updated value would not duplicate the current value version. - unescapedSubmittedInternalValueContent = submittedInternalValueContent.unescape + unescapedSubmittedInternalValueContent = + submittedInternalValueContent.unescape _ = if (unescapedSubmittedInternalValueContent.wouldDuplicateCurrentVersion(currentValue.valueContent)) { throw DuplicateValueException("The submitted value is the same as the current version") @@ -1225,9 +1237,10 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde // Check that the updated value would not duplicate another existing value of the resource. - currentValuesForProp: Seq[ReadValueV2] = resourceInfo.values - .getOrElse(submittedInternalPropertyIri, Seq.empty[ReadValueV2]) - .filter(_.valueIri != updateValueContentV2.valueIri) + currentValuesForProp: Seq[ReadValueV2] = + resourceInfo.values + .getOrElse(submittedInternalPropertyIri, Seq.empty[ReadValueV2]) + .filter(_.valueIri != updateValueContentV2.valueIri) _ = if ( currentValuesForProp.exists(currentVal => @@ -1264,49 +1277,50 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde // Create the new value version. - unverifiedValue: UnverifiedValueV2 <- (currentValue, submittedInternalValueContent) match { - case ( - currentLinkValue: ReadLinkValueV2, - newLinkValue: LinkValueContentV2 - ) => - updateLinkValueV2AfterChecks( - dataNamedGraph = dataNamedGraph, - resourceInfo = resourceInfo, - linkPropertyIri = - adjustedInternalPropertyInfo.entityInfoContent.propertyIri, - currentLinkValue = currentLinkValue, - newLinkValue = newLinkValue, - valueCreator = updateValueRequest.requestingUser.id, - valuePermissions = newValueVersionPermissionLiteral, - valueCreationDate = updateValueContentV2.valueCreationDate, - newValueVersionIri = updateValueContentV2.newValueVersionIri, - requestingUser = updateValueRequest.requestingUser - ) + unverifiedValue: UnverifiedValueV2 <- + (currentValue, submittedInternalValueContent) match { + case ( + currentLinkValue: ReadLinkValueV2, + newLinkValue: LinkValueContentV2 + ) => + updateLinkValueV2AfterChecks( + dataNamedGraph = dataNamedGraph, + resourceInfo = resourceInfo, + linkPropertyIri = adjustedInternalPropertyInfo.entityInfoContent.propertyIri, + currentLinkValue = currentLinkValue, + newLinkValue = newLinkValue, + valueCreator = updateValueRequest.requestingUser.id, + valuePermissions = newValueVersionPermissionLiteral, + valueCreationDate = updateValueContentV2.valueCreationDate, + newValueVersionIri = updateValueContentV2.newValueVersionIri, + requestingUser = updateValueRequest.requestingUser + ) - case _ => - updateOrdinaryValueV2AfterChecks( - dataNamedGraph = dataNamedGraph, - resourceInfo = resourceInfo, - propertyIri = - adjustedInternalPropertyInfo.entityInfoContent.propertyIri, - currentValue = currentValue, - newValueVersion = submittedInternalValueContent, - valueCreator = updateValueRequest.requestingUser.id, - valuePermissions = newValueVersionPermissionLiteral, - valueCreationDate = updateValueContentV2.valueCreationDate, - newValueVersionIri = updateValueContentV2.newValueVersionIri, - requestingUser = updateValueRequest.requestingUser - ) - } + case _ => + updateOrdinaryValueV2AfterChecks( + dataNamedGraph = dataNamedGraph, + resourceInfo = resourceInfo, + propertyIri = adjustedInternalPropertyInfo.entityInfoContent.propertyIri, + currentValue = currentValue, + newValueVersion = submittedInternalValueContent, + valueCreator = updateValueRequest.requestingUser.id, + valuePermissions = newValueVersionPermissionLiteral, + valueCreationDate = updateValueContentV2.valueCreationDate, + newValueVersionIri = updateValueContentV2.newValueVersionIri, + requestingUser = updateValueRequest.requestingUser + ) + } // Check that the value was written correctly to the triplestore. - verifiedValue: VerifiedValueV2 <- verifyValue( - resourceIri = updateValueContentV2.resourceIri, - propertyIri = submittedInternalPropertyIri, - unverifiedValue = unverifiedValue, - requestingUser = updateValueRequest.requestingUser - ) + verifiedValue: VerifiedValueV2 <- + verifyValue( + resourceIri = updateValueContentV2.resourceIri, + propertyIri = submittedInternalPropertyIri, + unverifiedValue = unverifiedValue, + requestingUser = updateValueRequest.requestingUser + ) + } yield UpdateValueResponseV2( valueIri = verifiedValue.newValueIri, valueType = unverifiedValue.valueContent.valueType, @@ -1380,98 +1394,92 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde ) // If we're updating a text value, update direct links and LinkValues for any resource references in Standoff. - standoffLinkUpdates: Seq[SparqlTemplateLinkUpdate] <- (currentValue.valueContent, newValueVersion) match { - case ( - currentTextValue: TextValueContentV2, - newTextValue: TextValueContentV2 - ) => - // Identify the resource references that have been added or removed in the new version of - // the value. - val addedResourceRefs = - newTextValue.standoffLinkTagTargetResourceIris -- currentTextValue.standoffLinkTagTargetResourceIris - val removedResourceRefs = - currentTextValue.standoffLinkTagTargetResourceIris -- newTextValue.standoffLinkTagTargetResourceIris - - // Construct a SparqlTemplateLinkUpdate for each reference that was added. - val standoffLinkUpdatesForAddedResourceRefFutures - : Seq[Future[SparqlTemplateLinkUpdate]] = - addedResourceRefs.toVector.map { targetResourceIri => - incrementLinkValue( - sourceResourceInfo = resourceInfo, - linkPropertyIri = - OntologyConstants.KnoraBase.HasStandoffLinkTo.toSmartIri, - targetResourceIri = targetResourceIri, - valueCreator = - OntologyConstants.KnoraAdmin.SystemUser, - valuePermissions = standoffLinkValuePermissions, - requestingUser = requestingUser - ) - } - - val standoffLinkUpdatesForAddedResourceRefsFuture - : Future[Seq[SparqlTemplateLinkUpdate]] = - Future.sequence( - standoffLinkUpdatesForAddedResourceRefFutures - ) - - // Construct a SparqlTemplateLinkUpdate for each reference that was removed. - val standoffLinkUpdatesForRemovedResourceRefFutures - : Seq[Future[SparqlTemplateLinkUpdate]] = - removedResourceRefs.toVector.map { - removedTargetResource => - decrementLinkValue( - sourceResourceInfo = resourceInfo, - linkPropertyIri = - OntologyConstants.KnoraBase.HasStandoffLinkTo.toSmartIri, - targetResourceIri = removedTargetResource, - valueCreator = - OntologyConstants.KnoraAdmin.SystemUser, - valuePermissions = standoffLinkValuePermissions, - requestingUser = requestingUser - ) - } - - val standoffLinkUpdatesForRemovedResourceRefFuture = - Future.sequence( - standoffLinkUpdatesForRemovedResourceRefFutures - ) - - for { - standoffLinkUpdatesForAddedResourceRefs <- - standoffLinkUpdatesForAddedResourceRefsFuture - standoffLinkUpdatesForRemovedResourceRefs <- - standoffLinkUpdatesForRemovedResourceRefFuture - } yield standoffLinkUpdatesForAddedResourceRefs ++ standoffLinkUpdatesForRemovedResourceRefs - - case _ => - FastFuture.successful( - Vector.empty[SparqlTemplateLinkUpdate] - ) - } + standoffLinkUpdates: Seq[SparqlTemplateLinkUpdate] <- + (currentValue.valueContent, newValueVersion) match { + case ( + currentTextValue: TextValueContentV2, + newTextValue: TextValueContentV2 + ) => + // Identify the resource references that have been added or removed in the new version of + // the value. + val addedResourceRefs = + newTextValue.standoffLinkTagTargetResourceIris -- currentTextValue.standoffLinkTagTargetResourceIris + val removedResourceRefs = + currentTextValue.standoffLinkTagTargetResourceIris -- newTextValue.standoffLinkTagTargetResourceIris + + // Construct a SparqlTemplateLinkUpdate for each reference that was added. + val standoffLinkUpdatesForAddedResourceRefFutures: Seq[Future[SparqlTemplateLinkUpdate]] = + addedResourceRefs.toVector.map { targetResourceIri => + incrementLinkValue( + sourceResourceInfo = resourceInfo, + linkPropertyIri = OntologyConstants.KnoraBase.HasStandoffLinkTo.toSmartIri, + targetResourceIri = targetResourceIri, + valueCreator = OntologyConstants.KnoraAdmin.SystemUser, + valuePermissions = standoffLinkValuePermissions, + requestingUser = requestingUser + ) + } + + val standoffLinkUpdatesForAddedResourceRefsFuture: Future[Seq[SparqlTemplateLinkUpdate]] = + Future.sequence( + standoffLinkUpdatesForAddedResourceRefFutures + ) + + // Construct a SparqlTemplateLinkUpdate for each reference that was removed. + val standoffLinkUpdatesForRemovedResourceRefFutures: Seq[Future[SparqlTemplateLinkUpdate]] = + removedResourceRefs.toVector.map { removedTargetResource => + decrementLinkValue( + sourceResourceInfo = resourceInfo, + linkPropertyIri = OntologyConstants.KnoraBase.HasStandoffLinkTo.toSmartIri, + targetResourceIri = removedTargetResource, + valueCreator = OntologyConstants.KnoraAdmin.SystemUser, + valuePermissions = standoffLinkValuePermissions, + requestingUser = requestingUser + ) + } + + val standoffLinkUpdatesForRemovedResourceRefFuture = + Future.sequence( + standoffLinkUpdatesForRemovedResourceRefFutures + ) + + for { + standoffLinkUpdatesForAddedResourceRefs <- + standoffLinkUpdatesForAddedResourceRefsFuture + standoffLinkUpdatesForRemovedResourceRefs <- + standoffLinkUpdatesForRemovedResourceRefFuture + } yield standoffLinkUpdatesForAddedResourceRefs ++ standoffLinkUpdatesForRemovedResourceRefs + + case _ => + FastFuture.successful( + Vector.empty[SparqlTemplateLinkUpdate] + ) + } // If no custom value creation date was provided, make a timestamp to indicate when the value // was updated. currentTime: Instant = valueCreationDate.getOrElse(Instant.now) // Generate a SPARQL update. - sparqlUpdate = org.knora.webapi.messages.twirl.queries.sparql.v2.txt - .addValueVersion( - dataNamedGraph = dataNamedGraph, - resourceIri = resourceInfo.resourceIri, - propertyIri = propertyIri, - currentValueIri = currentValue.valueIri, - newValueIri = newValueIri, - valueTypeIri = newValueVersion.valueType, - value = newValueVersion, - valueCreator = valueCreator, - valuePermissions = valuePermissions, - maybeComment = newValueVersion.comment, - linkUpdates = standoffLinkUpdates, - currentTime = currentTime, - requestingUser = requestingUser.id, - stringFormatter = stringFormatter - ) - .toString() + sparqlUpdate = + org.knora.webapi.messages.twirl.queries.sparql.v2.txt + .addValueVersion( + dataNamedGraph = dataNamedGraph, + resourceIri = resourceInfo.resourceIri, + propertyIri = propertyIri, + currentValueIri = currentValue.valueIri, + newValueIri = newValueIri, + valueTypeIri = newValueVersion.valueType, + value = newValueVersion, + valueCreator = valueCreator, + valuePermissions = valuePermissions, + maybeComment = newValueVersion.comment, + linkUpdates = standoffLinkUpdates, + currentTime = currentTime, + requestingUser = requestingUser.id, + stringFormatter = stringFormatter + ) + .toString() /* _ = println("================ Update value ================") @@ -1521,27 +1529,27 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde if (currentLinkValue.valueContent.referredResourceIri != newLinkValue.referredResourceIri) { for { // Yes. Delete the existing link and decrement its LinkValue's reference count. - sparqlTemplateLinkUpdateForCurrentLink: SparqlTemplateLinkUpdate <- decrementLinkValue( - sourceResourceInfo = resourceInfo, - linkPropertyIri = linkPropertyIri, - targetResourceIri = - currentLinkValue.valueContent.referredResourceIri, - valueCreator = valueCreator, - valuePermissions = valuePermissions, - requestingUser = requestingUser - ) + sparqlTemplateLinkUpdateForCurrentLink: SparqlTemplateLinkUpdate <- + decrementLinkValue( + sourceResourceInfo = resourceInfo, + linkPropertyIri = linkPropertyIri, + targetResourceIri = currentLinkValue.valueContent.referredResourceIri, + valueCreator = valueCreator, + valuePermissions = valuePermissions, + requestingUser = requestingUser + ) // Create a new link, and create a new LinkValue for it. - sparqlTemplateLinkUpdateForNewLink: SparqlTemplateLinkUpdate <- incrementLinkValue( - sourceResourceInfo = resourceInfo, - linkPropertyIri = linkPropertyIri, - targetResourceIri = - newLinkValue.referredResourceIri, - customNewLinkValueIri = newValueVersionIri, - valueCreator = valueCreator, - valuePermissions = valuePermissions, - requestingUser = requestingUser - ) + sparqlTemplateLinkUpdateForNewLink: SparqlTemplateLinkUpdate <- + incrementLinkValue( + sourceResourceInfo = resourceInfo, + linkPropertyIri = linkPropertyIri, + targetResourceIri = newLinkValue.referredResourceIri, + customNewLinkValueIri = newValueVersionIri, + valueCreator = valueCreator, + valuePermissions = valuePermissions, + requestingUser = requestingUser + ) // If no custom value creation date was provided, make a timestamp to indicate when the link value // was updated. @@ -1551,21 +1559,22 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde newLinkValueUUID = UUID.randomUUID // Generate a SPARQL update string. - sparqlUpdate <- Future( - org.knora.webapi.messages.twirl.queries.sparql.v2.txt - .changeLinkTarget( - dataNamedGraph = dataNamedGraph, - linkSourceIri = resourceInfo.resourceIri, - linkUpdateForCurrentLink = sparqlTemplateLinkUpdateForCurrentLink, - linkUpdateForNewLink = sparqlTemplateLinkUpdateForNewLink, - newLinkValueUUID = newLinkValueUUID, - maybeComment = newLinkValue.comment, - currentTime = currentTime, - requestingUser = requestingUser.id, - stringFormatter = stringFormatter - ) - .toString() - ) + sparqlUpdate <- + Future( + org.knora.webapi.messages.twirl.queries.sparql.v2.txt + .changeLinkTarget( + dataNamedGraph = dataNamedGraph, + linkSourceIri = resourceInfo.resourceIri, + linkUpdateForCurrentLink = sparqlTemplateLinkUpdateForCurrentLink, + linkUpdateForNewLink = sparqlTemplateLinkUpdateForNewLink, + newLinkValueUUID = newLinkValueUUID, + maybeComment = newLinkValue.comment, + currentTime = currentTime, + requestingUser = requestingUser.id, + stringFormatter = stringFormatter + ) + .toString() + ) /* _ = println("================ Update link ================") @@ -1584,32 +1593,34 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } else { for { // We're not changing the link target, just the metadata on the LinkValue. - sparqlTemplateLinkUpdate: SparqlTemplateLinkUpdate <- changeLinkValueMetadata( - sourceResourceInfo = resourceInfo, - linkPropertyIri = linkPropertyIri, - targetResourceIri = - currentLinkValue.valueContent.referredResourceIri, - customNewLinkValueIri = newValueVersionIri, - valueCreator = valueCreator, - valuePermissions = valuePermissions, - requestingUser = requestingUser - ) + sparqlTemplateLinkUpdate: SparqlTemplateLinkUpdate <- + changeLinkValueMetadata( + sourceResourceInfo = resourceInfo, + linkPropertyIri = linkPropertyIri, + targetResourceIri = currentLinkValue.valueContent.referredResourceIri, + customNewLinkValueIri = newValueVersionIri, + valueCreator = valueCreator, + valuePermissions = valuePermissions, + requestingUser = requestingUser + ) // Make a timestamp to indicate when the link value was updated. currentTime: Instant = Instant.now - sparqlUpdate = org.knora.webapi.messages.twirl.queries.sparql.v2.txt - .changeLinkMetadata( - dataNamedGraph = dataNamedGraph, - linkSourceIri = resourceInfo.resourceIri, - linkUpdate = sparqlTemplateLinkUpdate, - maybeComment = newLinkValue.comment, - currentTime = currentTime, - requestingUser = requestingUser.id - ) - .toString() + sparqlUpdate = + org.knora.webapi.messages.twirl.queries.sparql.v2.txt + .changeLinkMetadata( + dataNamedGraph = dataNamedGraph, + linkSourceIri = resourceInfo.resourceIri, + linkUpdate = sparqlTemplateLinkUpdate, + maybeComment = newLinkValue.comment, + currentTime = currentTime, + requestingUser = requestingUser.id + ) + .toString() _ <- appActor.ask(SparqlUpdateRequest(sparqlUpdate)).mapTo[SparqlUpdateResponse] + } yield UnverifiedValueV2( newValueIri = sparqlTemplateLinkUpdate.newLinkValueIri, newValueUUID = currentLinkValue.valueHasUUID, @@ -1628,25 +1639,28 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde def makeTaskFuture: Future[SuccessResponseV2] = { for { // Convert the submitted property IRI to the internal schema. - submittedInternalPropertyIri: SmartIri <- Future( - deleteValueRequest.propertyIri.toOntologySchema(InternalSchema) - ) + submittedInternalPropertyIri: SmartIri <- + Future( + deleteValueRequest.propertyIri.toOntologySchema(InternalSchema) + ) // Get ontology information about the submitted property. - propertyInfoRequestForSubmittedProperty = PropertiesGetRequestV2( - propertyIris = Set(submittedInternalPropertyIri), - allLanguages = false, - requestingUser = deleteValueRequest.requestingUser - ) + propertyInfoRequestForSubmittedProperty = + PropertiesGetRequestV2( + propertyIris = Set(submittedInternalPropertyIri), + allLanguages = false, + requestingUser = deleteValueRequest.requestingUser + ) propertyInfoResponseForSubmittedProperty: ReadOntologyV2 <- appActor .ask(propertyInfoRequestForSubmittedProperty) .mapTo[ReadOntologyV2] - propertyInfoForSubmittedProperty: ReadPropertyInfoV2 = propertyInfoResponseForSubmittedProperty.properties( - submittedInternalPropertyIri - ) + propertyInfoForSubmittedProperty: ReadPropertyInfoV2 = + propertyInfoResponseForSubmittedProperty.properties( + submittedInternalPropertyIri + ) // Don't accept link properties. _ = if (propertyInfoForSubmittedProperty.isLinkProp) { @@ -1664,24 +1678,26 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde // corresponding link property, whose objects we will need to query. Get ontology information about the // adjusted property. - adjustedInternalPropertyInfo: ReadPropertyInfoV2 <- getAdjustedInternalPropertyInfo( - submittedPropertyIri = deleteValueRequest.propertyIri, - maybeSubmittedValueType = None, - propertyInfoForSubmittedProperty = - propertyInfoForSubmittedProperty, - requestingUser = deleteValueRequest.requestingUser - ) + adjustedInternalPropertyInfo: ReadPropertyInfoV2 <- + getAdjustedInternalPropertyInfo( + submittedPropertyIri = deleteValueRequest.propertyIri, + maybeSubmittedValueType = None, + propertyInfoForSubmittedProperty = propertyInfoForSubmittedProperty, + requestingUser = deleteValueRequest.requestingUser + ) - adjustedInternalPropertyIri = adjustedInternalPropertyInfo.entityInfoContent.propertyIri + adjustedInternalPropertyIri = + adjustedInternalPropertyInfo.entityInfoContent.propertyIri // Get the resource's metadata and relevant property objects, using the adjusted property. Do this as the system user, // so we can see objects that the user doesn't have permission to see. - resourceInfo: ReadResourceV2 <- getResourceWithPropertyValues( - resourceIri = deleteValueRequest.resourceIri, - propertyInfo = adjustedInternalPropertyInfo, - requestingUser = KnoraSystemInstances.Users.SystemUser - ) + resourceInfo: ReadResourceV2 <- + getResourceWithPropertyValues( + resourceIri = deleteValueRequest.resourceIri, + propertyInfo = adjustedInternalPropertyInfo, + requestingUser = KnoraSystemInstances.Users.SystemUser + ) // Check that the resource belongs to the class that the client submitted. @@ -1693,19 +1709,21 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde // Check that the resource has the value that the user wants to delete, as an object of the submitted property. - maybeCurrentValue: Option[ReadValueV2] = resourceInfo.values - .get(submittedInternalPropertyIri) - .flatMap(_.find(_.valueIri == deleteValueRequest.valueIri)) + maybeCurrentValue: Option[ReadValueV2] = + resourceInfo.values + .get(submittedInternalPropertyIri) + .flatMap(_.find(_.valueIri == deleteValueRequest.valueIri)) // Check that the user has permission to delete the value. - currentValue: ReadValueV2 = maybeCurrentValue match { - case Some(value) => value - case None => - throw NotFoundException( - s"Resource <${deleteValueRequest.resourceIri}> does not have value <${deleteValueRequest.valueIri}> as an object of property <${deleteValueRequest.propertyIri}>" - ) - } + currentValue: ReadValueV2 = + maybeCurrentValue match { + case Some(value) => value + case None => + throw NotFoundException( + s"Resource <${deleteValueRequest.resourceIri}> does not have value <${deleteValueRequest.valueIri}> as an object of property <${deleteValueRequest.propertyIri}>" + ) + } // Check that the value is of the type that the client submitted. @@ -1727,26 +1745,30 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde // Get the definition of the resource class. - classInfoRequest = ClassesGetRequestV2( - classIris = Set(resourceInfo.resourceClassIri), - allLanguages = false, - requestingUser = deleteValueRequest.requestingUser - ) + classInfoRequest = + ClassesGetRequestV2( + classIris = Set(resourceInfo.resourceClassIri), + allLanguages = false, + requestingUser = deleteValueRequest.requestingUser + ) classInfoResponse: ReadOntologyV2 <- appActor.ask(classInfoRequest).mapTo[ReadOntologyV2] classInfo: ReadClassInfoV2 = classInfoResponse.classes(resourceInfo.resourceClassIri) - cardinalityInfo: Cardinality.KnoraCardinalityInfo = classInfo.allCardinalities.getOrElse( - submittedInternalPropertyIri, - throw InconsistentRepositoryDataException( - s"Resource <${deleteValueRequest.resourceIri}> belongs to class <${resourceInfo.resourceClassIri - .toOntologySchema(ApiV2Complex)}>, which has no cardinality for property <${deleteValueRequest.propertyIri}>" - ) - ) + + cardinalityInfo: Cardinality.KnoraCardinalityInfo = + classInfo.allCardinalities.getOrElse( + submittedInternalPropertyIri, + throw InconsistentRepositoryDataException( + s"Resource <${deleteValueRequest.resourceIri}> belongs to class <${resourceInfo.resourceClassIri + .toOntologySchema(ApiV2Complex)}>, which has no cardinality for property <${deleteValueRequest.propertyIri}>" + ) + ) // Check that the resource class's cardinality for the submitted property allows this value to be deleted. - currentValuesForProp: Seq[ReadValueV2] = resourceInfo.values - .getOrElse(submittedInternalPropertyIri, Seq.empty[ReadValueV2]) + currentValuesForProp: Seq[ReadValueV2] = + resourceInfo.values + .getOrElse(submittedInternalPropertyIri, Seq.empty[ReadValueV2]) _ = if ( @@ -1767,22 +1789,24 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde dataNamedGraph: IRI = stringFormatter.projectDataNamedGraphV2(resourceInfo.projectADM) // Do the update. - deletedValueIri: IRI <- deleteValueV2AfterChecks( - dataNamedGraph = dataNamedGraph, - resourceInfo = resourceInfo, - propertyIri = adjustedInternalPropertyIri, - deleteComment = deleteValueRequest.deleteComment, - deleteDate = deleteValueRequest.deleteDate, - currentValue = currentValue, - requestingUser = deleteValueRequest.requestingUser - ) + deletedValueIri: IRI <- + deleteValueV2AfterChecks( + dataNamedGraph = dataNamedGraph, + resourceInfo = resourceInfo, + propertyIri = adjustedInternalPropertyIri, + deleteComment = deleteValueRequest.deleteComment, + deleteDate = deleteValueRequest.deleteDate, + currentValue = currentValue, + requestingUser = deleteValueRequest.requestingUser + ) // Check whether the update succeeded. - sparqlQuery = org.knora.webapi.messages.twirl.queries.sparql.v2.txt - .checkValueDeletion( - valueIri = deletedValueIri - ) - .toString() + sparqlQuery = + org.knora.webapi.messages.twirl.queries.sparql.v2.txt + .checkValueDeletion( + valueIri = deletedValueIri + ) + .toString() sparqlSelectResponse <- appActor.ask(SparqlSelectRequest(sparqlQuery)).mapTo[SparqlSelectResult] rows = sparqlSelectResponse.results.bindings @@ -1813,11 +1837,12 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } // Do the remaining pre-update checks and the update while holding an update lock on the resource. - taskResult <- IriLocker.runWithIriLock( - deleteValueRequest.apiRequestID, - deleteValueRequest.resourceIri, - () => makeTaskFuture - ) + taskResult <- + IriLocker.runWithIriLock( + deleteValueRequest.apiRequestID, + deleteValueRequest.resourceIri, + () => makeTaskFuture + ) } yield taskResult } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV1.scala index ac64953d51..3847314c1d 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV1.scala @@ -14,7 +14,6 @@ import akka.pattern._ import akka.util.Timeout import org.knora.webapi.IRI import dsp.errors.BadRequestException -import dsp.errors.SipiException import dsp.errors.UnexpectedMessageException import org.knora.webapi.http.status.ApiStatusCodesV1 @@ -35,6 +34,8 @@ import org.knora.webapi.messages.v2.responder.standoffmessages.GetMappingRequest import org.knora.webapi.messages.v2.responder.standoffmessages.GetMappingResponseV2 import org.knora.webapi.responders.ResponderManager import org.knora.webapi.settings.KnoraSettingsImpl +import org.knora.webapi.store.iiif.errors.SipiException + import spray.json.JsNumber import spray.json.JsObject diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala index 1c7f105c0a..3a43835e6c 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala @@ -89,6 +89,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) ) private def getProjects(): Route = path(ProjectsBasePath) { get { requestContext => + log.info("All projects requested.") val requestMessage: Future[ProjectsGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala index 7bc3ab1f1c..1323d4eb59 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala @@ -206,6 +206,8 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit private def fullTextSearch(): Route = path("v2" / "search" / Segment) { searchStr => // TODO: if a space is encoded as a "+", this is not converted back to a space get { requestContext => + log.info(s"Full Text Search for string: $searchStr") + if (searchStr.contains(OntologyConstants.KnoraApi.ApiOntologyHostname)) { throw BadRequestException("It looks like you are submitting a Gravsearch request to a full-text search route") } diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/api/IIIFService.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/api/IIIFService.scala index 7475033114..b473103830 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/api/IIIFService.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/api/IIIFService.scala @@ -20,7 +20,7 @@ trait IIIFService { * @param getFileMetadataRequest the request. * @return a [[GetFileMetadataResponse]] containing the requested metadata. */ - def getFileMetadata(getFileMetadataRequest: GetFileMetadataRequest): Task[GetFileMetadataResponse] + def getFileMetadata(getFileMetadataRequest: GetFileMetadataRequest): UIO[GetFileMetadataResponse] /** * Asks Sipi to move a file from temporary storage to permanent storage. @@ -30,7 +30,7 @@ trait IIIFService { */ def moveTemporaryFileToPermanentStorage( moveTemporaryFileToPermanentStorageRequestV2: MoveTemporaryFileToPermanentStorageRequest - ): Task[SuccessResponseV2] + ): UIO[SuccessResponseV2] /** * Asks Sipi to delete a temporary file. @@ -38,17 +38,17 @@ trait IIIFService { * @param deleteTemporaryFileRequestV2 the request. * @return a [[SuccessResponseV2]]. */ - def deleteTemporaryFile(deleteTemporaryFileRequestV2: DeleteTemporaryFileRequest): Task[SuccessResponseV2] + def deleteTemporaryFile(deleteTemporaryFileRequestV2: DeleteTemporaryFileRequest): UIO[SuccessResponseV2] /** * Asks Sipi for a text file used internally by Knora. * * @param textFileRequest the request message. */ - def getTextFileRequest(textFileRequest: SipiGetTextFileRequest): Task[SipiGetTextFileResponse] + def getTextFileRequest(textFileRequest: SipiGetTextFileRequest): UIO[SipiGetTextFileResponse] /** * Tries to access the IIIF Service. */ - def getStatus(): Task[IIIFServiceStatusResponse] + def getStatus(): UIO[IIIFServiceStatusResponse] } diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/domain/SipiKnoraJsonResponse.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/domain/SipiKnoraJsonResponse.scala index f7ff331a6b..818684764c 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/domain/SipiKnoraJsonResponse.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/domain/SipiKnoraJsonResponse.scala @@ -1,7 +1,7 @@ package org.knora.webapi.store.iiif.domain import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -import dsp.errors.SipiException +import org.knora.webapi.store.iiif.errors.SipiException import spray.json.DefaultJsonProtocol import spray.json.RootJsonFormat diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/errors/Errors.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/errors/Errors.scala new file mode 100644 index 0000000000..d6bc2461b7 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/errors/Errors.scala @@ -0,0 +1,21 @@ +package org.knora.webapi.store.iiif.errors + +import dsp.errors.InternalServerException +import com.typesafe.scalalogging.Logger +import dsp.errors.ExceptionUtil + +/** + * Indicates that an error occurred with Sipi not relating to the user's request (it is not the user's fault). + * + * @param message a description of the error. + */ +final case class SipiException(message: String, cause: Option[Throwable] = None) + extends InternalServerException(message, cause) + +object SipiException { + def apply(message: String, e: Throwable, log: Logger): SipiException = + SipiException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) + + def apply(message: String, e: Throwable): SipiException = + SipiException(message, Some(e)) +} diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala index 34d1983a16..e733976c63 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala @@ -26,12 +26,13 @@ import org.knora.webapi.auth.JWTService import org.knora.webapi.config.AppConfig import dsp.errors.BadRequestException import dsp.errors.NotFoundException -import dsp.errors.SipiException + import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.store.sipimessages._ import org.knora.webapi.messages.v2.responder.SuccessResponseV2 import org.knora.webapi.store.iiif.api.IIIFService import org.knora.webapi.store.iiif.domain._ +import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.util.SipiUtil import spray.json._ import zio._ @@ -56,15 +57,15 @@ case class IIIFServiceSipiImpl( * @param getFileMetadataRequest the request. * @return a [[GetFileMetadataResponse]] containing the requested metadata. */ - def getFileMetadata(getFileMetadataRequest: GetFileMetadataRequest): Task[GetFileMetadataResponse] = { + def getFileMetadata(getFileMetadataRequest: GetFileMetadataRequest): UIO[GetFileMetadataResponse] = { import SipiKnoraJsonResponseProtocol._ for { url <- ZIO.succeed(config.sipi.internalBaseUrl + getFileMetadataRequest.filePath + "/knora.json") request <- ZIO.succeed(new HttpGet(url)) // _ <- ZIO.debug(request) - sipiResponseStr <- doSipiRequest(request) - sipiResponse <- ZIO.attempt(sipiResponseStr.parseJson.convertTo[SipiKnoraJsonResponse]) + sipiResponseStr <- doSipiRequest(request).orDie + sipiResponse <- ZIO.attempt(sipiResponseStr.parseJson.convertTo[SipiKnoraJsonResponse]).orDie } yield GetFileMetadataResponse( originalFilename = sipiResponse.originalFilename, originalMimeType = sipiResponse.originalMimeType, @@ -85,7 +86,7 @@ case class IIIFServiceSipiImpl( */ def moveTemporaryFileToPermanentStorage( moveTemporaryFileToPermanentStorageRequestV2: MoveTemporaryFileToPermanentStorageRequest - ): Task[SuccessResponseV2] = { + ): UIO[SuccessResponseV2] = { // create the JWT token with the necessary permission val jwtToken: UIO[String] = jwt.newToken( @@ -123,7 +124,7 @@ case class IIIFServiceSipiImpl( url <- moveFileUrl(token) entity <- ZIO.succeed(requestEntity) request <- ZIO.succeed(request(url, entity)) - _ <- doSipiRequest(request) + _ <- doSipiRequest(request).orDie } yield SuccessResponseV2("Moved file to permanent storage.") } @@ -133,7 +134,7 @@ case class IIIFServiceSipiImpl( * @param deleteTemporaryFileRequestV2 the request. * @return a [[SuccessResponseV2]]. */ - def deleteTemporaryFile(deleteTemporaryFileRequestV2: DeleteTemporaryFileRequest): Task[SuccessResponseV2] = { + def deleteTemporaryFile(deleteTemporaryFileRequestV2: DeleteTemporaryFileRequest): UIO[SuccessResponseV2] = { val jwtToken: UIO[String] = jwt.newToken( deleteTemporaryFileRequestV2.requestingUser.id, @@ -156,7 +157,7 @@ case class IIIFServiceSipiImpl( token <- jwtToken url <- deleteUrl(token) request <- ZIO.succeed(new HttpDelete(url)) - _ <- doSipiRequest(request) + _ <- doSipiRequest(request).orDie } yield SuccessResponseV2("Deleted temporary file.") } @@ -165,26 +166,26 @@ case class IIIFServiceSipiImpl( * * @param textFileRequest the request message. */ - def getTextFileRequest(textFileRequest: SipiGetTextFileRequest): Task[SipiGetTextFileResponse] = { + def getTextFileRequest(textFileRequest: SipiGetTextFileRequest): UIO[SipiGetTextFileResponse] = { // helper method to handle errors - def handleErrors(ex: Throwable): ZIO[Any, Exception with KnoraException with Product, Nothing] = ex match { + def handleErrors(ex: Throwable) = ex match { case notFoundException: NotFoundException => - ZIO.fail( + ZIO.die( NotFoundException( s"Unable to get file ${textFileRequest.fileUrl} from Sipi as requested by ${textFileRequest.senderName}: ${notFoundException.message}" ) ) case badRequestException: BadRequestException => - ZIO.fail( + ZIO.die( SipiException( s"Unable to get file ${textFileRequest.fileUrl} from Sipi as requested by ${textFileRequest.senderName}: ${badRequestException.message}" ) ) case sipiException: SipiException => - ZIO.fail( + ZIO.die( SipiException( s"Unable to get file ${textFileRequest.fileUrl} from Sipi as requested by ${textFileRequest.senderName}: ${sipiException.message}", sipiException.cause @@ -195,7 +196,7 @@ case class IIIFServiceSipiImpl( ZIO.logError( s"Unable to get file ${textFileRequest.fileUrl} from Sipi as requested by ${textFileRequest.senderName}: ${other.getMessage}" ) *> - ZIO.fail( + ZIO.die( SipiException( s"Unable to get file ${textFileRequest.fileUrl} from Sipi as requested by ${textFileRequest.senderName}: ${other.getMessage}" ) diff --git a/webapi/src/main/scala/org/knora/webapi/store/triplestore/errors/Errors.scala b/webapi/src/main/scala/org/knora/webapi/store/triplestore/errors/Errors.scala new file mode 100644 index 0000000000..a70644751a --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/triplestore/errors/Errors.scala @@ -0,0 +1,93 @@ +package org.knora.webapi.store.triplestore.errors + +import dsp.errors.InternalServerException +import com.typesafe.scalalogging.Logger +import dsp.errors.ExceptionUtil + +/** + * An abstract class for exceptions indicating that something went wrong with the triplestore. + * + * @param message a description of the error. + * @param cause the original exception representing the cause of the error, if any. + */ +abstract class TriplestoreException(message: String, cause: Option[Throwable] = None) + extends InternalServerException(message, cause) + +/** + * Indicates that the network connection to the triplestore failed. + * + * @param message a description of the error. + * @param cause the original exception representing the cause of the error, if any. + */ +final case class TriplestoreConnectionException(message: String, cause: Option[Throwable] = None) + extends TriplestoreException(message, cause) + +object TriplestoreConnectionException { + def apply(message: String, e: Throwable, log: Logger): TriplestoreConnectionException = + TriplestoreConnectionException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) +} + +/** + * Indicates that a read timeout occurred while waiting for data from the triplestore. + * + * @param message a description of the error. + * @param cause the original exception representing the cause of the error, if any. + */ +final case class TriplestoreTimeoutException(message: String, cause: Option[Throwable] = None) + extends TriplestoreException(message, cause) + +object TriplestoreTimeoutException { + def apply(message: String, e: Throwable, log: Logger): TriplestoreTimeoutException = + TriplestoreTimeoutException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) + + def apply(message: String, cause: Throwable): TriplestoreTimeoutException = + TriplestoreTimeoutException(message, Some(cause)) + + def apply(message: String): TriplestoreTimeoutException = + TriplestoreTimeoutException(message, None) +} + +/** + * Indicates that we tried using a feature which is unsuported by the selected triplestore. + * + * @param message a description of the error. + * @param cause the original exception representing the cause of the error, if any. + */ +final case class TriplestoreUnsupportedFeatureException(message: String, cause: Option[Throwable] = None) + extends TriplestoreException(message, cause) + +object TriplestoreUnsupportedFeatureException { + def apply(message: String, e: Throwable, log: Logger): TriplestoreUnsupportedFeatureException = + TriplestoreUnsupportedFeatureException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) +} + +/** + * Indicates that something inside the Triplestore package went wrong. More details can be given in the message parameter. + * + * @param message a description of the error. + * @param cause the original exception representing the cause of the error, if any. + */ +case class TriplestoreInternalException(message: String, cause: Option[Throwable] = None) + extends TriplestoreException(message, cause) + +object TriplestoreInternalException { + def apply(message: String, e: Throwable, log: Logger): TriplestoreInternalException = + TriplestoreInternalException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) +} + +/** + * Indicates that the triplestore returned an error message, or a response that could not be parsed. + * + * @param message a description of the error. + * @param cause the original exception representing the cause of the error, if any. + */ +final case class TriplestoreResponseException(message: String, cause: Option[Throwable] = None) + extends TriplestoreException(message, cause) + +object TriplestoreResponseException { + def apply(message: String, e: Throwable, log: Logger): TriplestoreResponseException = + TriplestoreResponseException(message, Some(ExceptionUtil.logAndWrapIfNotSerializable(e, log))) + + def apply(message: String): TriplestoreResponseException = + TriplestoreResponseException(message, None) +} diff --git a/webapi/src/main/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceHttpConnectorImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceHttpConnectorImpl.scala index effa86d789..940f135977 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceHttpConnectorImpl.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceHttpConnectorImpl.scala @@ -48,6 +48,7 @@ import org.knora.webapi.settings.KnoraDispatchers import org.knora.webapi.settings.KnoraSettings import org.knora.webapi.util.ActorUtil._ import org.knora.webapi.util.FileUtil +import org.knora.webapi.store.triplestore.errors._ import spray.json._ import java.io.BufferedInputStream @@ -878,32 +879,32 @@ case class TriplestoreServiceHttpConnectorImpl( } case e: Exception => { val message = s"Failed to connect to triplestore." - val error = TriplestoreConnectionException(message) + val error = TriplestoreConnectionException(message, Some(e)) ZIO.logError(error.toString()) *> ZIO.die(error) } } + .tap(_ => ZIO.logDebug(s"Executing Query: $request")) .orDie def checkResponse(response: CloseableHttpResponse, statusCode: Int): UIO[Unit] = - if (statusCode == 404) { - ZIO.die(NotFoundException("The requested data was not found")) - } else { - val statusCategory: Int = statusCode / 100 - if (statusCategory != 2) { + if (statusCode / 100 == 2) + ZIO.unit + else { + val entity = Option(response.getEntity) - .map(responseEntity => EntityUtils.toString(responseEntity, StandardCharsets.UTF_8)) match { - case Some(responseEntityStr) => - val msg = s"Triplestore responded with HTTP code $statusCode: $responseEntityStr" - if (statusCode == 503 && responseEntityStr.contains("Query timed out")) - ZIO.die(TriplestoreTimeoutException(msg)) - else - ZIO.die(TriplestoreResponseException(msg)) - case None => - ZIO.die(TriplestoreResponseException(s"Triplestore responded with HTTP code $statusCode")) - } - } else { - ZIO.unit + .map(responseEntity => EntityUtils.toString(responseEntity, StandardCharsets.UTF_8)) + + val statusResponseMsg = + s"Triplestore responded with HTTP code $statusCode" + + (statusCode, entity) match { + case (404, _) => ZIO.die(NotFoundException.notFound) + case (500, _) => ZIO.die(TriplestoreResponseException(statusResponseMsg)) + case (503, Some(response)) if response.contains("Query timed out") => + ZIO.die(TriplestoreTimeoutException(s"$statusResponseMsg: $response")) + case (503, _) => ZIO.die(TriplestoreResponseException(statusResponseMsg)) + case _ => ZIO.die(TriplestoreResponseException(statusResponseMsg)) } } @@ -915,10 +916,14 @@ case class TriplestoreServiceHttpConnectorImpl( (for { _ <- checkSimulateTimeout() // start <- ZIO.attempt(java.lang.System.currentTimeMillis()).orDie + _ <- ZIO.logDebug("Executing query...") response <- executeQuery() statusCode <- ZIO.attempt(response.getStatusLine.getStatusCode).orDie + _ <- ZIO.logDebug(s"Executing query done with status code: $statusCode") _ <- checkResponse(response, statusCode) + _ <- ZIO.logDebug("Checking response done.") result <- processResponse(response) + _ <- ZIO.logDebug("Processing response done.") _ <- ZIO.attempt(response.close()).orDie // TODO: rewrite with ensuring // _ <- logTimeTook(start, statusCode) } yield result) @@ -929,8 +934,12 @@ case class TriplestoreServiceHttpConnectorImpl( */ private def returnResponseAsString(response: CloseableHttpResponse): UIO[String] = Option(response.getEntity) match { - case None => ZIO.succeed("") - case Some(responseEntity) => ZIO.attempt(EntityUtils.toString(responseEntity, StandardCharsets.UTF_8)).orDie + case None => ZIO.succeed("") + case Some(responseEntity) => + ZIO + .attempt(EntityUtils.toString(responseEntity, StandardCharsets.UTF_8)) + .tapDefect(e => ZIO.logError(s"Failed to return response as string: $e")) + .orDie } /** diff --git a/webapi/src/main/scala/org/knora/webapi/util/ActorUtil.scala b/webapi/src/main/scala/org/knora/webapi/util/ActorUtil.scala index 72fd8b0011..92af858bde 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ActorUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ActorUtil.scala @@ -6,16 +6,18 @@ package org.knora.webapi.util import akka.actor.ActorRef -import com.typesafe.scalalogging.Logger import akka.http.scaladsl.util.FastFuture import akka.util.Timeout -import org.knora.webapi.config.AppConfig -import org.knora.webapi.core.Logging +import com.typesafe.scalalogging.Logger +import dsp.errors.BadRequestException import dsp.errors.ExceptionUtil import dsp.errors.RequestRejectedException import dsp.errors.UnexpectedMessageException -import zio._ +import org.knora.webapi.config.AppConfig +import org.knora.webapi.core.Logging +import zio.Cause._ import zio.Unsafe.unsafe +import zio._ import scala.concurrent.ExecutionContext import scala.concurrent.Future @@ -23,7 +25,7 @@ import scala.reflect.ClassTag import scala.util.Failure import scala.util.Success import scala.util.Try -import com.typesafe.scalalogging.Logger +import dsp.errors.NotFoundException object ActorUtil { @@ -48,10 +50,10 @@ object ActorUtil { * * Since this is the "edge" of the ZIO world for now, we need to log all errors that ZIO has potentially accumulated */ - def zio2Message[A](sender: ActorRef, zioTask: zio.Task[A], appConfig: AppConfig): Unit = + def zio2Message[A](sender: ActorRef, zioTask: zio.Task[A], appConfig: AppConfig, log: Logger): Unit = Unsafe.unsafe { implicit u => runtime.unsafe.run( - zioTask.foldCauseZIO(cause => handleCause(cause, sender), success => ZIO.succeed(sender ! success)) + zioTask.foldCause(cause => handleCause(cause, sender, log), success => sender ! success) ) } @@ -63,32 +65,24 @@ object ActorUtil { * @param cause the failures and defects that need to be handled. * @param sender the actor that made the request in the `ask` pattern. */ - def handleCause(cause: Cause[Throwable], sender: ActorRef): ZIO[Any, Nothing, Unit] = - cause.failureOrCause match { - case Left(rejectedEx: RequestRejectedException) => { - // The error was the client's fault. Log the exception, and also - // let the client know. - ZIO.logDebug(s"This error is presumably the clients fault: $rejectedEx") *> - ZIO.succeed(sender ! akka.actor.Status.Failure(rejectedEx)) - } - - case Left(otherEx: Exception) => { - // The error wasn't the client's fault. Log the exception, and also - // let the client know. - ZIO.logError(s"This error is presumably NOT the clients fault: $otherEx") *> - ZIO.succeed(sender ! akka.actor.Status.Failure(otherEx)) - } - - case Left(otherThrowable: Throwable) => - // Don't try to recover from a Throwable that isn't an Exception. - ZIO.logError(s"Presumably something realy bad has happened: $otherThrowable") *> - ZIO.succeed(sender ! akka.actor.Status.Failure(otherThrowable)) - - case Right(otherCauses: Cause[Nothing]) => - // Now we are getting all non-recoverable defects, which we need to squash before - // sending it back to the requesting actor. - ZIO.logErrorCause(otherCauses) *> - ZIO.succeed(sender ! akka.actor.Status.Failure(otherCauses.squashTrace)) + def handleCause(cause: Cause[Throwable], sender: ActorRef, log: Logger): Unit = + cause match { + case Fail(value, trace) => + value match { + case notFoundEx: NotFoundException => + log.info(s"This error is presumably the clients fault: $notFoundEx") + sender ! akka.actor.Status.Failure(notFoundEx) + } + case Die(value, trace) => + value match { + case rejectedEx: RequestRejectedException => + log.info(s"This error is presumably the clients fault: $rejectedEx") + sender ! akka.actor.Status.Failure(rejectedEx) + case otherEx => + log.error(s"This error is presumably NOT the clients fault: $otherEx") + sender ! akka.actor.Status.Failure(otherEx) + } + case other => log.error(s"handleCause() expects a ZIO.Die, but got $other") } /** diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala index 9583d6dbec..2f1b6e9740 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala @@ -14,7 +14,6 @@ import org.knora.webapi._ import dsp.errors.AssertionException import dsp.errors.InvalidApiJsonException import dsp.errors.NotFoundException -import dsp.errors.TriplestoreResponseException import org.knora.webapi.http.directives.DSPApiDirectives import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.store.triplestoremessages._ @@ -28,6 +27,7 @@ import org.knora.webapi.routing.v2.ResourcesRouteV2 import org.knora.webapi.sharedtestdata.SharedOntologyTestDataADM._ import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM._ +import org.knora.webapi.store.triplestore.errors.TriplestoreResponseException import org.knora.webapi.util.AkkaHttpUtils import org.knora.webapi.util.MutableTestIri import org.scalatest.Assertion diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala index d68bfa3f3b..fc8324fa6d 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala @@ -32,6 +32,7 @@ import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.store.cache.CacheServiceManager import org.knora.webapi.store.cache.impl.CacheServiceInMemImpl import org.knora.webapi.store.iiif.IIIFServiceManager +import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.store.iiif.impl.IIIFServiceMockImpl import org.knora.webapi.util.MutableTestIri import zio.& @@ -4204,7 +4205,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { apiRequestID = UUID.randomUUID ) - expectMsgPF(timeout) { case msg: akka.actor.Status.Failure => + expectMsgPF(3.seconds) { case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[ForbiddenException]) } } diff --git a/webapi/src/test/scala/org/knora/webapi/store/iiif/impl/IIIFServiceMockImpl.scala b/webapi/src/test/scala/org/knora/webapi/store/iiif/impl/IIIFServiceMockImpl.scala index 92a94c2f93..2778d3676b 100644 --- a/webapi/src/test/scala/org/knora/webapi/store/iiif/impl/IIIFServiceMockImpl.scala +++ b/webapi/src/test/scala/org/knora/webapi/store/iiif/impl/IIIFServiceMockImpl.scala @@ -5,10 +5,10 @@ package org.knora.webapi.store.iiif.impl -import dsp.errors.SipiException import org.knora.webapi.messages.store.sipimessages._ import org.knora.webapi.messages.v2.responder.SuccessResponseV2 import org.knora.webapi.store.iiif.api.IIIFService +import org.knora.webapi.store.iiif.errors.SipiException import zio._ /** @@ -22,7 +22,7 @@ case class IIIFServiceMockImpl() extends IIIFService { */ private val FAILURE_FILENAME: String = "failure.jp2" - def getFileMetadata(getFileMetadataRequestV2: GetFileMetadataRequest): Task[GetFileMetadataResponse] = + def getFileMetadata(getFileMetadataRequestV2: GetFileMetadataRequest): UIO[GetFileMetadataResponse] = ZIO.succeed( GetFileMetadataResponse( originalFilename = Some("test2.tiff"), @@ -38,23 +38,23 @@ case class IIIFServiceMockImpl() extends IIIFService { def moveTemporaryFileToPermanentStorage( moveTemporaryFileToPermanentStorageRequestV2: MoveTemporaryFileToPermanentStorageRequest - ): Task[SuccessResponseV2] = + ): UIO[SuccessResponseV2] = if (moveTemporaryFileToPermanentStorageRequestV2.internalFilename == FAILURE_FILENAME) { - ZIO.fail(SipiException("Sipi failed to move file to permanent storage")) + ZIO.die(SipiException("Sipi failed to move file to permanent storage")) } else { ZIO.succeed(SuccessResponseV2("Moved file to permanent storage")) } - def deleteTemporaryFile(deleteTemporaryFileRequestV2: DeleteTemporaryFileRequest): Task[SuccessResponseV2] = + def deleteTemporaryFile(deleteTemporaryFileRequestV2: DeleteTemporaryFileRequest): UIO[SuccessResponseV2] = if (deleteTemporaryFileRequestV2.internalFilename == FAILURE_FILENAME) { - ZIO.fail(SipiException("Sipi failed to delete temporary file")) + ZIO.die(SipiException("Sipi failed to delete temporary file")) } else { ZIO.succeed(SuccessResponseV2("Deleted temporary file")) } - override def getTextFileRequest(textFileRequest: SipiGetTextFileRequest): Task[SipiGetTextFileResponse] = ??? + override def getTextFileRequest(textFileRequest: SipiGetTextFileRequest): UIO[SipiGetTextFileResponse] = ??? - override def getStatus(): Task[IIIFServiceStatusResponse] = ZIO.succeed(IIIFServiceStatusOK) + override def getStatus(): UIO[IIIFServiceStatusResponse] = ZIO.succeed(IIIFServiceStatusOK) } object IIIFServiceMockImpl { diff --git a/webapi/src/test/scala/org/knora/webapi/store/triplestore/TriplestoreServiceManagerSpec.scala b/webapi/src/test/scala/org/knora/webapi/store/triplestore/TriplestoreServiceManagerSpec.scala index f55d88dec7..212a91b49e 100644 --- a/webapi/src/test/scala/org/knora/webapi/store/triplestore/TriplestoreServiceManagerSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/store/triplestore/TriplestoreServiceManagerSpec.scala @@ -7,7 +7,6 @@ package org.knora.webapi.store.triplestore import akka.testkit.ImplicitSender import org.knora.webapi.CoreSpec -import dsp.errors.TriplestoreTimeoutException import org.knora.webapi.messages.store.triplestoremessages.SimulateTimeoutRequest import scala.concurrent.duration._ @@ -27,6 +26,7 @@ import org.knora.webapi.messages.store.triplestoremessages.InsertGraphDataConten import org.knora.webapi.messages.store.triplestoremessages.InsertGraphDataContentResponse import org.knora.webapi.messages.store.triplestoremessages.NamedGraphDataRequest import org.knora.webapi.messages.store.triplestoremessages.NamedGraphDataResponse +import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException class TriplestoreServiceManagerSpec extends CoreSpec() with ImplicitSender { diff --git a/webapi/src/test/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceHttpConnectorImplZSpec.scala b/webapi/src/test/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceHttpConnectorImplZSpec.scala index 682550f24e..81726006b2 100644 --- a/webapi/src/test/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceHttpConnectorImplZSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceHttpConnectorImplZSpec.scala @@ -8,7 +8,6 @@ package org.knora.webapi.store.triplestore.impl import akka.http.javadsl.server.AuthenticationFailedRejection import org.knora.webapi.config.AppConfig import org.knora.webapi.config.AppConfigForTestContainers -import dsp.errors.TriplestoreTimeoutException import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM @@ -18,10 +17,13 @@ import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.store.cache.api.CacheService import org.knora.webapi.store.cache.impl.CacheServiceInMemImpl import org.knora.webapi.store.triplestore.api.TriplestoreService +import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException import org.knora.webapi.testcontainers.FusekiTestContainer import zio._ import zio.test.Assertion._ import zio.test._ +import org.knora.webapi.store.triplestore.errors.TriplestoreResponseException +import com.github.dockerjava.api.exception.NotFoundException /** * This spec is used to test [[org.knora.webapi.store.triplestore.impl.TriplestoreServiceHttpConnectorImpl]]. @@ -39,15 +41,32 @@ object TriplestoreServiceHttpConnectorImplZSpec extends ZIOSpecDefault { FusekiTestContainer.layer ) - def spec = suite("TriplestoreServiceHttpConnectorImplSpec")( - test("successfully simulate a timeout") { - for { - result <- TriplestoreService.doSimulateTimeout().exit - } yield assertTrue( - result.is(_.die) == TriplestoreTimeoutException( - "The triplestore took too long to process a request. This can happen because the triplestore needed too much time to search through the data that is currently in the triplestore. Query optimisation may help." + def spec = + suite("TriplestoreServiceHttpConnectorImplSpec")( + test("successfully simulate a timeout") { + for { + result <- TriplestoreService.doSimulateTimeout().exit + } yield assertTrue( + result.is(_.die) == TriplestoreTimeoutException( + "The triplestore took too long to process a request. This can happen because the triplestore needed too much time to search through the data that is currently in the triplestore. Query optimisation may help." + ) ) - ) - } - ).provideLayer(testLayer) @@ TestAspect.sequential + } + + test("successfully call a request that triggers a TriplestoreResponseException") { + + val searchStringOfDeath = + """ + PREFIX knora-base: PREFIX rdfs: SELECT DISTINCT ?resource (GROUP_CONCAT(IF(BOUND(?valueObject), STR(?valueObject), ""); separator="") AS ?valueObjectConcat) WHERE { { SELECT DISTINCT ?matchingSubject WHERE { ?matchingSubject 'fiche_CLSR AND GR AND MS AND 6 AND E129b_[Le AND sommeil AND et AND la AND mort…]*' . } } OPTIONAL { ?matchingSubject a ?valueObjectType . ?valueObjectType rdfs:subClassOf *knora-base:Value . FILTER(?valueObjectType != knora-base:LinkValue && ?valueObjectType != knora-base:ListValue) ?containingResource ?property ?matchingSubject . ?property rdfs:subPropertyOf* knora-base:hasValue . FILTER NOT EXISTS { ?matchingSubject knora-base:isDeleted true } # this variable will only be bound if the search matched a value object BIND(?matchingSubject AS ?valueObject) } OPTIONAL { # get all list nodes that match the search term ?matchingSubject a knora-base:ListNode . # get sub-node(s) of that node(s) (recursively) ?matchingSubject knora-base:hasSubListNode* ?subListNode . # get all values that point to the node(s) and sub-node(s) ?listValue knora-base:valueHasListNode ?subListNode . # get all resources that have that values ?subjectWithListValue ?predicate ?listValue . FILTER NOT EXISTS { ?matchingSubject knora-base:isDeleted true } # this variable will only be bound if the search matched a list node BIND(?listValue AS ?valueObject) } # If the first OPTIONAL clause was executed, ?matchingSubject is a value object, and ?containingResource will be set as ?valueObject. # If the second OPTIONAL clause was executed, ?matchingSubject is a list node, and ?listValue will be set as ?valueObject. # Otherwise, ?matchingSubject is a resource (its rdfs:label matched the search pattern). BIND( COALESCE( ?containingResource, ?subjectWithListValue, ?matchingSubject) AS ?resource) ?resource a ?resourceClass . ?resourceClass rdfs:subClassOf* knora-base:Resource . FILTER NOT EXISTS { ?resource knora-base:isDeleted true . } } GROUP BY ?resource ORDER BY ?resource OFFSET 0 LIMIT 25 + """ + + for { + // TODO: Need to first load testdata. Only then this query should trigger a 500 error in Fuseki. + // _ <- TriplestoreService.sparqlHttpSelect(searchStringOfDeath, false).exit.repeatN(100) + // _ <- Clock.ClockLive.sleep(10.seconds) + result <- TriplestoreService.sparqlHttpSelect(searchStringOfDeath, false).exit + } yield assert(result)( + diesWithA[dsp.errors.NotFoundException] + ) + } + ).provideLayer(testLayer) @@ TestAspect.sequential } diff --git a/webapi/src/test/scala/org/knora/webapi/testservices/TestClientService.scala b/webapi/src/test/scala/org/knora/webapi/testservices/TestClientService.scala index 2fc7a40b6f..dc092bbdfb 100644 --- a/webapi/src/test/scala/org/knora/webapi/testservices/TestClientService.scala +++ b/webapi/src/test/scala/org/knora/webapi/testservices/TestClientService.scala @@ -20,7 +20,6 @@ import org.knora.webapi.config.AppConfig import dsp.errors.AssertionException import dsp.errors.BadRequestException import dsp.errors.NotFoundException -import dsp.errors.SipiException import org.knora.webapi.messages.store.sipimessages.SipiUploadResponse import org.knora.webapi.messages.store.sipimessages.SipiUploadResponseJsonProtocol._ import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject @@ -30,6 +29,7 @@ import org.knora.webapi.messages.util.rdf.JsonLDUtil import org.knora.webapi.settings.KnoraDispatchers import org.knora.webapi.settings.KnoraSettings import org.knora.webapi.settings.KnoraSettingsImpl +import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.util.SipiUtil import spray.json.JsObject import spray.json._