From 7497023c3cc117d08c3d5eaae807ac53451dae64 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Thu, 25 Feb 2021 09:45:21 +0100 Subject: [PATCH] feat: Add support for audio files (DSP-1343) (#1818) --- knora-ontologies/knora-base.ttl | 6 +- sipi/scripts/file_info.lua | 9 +- test_data/test_route/files/minimal.wav | Bin 0 -> 44 bytes test_data/test_route/files/test.wav | Bin 0 -> 176080 bytes webapi/src/main/resources/application.conf | 4 +- .../store/sipimessages/SipiMessages.scala | 6 +- .../util/ConstructResponseUtilV2.scala | 13 ++ .../webapi/messages/util/ValueUtilV1.scala | 63 ++++++- .../valuemessages/ValueMessagesV1.scala | 57 ++++++ .../valuemessages/ValueMessagesV2.scala | 110 +++++++++++ .../main/scala/org/knora/webapi/package.scala | 2 +- .../knora/webapi/routing/RouteUtilV1.scala | 25 +++ .../knora/webapi/settings/KnoraSettings.scala | 18 ++ .../webapi/store/iiif/SipiConnector.scala | 9 +- .../upgrade/RepositoryUpdatePlan.scala | 3 +- .../sparql/v1/addValueVersion.scala.txt | 29 +++ ...teInsertStatementsForCreateValue.scala.txt | 29 +++ ...eInsertStatementsForValueContent.scala.txt | 11 ++ .../it/v1/KnoraSipiIntegrationV1ITSpec.scala | 97 ++++++++++ .../it/v2/KnoraSipiIntegrationV2ITSpec.scala | 172 +++++++++++++++++- .../webapi/store/iiif/MockSipiConnector.scala | 3 +- 21 files changed, 640 insertions(+), 26 deletions(-) create mode 100644 test_data/test_route/files/minimal.wav create mode 100644 test_data/test_route/files/test.wav diff --git a/knora-ontologies/knora-base.ttl b/knora-ontologies/knora-base.ttl index 2ad4b5fc01..2d5c77c4d6 100644 --- a/knora-ontologies/knora-base.ttl +++ b/knora-ontologies/knora-base.ttl @@ -33,7 +33,7 @@ :attachedToProject knora-admin:SystemProject ; - :ontologyVersion "knora-base v10" . + :ontologyVersion "knora-base v11" . @@ -1674,7 +1674,7 @@ rdfs:subClassOf :FileValue , [ rdf:type owl:Restriction ; owl:onProperty :duration ; - owl:cardinality "1"^^xsd:nonNegativeInteger + owl:maxCardinality "1"^^xsd:nonNegativeInteger ] ; rdfs:comment "Represents an audio file"@en . @@ -2163,7 +2163,7 @@ ] , [ rdf:type owl:Restriction ; owl:onProperty :duration ; - owl:cardinality "1"^^xsd:nonNegativeInteger + owl:maxCardinality "1"^^xsd:nonNegativeInteger ] ; rdfs:comment "Represents a moving image file"@en . diff --git a/sipi/scripts/file_info.lua b/sipi/scripts/file_info.lua index 069590bc74..82c83e832b 100644 --- a/sipi/scripts/file_info.lua +++ b/sipi/scripts/file_info.lua @@ -23,6 +23,7 @@ require "util" TEXT = "text" IMAGE = "image" DOCUMENT = "document" +AUDIO = "audio" ------------------------------------------------------------------------------- -- Mimetype constants @@ -37,7 +38,9 @@ local TEXT_XML = "text/xml" local TEXT_PLAIN = "text/plain" local AUDIO_MP3 = "audio/mpeg" local AUDIO_MP4 = "audio/mp4" -local AUDIO_WAV = "audio/x-wav" +local AUDIO_WAV = "audio/wav" +local AUDIO_X_WAV = "audio/x-wav" +local AUDIO_VND_WAVE = "audio/vnd.wave" local APPLICATION_PDF = "application/pdf" local APPLICATION_DOC = "application/msword" local APPLICATION_DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" @@ -61,7 +64,9 @@ local image_mime_types = { local audio_mime_types = { AUDIO_MP3, AUDIO_MP4, - AUDIO_WAV + AUDIO_WAV, + AUDIO_X_WAV, + AUDIO_VND_WAVE } local text_mime_types = { diff --git a/test_data/test_route/files/minimal.wav b/test_data/test_route/files/minimal.wav new file mode 100644 index 0000000000000000000000000000000000000000..8dbde9545c9f3bc0f0edf6804a28471e5c5cc00f GIT binary patch literal 44 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 J3_kpk{|mR0{}2EG literal 0 HcmV?d00001 diff --git a/webapi/src/main/resources/application.conf b/webapi/src/main/resources/application.conf index 749d1dfb0d..151af9735e 100644 --- a/webapi/src/main/resources/application.conf +++ b/webapi/src/main/resources/application.conf @@ -398,8 +398,8 @@ app { "application/x-iso9660-image", ] text-mime-types = ["application/xml", "text/xml", "text/csv", "text/plain"] - movie-mime-types = [] - sound-mime-types = ["audio/mpeg", "audio/mp4", "audio/x-wav", "audio/vnd.wav"] + video-mime-types = [] + audio-mime-types = ["audio/mpeg", "audio/mp4", "audio/wav", "audio/x-wav", "audio/vnd.wave"] } ark { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/store/sipimessages/SipiMessages.scala b/webapi/src/main/scala/org/knora/webapi/messages/store/sipimessages/SipiMessages.scala index 7d6a86c7d0..e7b820760b 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/store/sipimessages/SipiMessages.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/store/sipimessages/SipiMessages.scala @@ -51,14 +51,16 @@ case class GetFileMetadataRequest(fileUrl: String, requestingUser: UserADM) exte * @param internalMimeType the file's internal MIME type. Always defined (https://dasch.myjetbrains.com/youtrack/issue/DSP-711). * @param width the file's width in pixels, if applicable. * @param height the file's height in pixels, if applicable. - * @param pageCount the number of pages in the file, if applicable. + * @param pageCount the number of pages in the file, if applicable. + * @param duration the duration of the file in seconds, if applicable. */ case class GetFileMetadataResponse(originalFilename: Option[String], originalMimeType: Option[String], internalMimeType: String, width: Option[Int], height: Option[Int], - pageCount: Option[Int]) + pageCount: Option[Int], + duration: Option[BigDecimal]) /** * Asks Sipi to move a file from temporary to permanent storage. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala index 60a05d5224..8e3827130f 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala @@ -1087,6 +1087,19 @@ object ConstructResponseUtilV2 { fileValue = fileValue, comment = valueCommentOption )) + + case OntologyConstants.KnoraBase.AudioFileValue => + FastFuture.successful( + AudioFileValueContentV2( + ontologySchema = InternalSchema, + fileValue = fileValue, + duration = valueObject + .maybeStringObject(OntologyConstants.KnoraBase.Duration.toSmartIri) + .map(definedDuration => BigDecimal(definedDuration)), + comment = valueCommentOption + )) + + case _ => throw InconsistentRepositoryDataException(s"Unexpected file value type: $valueType") } } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/ValueUtilV1.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/ValueUtilV1.scala index fb2314c443..1727445b35 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/ValueUtilV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/ValueUtilV1.scala @@ -88,6 +88,8 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { makeStillImageValue(valueProps, projectShortcode, responderManager, userProfile) case OntologyConstants.KnoraBase.TextFileValue => makeTextFileValue(valueProps, projectShortcode, responderManager, userProfile) + case OntologyConstants.KnoraBase.AudioFileValue => + makeAudioFileValue(valueProps, projectShortcode, responderManager, userProfile) case OntologyConstants.KnoraBase.DocumentFileValue => makeDocumentFileValue(valueProps, projectShortcode, responderManager, userProfile) case OntologyConstants.KnoraBase.LinkValue => makeLinkValue(valueProps, responderManager, userProfile) @@ -122,16 +124,20 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { * Creates a URL for accessing a text file via Sipi. * * @param textFileValue the text file value representing the text file. - * @param external a flag denoting the type of URL that should be generated. * @return a Sipi URL. */ - def makeSipiTextFileGetUrlFromFilename(textFileValue: TextFileValueV1, external: Boolean = true): String = { + def makeSipiTextFileGetUrlFromFilename(textFileValue: TextFileValueV1): String = { + s"${settings.externalSipiBaseUrl}/${textFileValue.projectShortcode}/${textFileValue.internalFilename}" + } - if (external) { - s"${settings.externalSipiBaseUrl}/${textFileValue.projectShortcode}/${textFileValue.internalFilename}" - } else { - s"${settings.internalSipiBaseUrl}/${textFileValue.projectShortcode}/${textFileValue.internalFilename}" - } + /** + * Creates a URL for accessing an audio file via Sipi. + * + * @param audioFileValue the file value representing the audio file. + * @return a Sipi URL. + */ + def makeSipiAudioFileGetUrlFromFilename(audioFileValue: AudioFileValueV1): String = { + s"${settings.externalSipiIIIFGetUrl}/${audioFileValue.projectShortcode}/${audioFileValue.internalFilename}/file" } // A Map of MIME types to Knora API v1 binary format name. @@ -158,9 +164,11 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { "text/csv" -> "CSV", "application/zip" -> "ZIP", "application/x-compressed-zip" -> "ZIP", - "audio/x-wav" -> "AUDIO", + "audio/mpeg" -> "AUDIO", "audio/mp4" -> "AUDIO", - "audio/mpeg" -> "AUDIO" + "audio/wav" -> "AUDIO", + "audio/x-wav" -> "AUDIO", + "audio/vnd.wave" -> "AUDIO" ), { key: String => s"Unknown MIME type: $key" } @@ -199,6 +207,14 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { origname = textFileValue.originalFilename, path = makeSipiTextFileGetUrlFromFilename(textFileValue) ) + + case audioFileValue: AudioFileValueV1 => + LocationV1( + format_name = mimeType2V1Format(audioFileValue.internalMimeType), + origname = audioFileValue.originalFilename, + path = makeSipiAudioFileGetUrlFromFilename(audioFileValue) + ) + case otherType => throw NotImplementedException(s"Type not yet implemented: ${otherType.valueTypeIri}") } } @@ -364,12 +380,14 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { case _: LinkV1 => basicObjectResponse - case _: StillImageFileValueV1 => basicObjectResponse // TODO: implement this. + case _: StillImageFileValueV1 => basicObjectResponse case _: TextFileValueV1 => basicObjectResponse case _: DocumentFileValueV1 => basicObjectResponse + case _: AudioFileValueV1 => basicObjectResponse + case _: HierarchicalListValueV1 => basicObjectResponse case _: ColorValueV1 => basicObjectResponse @@ -856,6 +874,31 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { )) } + /** + * Converts a [[ValueProps]] into a [[AudioFileValueV1]]. + * + * @param valueProps a [[ValueProps]] representing the SPARQL query results to be converted. + * @return a [[DocumentFileValueV1]]. + */ + private def makeAudioFileValue( + valueProps: ValueProps, + projectShortcode: String, + responderManager: ActorRef, + userProfile: UserADM)(implicit timeout: Timeout, executionContext: ExecutionContext): Future[ApiValueV1] = { + val predicates = valueProps.literalData + + Future( + AudioFileValueV1( + internalMimeType = predicates(OntologyConstants.KnoraBase.InternalMimeType).literals.head, + internalFilename = predicates(OntologyConstants.KnoraBase.InternalFilename).literals.head, + originalFilename = predicates.get(OntologyConstants.KnoraBase.OriginalFilename).map(_.literals.head), + projectShortcode = projectShortcode, + duration = predicates + .get(OntologyConstants.KnoraBase.Duration) + .map(valueLiterals => BigDecimal(valueLiterals.literals.head)) + )) + } + /** * Converts a [[ValueProps]] into a [[LinkValueV1]]. * diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v1/responder/valuemessages/ValueMessagesV1.scala b/webapi/src/main/scala/org/knora/webapi/messages/v1/responder/valuemessages/ValueMessagesV1.scala index a76241569c..ce76c7a403 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v1/responder/valuemessages/ValueMessagesV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v1/responder/valuemessages/ValueMessagesV1.scala @@ -1638,6 +1638,62 @@ case class DocumentFileValueV1(internalMimeType: String, } } +case class AudioFileValueV1(internalMimeType: String, + internalFilename: String, + originalFilename: Option[String], + originalMimeType: Option[String] = None, + projectShortcode: String, + duration: Option[BigDecimal] = None) + extends FileValueV1 { + + def valueTypeIri: IRI = OntologyConstants.KnoraBase.AudioFileValue + + def toJsValue: JsValue = ApiValueV1JsonProtocol.audioFileValueV1Format.write(this) + + override def toString: String = internalFilename + + /** + * Checks if a new moving image file value would duplicate an existing moving image file value. + * + * @param other another [[ValueV1]]. + * @return `true` if `other` is a duplicate of `this`. + */ + override def isDuplicateOfOtherValue(other: ApiValueV1): Boolean = { + other match { + case audioFileValueV1: AudioFileValueV1 => audioFileValueV1 == this + case otherValue => + throw InconsistentRepositoryDataException(s"Cannot compare a $valueTypeIri to a ${otherValue.valueTypeIri}") + } + } + + /** + * Checks if a new version of a moving image file value would be redundant given the current version of the value. + * + * @param currentVersion the current version of the value. + * @return `true` if this [[UpdateValueV1]] is redundant given `currentVersion`. + */ + override def isRedundant(currentVersion: ApiValueV1): Boolean = { + currentVersion match { + case audioFileValueV1: AudioFileValueV1 => audioFileValueV1 == this + case other => + throw InconsistentRepositoryDataException(s"Cannot compare a $valueTypeIri to a ${other.valueTypeIri}") + } + } + + override def toFileValueContentV2: FileValueContentV2 = { + AudioFileValueContentV2( + ontologySchema = InternalSchema, + fileValue = FileValueV2( + internalFilename = internalFilename, + internalMimeType = internalMimeType, + originalFilename = originalFilename, + originalMimeType = Some(internalMimeType) + ), + duration = duration + ) + } +} + case class MovingImageFileValueV1(internalMimeType: String, internalFilename: String, originalFilename: Option[String], @@ -1822,6 +1878,7 @@ object ApiValueV1JsonProtocol extends SprayJsonSupport with DefaultJsonProtocol implicit val stillImageFileValueV1Format: JsonFormat[StillImageFileValueV1] = jsonFormat7(StillImageFileValueV1) implicit val documentFileValueV1Format: JsonFormat[DocumentFileValueV1] = jsonFormat8(DocumentFileValueV1) implicit val textFileValueV1Format: JsonFormat[TextFileValueV1] = jsonFormat5(TextFileValueV1) + implicit val audioFileValueV1Format: JsonFormat[AudioFileValueV1] = jsonFormat6(AudioFileValueV1) implicit val movingImageFileValueV1Format: JsonFormat[MovingImageFileValueV1] = jsonFormat5(MovingImageFileValueV1) implicit val valueVersionV1Format: JsonFormat[ValueVersionV1] = jsonFormat3(ValueVersionV1) implicit val linkValueV1Format: JsonFormat[LinkValueV1] = jsonFormat4(LinkValueV1) 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 f9bac2f921..8455cec6d7 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 @@ -1337,6 +1337,17 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { log = log ) + case OntologyConstants.KnoraApiV2Complex.AudioFileValue => + AudioFileValueContentV2.fromJsonLDObject( + jsonLDObject = jsonLDObject, + requestingUser = requestingUser, + responderManager = responderManager, + storeManager = storeManager, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + log = log + ) + case other => throw NotImplementedException(s"Parsing of JSON-LD value type not implemented: $other") } @@ -3453,6 +3464,105 @@ object TextFileValueContentV2 extends ValueContentReaderV2[TextFileValueContentV } } +/** + * Represents audio file metadata. + * + * @param fileValue the basic metadata about the file value. + * @param duration the duration of the audio file in seconds. + * @param comment a comment on this [[AudioFileValueContentV2]], if any. + */ +case class AudioFileValueContentV2(ontologySchema: OntologySchema, + fileValue: FileValueV2, + duration: Option[BigDecimal] = None, + comment: Option[String] = None) + extends FileValueContentV2 { + override def valueType: SmartIri = { + implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + OntologyConstants.KnoraBase.AudioFileValue.toSmartIri.toOntologySchema(ontologySchema) + } + + override def valueHasString: String = fileValue.internalFilename + + override def toOntologySchema(targetSchema: OntologySchema): AudioFileValueContentV2 = + copy(ontologySchema = targetSchema) + + override def toJsonLDValue(targetSchema: ApiV2Schema, + projectADM: ProjectADM, + settings: KnoraSettingsImpl, + schemaOptions: Set[SchemaOption]): JsonLDValue = { + val fileUrl: String = s"${settings.externalSipiBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/file" + + targetSchema match { + case ApiV2Simple => toJsonLDValueInSimpleSchema(fileUrl) + + case ApiV2Complex => + JsonLDObject(toJsonLDObjectMapInComplexSchema(fileUrl)) + } + } + + override def unescape: ValueContentV2 = { + copy(comment = comment.map(commentStr => stringFormatter.fromSparqlEncodedString(commentStr))) + } + + override def wouldDuplicateOtherValue(that: ValueContentV2): Boolean = { + that match { + case thatAudioFile: AudioFileValueContentV2 => + fileValue == thatAudioFile.fileValue + + case _ => throw AssertionException(s"Can't compare a <$valueType> to a <${that.valueType}>") + } + } + + override def wouldDuplicateCurrentVersion(currentVersion: ValueContentV2): Boolean = { + currentVersion match { + case thatAudioFile: AudioFileValueContentV2 => + fileValue == thatAudioFile.fileValue && + comment == thatAudioFile.comment + + case _ => throw AssertionException(s"Can't compare a <$valueType> to a <${currentVersion.valueType}>") + } + } +} + +/** + * Constructs [[AudioFileValueContentV2]] objects based on JSON-LD input. + */ +object AudioFileValueContentV2 extends ValueContentReaderV2[AudioFileValueContentV2] { + override def fromJsonLDObject(jsonLDObject: JsonLDObject, + requestingUser: UserADM, + responderManager: ActorRef, + storeManager: ActorRef, + featureFactoryConfig: FeatureFactoryConfig, + settings: KnoraSettingsImpl, + log: LoggingAdapter)( + implicit timeout: Timeout, + executionContext: ExecutionContext): Future[AudioFileValueContentV2] = { + implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + + for { + fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLDObject( + jsonLDObject = jsonLDObject, + requestingUser = requestingUser, + responderManager = responderManager, + storeManager = storeManager, + settings = settings, + log = log + ) + + _ = if (!settings.audioMimeTypes.contains(fileValueWithSipiMetadata.fileValue.internalMimeType)) { + throw BadRequestException( + s"File ${fileValueWithSipiMetadata.fileValue.internalFilename} has MIME type ${fileValueWithSipiMetadata.fileValue.internalMimeType}, which is not supported for audio files") + } + } yield + AudioFileValueContentV2( + ontologySchema = ApiV2Complex, + fileValue = fileValueWithSipiMetadata.fileValue, + duration = fileValueWithSipiMetadata.sipiFileMetadata.duration, + comment = getComment(jsonLDObject) + ) + } +} + /** * Represents a Knora link value. * diff --git a/webapi/src/main/scala/org/knora/webapi/package.scala b/webapi/src/main/scala/org/knora/webapi/package.scala index 0396ad51b2..2822a71c89 100644 --- a/webapi/src/main/scala/org/knora/webapi/package.scala +++ b/webapi/src/main/scala/org/knora/webapi/package.scala @@ -25,7 +25,7 @@ package object webapi { * The version of `knora-base` and of the other built-in ontologies that this version of Knora requires. * Must be the same as the object of `knora-base:ontologyVersion` in the `knora-base` ontology being used. */ - val KnoraBaseVersion: String = "knora-base v10" + val KnoraBaseVersion: String = "knora-base v11" /** * `IRI` is a synonym for `String`, used to improve code readability. 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 c5ca3ae6ee..3cb226b073 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV1.scala @@ -34,6 +34,7 @@ import org.knora.webapi.messages.store.sipimessages.GetFileMetadataResponse import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2 import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2.TextWithStandoffTagsV2 import org.knora.webapi.messages.v1.responder.valuemessages.{ + AudioFileValueV1, DocumentFileValueV1, FileValueV1, StillImageFileValueV1, @@ -279,6 +280,21 @@ object RouteUtilV1 { "application/gzip" ) + /** + * MIME types used in Sipi to store audio files. + */ + private val audioMimeTypes: Set[String] = Set( + "application/xml", + "text/xml", + "text/csv", + "text/plain", + "audio/mpeg", + "audio/mp4", + "audio/wav", + "audio/x-wav", + "audio/vnd.wave" + ) + /** * Converts file metadata from Sipi into a [[FileValueV1]]. * @@ -320,6 +336,15 @@ object RouteUtilV1 { dimX = fileMetadataResponse.width, dimY = fileMetadataResponse.height ) + } else if (audioMimeTypes.contains(fileMetadataResponse.internalMimeType)) { + AudioFileValueV1( + internalFilename = filename, + internalMimeType = fileMetadataResponse.internalMimeType, + originalFilename = fileMetadataResponse.originalFilename, + originalMimeType = fileMetadataResponse.originalMimeType, + projectShortcode = projectShortcode, + duration = fileMetadataResponse.duration + ) } else { throw BadRequestException(s"MIME type ${fileMetadataResponse.internalMimeType} not supported in Knora API v1") } diff --git a/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala b/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala index da9fed938a..1f20055863 100644 --- a/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala +++ b/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala @@ -125,6 +125,24 @@ class KnoraSettingsImpl(config: Config, log: LoggingAdapter) extends Extension { } .toSet + val audioMimeTypes: Set[String] = config + .getList("app.sipi.audio-mime-types") + .iterator + .asScala + .map { mType: ConfigValue => + mType.unwrapped.toString + } + .toSet + + val videoMimeTypes: Set[String] = config + .getList("app.sipi.video-mime-types") + .iterator + .asScala + .map { mType: ConfigValue => + mType.unwrapped.toString + } + .toSet + val internalSipiProtocol: String = config.getString("app.sipi.internal-protocol") val internalSipiHost: String = config.getString("app.sipi.internal-host") val internalSipiPort: Int = config.getInt("app.sipi.internal-port") diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/SipiConnector.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/SipiConnector.scala index 62eadd17fb..65b67d6274 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/SipiConnector.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/SipiConnector.scala @@ -91,13 +91,15 @@ class SipiConnector extends Actor with ActorLogging { * @param width the file's width in pixels, if applicable. * @param height the file's height in pixels, if applicable. * @param numpages the number of pages in the file, if applicable. + * @param duration the duration of the file in seconds, if applicable. */ case class SipiKnoraJsonResponse(originalFilename: Option[String], originalMimeType: Option[String], internalMimeType: String, width: Option[Int], height: Option[Int], - numpages: Option[Int]) { + numpages: Option[Int], + duration: Option[BigDecimal]) { if (originalFilename.contains("")) { throw SipiException(s"Sipi returned an empty originalFilename") } @@ -108,7 +110,7 @@ class SipiConnector extends Actor with ActorLogging { } object SipiKnoraJsonResponseProtocol extends SprayJsonSupport with DefaultJsonProtocol { - implicit val sipiKnoraJsonResponseFormat: RootJsonFormat[SipiKnoraJsonResponse] = jsonFormat6(SipiKnoraJsonResponse) + implicit val sipiKnoraJsonResponseFormat: RootJsonFormat[SipiKnoraJsonResponse] = jsonFormat7(SipiKnoraJsonResponse) } /** @@ -133,7 +135,8 @@ class SipiConnector extends Actor with ActorLogging { internalMimeType = sipiResponse.internalMimeType, width = sipiResponse.width, height = sipiResponse.height, - pageCount = sipiResponse.numpages + pageCount = sipiResponse.numpages, + duration = sipiResponse.duration ) } diff --git a/webapi/src/main/scala/org/knora/webapi/store/triplestore/upgrade/RepositoryUpdatePlan.scala b/webapi/src/main/scala/org/knora/webapi/store/triplestore/upgrade/RepositoryUpdatePlan.scala index a174206d47..ded46d9f26 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/triplestore/upgrade/RepositoryUpdatePlan.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/triplestore/upgrade/RepositoryUpdatePlan.scala @@ -33,7 +33,8 @@ object RepositoryUpdatePlan { PluginForKnoraBaseVersion(versionNumber = 7, plugin = new NoopPlugin), // PR 1403 PluginForKnoraBaseVersion(versionNumber = 8, plugin = new UpgradePluginPR1615(featureFactoryConfig)), PluginForKnoraBaseVersion(versionNumber = 9, plugin = new UpgradePluginPR1746(featureFactoryConfig, log)), - PluginForKnoraBaseVersion(versionNumber = 10, plugin = new NoopPlugin) // PR 1808 + PluginForKnoraBaseVersion(versionNumber = 10, plugin = new NoopPlugin), // PR 1808 + PluginForKnoraBaseVersion(versionNumber = 11, plugin = new NoopPlugin) // PR 1813 ) /** diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v1/addValueVersion.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v1/addValueVersion.scala.txt index 0b71321e46..39e0eeebcc 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v1/addValueVersion.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v1/addValueVersion.scala.txt @@ -265,6 +265,35 @@ DELETE { } } + case audioFileValue: AudioFileValueV1 => { + ?newValue knora-base:internalFilename """@audioFileValue.internalFilename""" ; + knora-base:internalMimeType """@audioFileValue.internalMimeType""" . + + @audioFileValue.originalFilename match { + case Some(definedOriginalFilename) => { + ?newValue knora-base:originalFilename """@definedOriginalFilename""" . + } + + case None => {} + } + + @audioFileValue.originalMimeType match { + case Some(definedOriginalMimeType) => { + ?newValue knora-base:originalMimeType """@definedOriginalMimeType""" . + } + + case None => {} + } + + @audioFileValue.duration match { + case Some(definedDuration) => { + ?newValue knora-base:duration """@definedDuration""" . + } + + case None => {} + } + } + case documentFileValue: DocumentFileValueV1 => { ?newValue knora-base:internalFilename """@documentFileValue.internalFilename""" . ?newValue knora-base:internalMimeType """@documentFileValue.internalMimeType""" . diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v1/generateInsertStatementsForCreateValue.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v1/generateInsertStatementsForCreateValue.scala.txt index e610b088c1..a4fe7a5be3 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v1/generateInsertStatementsForCreateValue.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v1/generateInsertStatementsForCreateValue.scala.txt @@ -221,6 +221,35 @@ } } + case audioFileValue: AudioFileValueV1 => { + <@newValueIri> knora-base:internalFilename """@audioFileValue.internalFilename""" ; + knora-base:internalMimeType """@audioFileValue.internalMimeType""" . + + @audioFileValue.originalFilename match { + case Some(definedOriginalFilename) => { + <@newValueIri> knora-base:originalFilename """@definedOriginalFilename""" . + } + + case None => {} + } + + @audioFileValue.originalMimeType match { + case Some(definedOriginalMimeType) => { + <@newValueIri> knora-base:originalMimeType """@definedOriginalMimeType""" . + } + + case None => {} + } + + @audioFileValue.duration match { + case Some(definedDuration) => { + <@newValueIri> knora-base:duration """@definedDuration""" . + } + + case None => {} + } + } + case documentFileValue: DocumentFileValueV1 => { <@newValueIri> knora-base:internalFilename """@documentFileValue.internalFilename""" ; knora-base:internalMimeType """@documentFileValue.internalMimeType""" . diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/generateInsertStatementsForValueContent.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/generateInsertStatementsForValueContent.scala.txt index 0c9ae4cc50..296e5f6d3a 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/generateInsertStatementsForValueContent.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/generateInsertStatementsForValueContent.scala.txt @@ -205,6 +205,17 @@ } } + case audioFileValue: AudioFileValueContentV2 => { + @audioFileValue.duration match { + case Some(definedDuration) => { + <@newValueIri> knora-base:duration @definedDuration . + } + + case None => {} + } + + } + case _ => {} } } diff --git a/webapi/src/test/scala/org/knora/webapi/it/v1/KnoraSipiIntegrationV1ITSpec.scala b/webapi/src/test/scala/org/knora/webapi/it/v1/KnoraSipiIntegrationV1ITSpec.scala index 513d069bf2..d4d68000e5 100644 --- a/webapi/src/test/scala/org/knora/webapi/it/v1/KnoraSipiIntegrationV1ITSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/it/v1/KnoraSipiIntegrationV1ITSpec.scala @@ -80,6 +80,7 @@ class KnoraSipiIntegrationV1ITSpec private val pdfResourceIri = new MutableTestIri private val zipResourceIri = new MutableTestIri + private val wavResourceIri = new MutableTestIri private val minimalPdfOriginalFilename = "minimal.pdf" private val pathToMinimalPdf = s"test_data/test_route/files/$minimalPdfOriginalFilename" @@ -97,6 +98,12 @@ class KnoraSipiIntegrationV1ITSpec private val testZipOriginalFilename = "test.zip" private val pathToTestZip = s"test_data/test_route/files/$testZipOriginalFilename" + private val minimalWavOriginalFilename = "minimal.wav" + private val pathToMinimalWav = s"test_data/test_route/files/$minimalWavOriginalFilename" + + private val testWavOriginalFilename = "test.wav" + private val pathToTestWav = s"test_data/test_route/files/$testWavOriginalFilename" + /** * Adds the IRI of a XSL transformation to the given mapping. * @@ -906,5 +913,95 @@ class KnoraSipiIntegrationV1ITSpec val sipiGetRequest = Get(zipUrl) ~> addCredentials(BasicHttpCredentials(userEmail, password)) checkResponseOK(sipiGetRequest) } + + "create a resource with a WAV file attached" in { + // Upload the WAV file to Sipi. + val zipUploadResponse: SipiUploadResponse = uploadToSipi( + loginToken = loginToken, + filesToUpload = Seq(FileToUpload(path = pathToMinimalWav, mimeType = MediaTypes.`audio/wav`)) + ) + + val uploadedWavFile: SipiUploadResponseEntry = zipUploadResponse.uploadedFiles.head + uploadedWavFile.originalFilename should ===(minimalWavOriginalFilename) + + // Create a resource for the WAV file. + val createAudioResourceParams = JsObject( + Map( + "restype_id" -> JsString("http://www.knora.org/ontology/knora-base#AudioRepresentation"), + "label" -> JsString("Wav file"), + "project_id" -> JsString("http://rdfh.ch/projects/0001"), + "properties" -> JsObject(), + "file" -> JsString(uploadedWavFile.internalFilename) + ) + ) + + // Send the JSON in a POST request to the Knora API server. + val createAudioResourceRequest: HttpRequest = Post( + baseApiUrl + "/v1/resources", + HttpEntity(ContentTypes.`application/json`, createAudioResourceParams.compactPrint)) ~> addCredentials( + BasicHttpCredentials(userEmail, password)) + + val createAudioResourceResponseJson: JsObject = getResponseJson(createAudioResourceRequest) + + // get the IRI of the audio file resource + val resourceIri: String = createAudioResourceResponseJson.fields.get("res_id") match { + case Some(JsString(res_id: String)) => res_id + case _ => throw InvalidApiJsonException("member 'res_id' was expected") + } + + wavResourceIri.set(resourceIri) + + // Request the audio file resource from the Knora API server. + val audioResourceRequest = Get(baseApiUrl + "/v1/resources/" + URLEncoder.encode(resourceIri, "UTF-8")) ~> addCredentials( + BasicHttpCredentials(userEmail, password)) + + val audioResourceResponse: JsObject = getResponseJson(audioResourceRequest) + val locdata = audioResourceResponse.fields("resinfo").asJsObject.fields("locdata").asJsObject + val zipUrl = + locdata.fields("path").asInstanceOf[JsString].value.replace("http://0.0.0.0:1024", baseInternalSipiUrl) + + // Request the file from Sipi. + val sipiGetRequest = Get(zipUrl) ~> addCredentials(BasicHttpCredentials(userEmail, password)) + checkResponseOK(sipiGetRequest) + } + + "change the WAV file attached to a resource" in { + // Upload the file to Sipi. + val sipiUploadResponse: SipiUploadResponse = uploadToSipi( + loginToken = loginToken, + filesToUpload = Seq(FileToUpload(path = pathToTestWav, mimeType = MediaTypes.`audio/wav`)) + ) + + val uploadedFile: SipiUploadResponseEntry = sipiUploadResponse.uploadedFiles.head + uploadedFile.originalFilename should ===(testWavOriginalFilename) + + // JSON describing the new file to Knora. + val knoraParams = JsObject( + Map( + "file" -> JsString(s"${uploadedFile.internalFilename}") + ) + ) + + // Send the JSON in a PUT request to the Knora API server. + val knoraPutRequest = Put( + baseApiUrl + "/v1/filevalue/" + URLEncoder.encode(wavResourceIri.get, "UTF-8"), + HttpEntity(ContentTypes.`application/json`, knoraParams.compactPrint)) ~> addCredentials( + BasicHttpCredentials(userEmail, password)) + + checkResponseOK(knoraPutRequest) + + // Request the document resource from the Knora API server. + val audioResourceRequest = Get(baseApiUrl + "/v1/resources/" + URLEncoder.encode(wavResourceIri.get, "UTF-8")) ~> addCredentials( + BasicHttpCredentials(userEmail, password)) + + val audioResourceResponse: JsObject = getResponseJson(audioResourceRequest) + val locdata = audioResourceResponse.fields("resinfo").asJsObject.fields("locdata").asJsObject + val wavUrl = + locdata.fields("path").asInstanceOf[JsString].value.replace("http://0.0.0.0:1024", baseInternalSipiUrl) + + // Request the file from Sipi. + val sipiGetRequest = Get(wavUrl) ~> addCredentials(BasicHttpCredentials(userEmail, password)) + checkResponseOK(sipiGetRequest) + } } } diff --git a/webapi/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala b/webapi/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala index 29829d5ddc..9bf94ce68e 100644 --- a/webapi/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala @@ -69,6 +69,8 @@ class KnoraSipiIntegrationV2ITSpec private val csvValueIri = new MutableTestIri private val zipResourceIri = new MutableTestIri private val zipValueIri = new MutableTestIri + private val wavResourceIri = new MutableTestIri + private val wavValueIri = new MutableTestIri private val marblesOriginalFilename = "marbles.tif" private val pathToMarbles = s"test_data/test_route/images/$marblesOriginalFilename" @@ -110,6 +112,13 @@ class KnoraSipiIntegrationV2ITSpec private val testZipOriginalFilename = "test.zip" private val pathToTestZip = s"test_data/test_route/files/$testZipOriginalFilename" + private val minimalWavOriginalFilename = "minimal.wav" + private val pathToMinimalWav = s"test_data/test_route/files/$minimalWavOriginalFilename" + private val minimalWavDuration = BigDecimal("0.0") + + private val testWavOriginalFilename = "test.wav" + private val pathToTestWav = s"test_data/test_route/files/$testWavOriginalFilename" + /** * Represents the information that Knora returns about an image file value that was created. * @@ -138,11 +147,20 @@ class KnoraSipiIntegrationV2ITSpec /** * Represents the information that Knora returns about a text file value that was created. * - * @param internalFilename the files's internal filename. + * @param internalFilename the file's internal filename. * @param url the file's URL. */ case class SavedTextFile(internalFilename: String, url: String) + /** + * Represents the information that Knora returns about an audio file value that was created. + * + * @param internalFilename the file's internal filename. + * @param url the file's URL. + * @param duration the duration of the audio in seconds. + */ + case class SavedAudioFile(internalFilename: String, url: String, duration: Option[BigDecimal]) + /** * Given a JSON-LD document representing a resource, returns a JSON-LD array containing the values of the specified * property. @@ -265,6 +283,34 @@ class KnoraSipiIntegrationV2ITSpec ) } + /** + * Given a JSON-LD object representing a Knora text file value, returns a [[SavedTextFile]] containing the same information. + * + * @param savedValue a JSON-LD object representing a Knora document file value. + * @return a [[SavedTextFile]] containing the same information. + */ + private def savedValueToSavedAudioFile(savedValue: JsonLDObject): SavedAudioFile = { + val internalFilename = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.FileValueHasFilename) + + val url: String = savedValue.requireDatatypeValueInObject( + key = OntologyConstants.KnoraApiV2Complex.FileValueAsUrl, + expectedDatatype = OntologyConstants.Xsd.Uri.toSmartIri, + validationFun = stringFormatter.toSparqlEncodedString + ) + + val duration: Option[BigDecimal] = savedValue.maybeDatatypeValueInObject( + key = OntologyConstants.KnoraApiV2Complex.AudioFileValueHasDuration, + expectedDatatype = OntologyConstants.Xsd.Decimal.toSmartIri, + validationFun = stringFormatter.validateBigDecimal + ) + + SavedAudioFile( + internalFilename = internalFilename, + url = url, + duration = duration + ) + } + "The Knora/Sipi integration" should { var loginToken: String = "" @@ -1035,5 +1081,129 @@ class KnoraSipiIntegrationV2ITSpec val sipiGetFileRequest = Get(savedDocument.url.replace("http://0.0.0.0:1024", baseInternalSipiUrl)) checkResponseOK(sipiGetFileRequest) } + + "create a resource with a WAV file" in { + // Upload the file to Sipi. + val sipiUploadResponse: SipiUploadResponse = uploadToSipi( + loginToken = loginToken, + filesToUpload = Seq(FileToUpload(path = pathToMinimalWav, mimeType = MediaTypes.`audio/wav`)) + ) + + val uploadedFile: SipiUploadResponseEntry = sipiUploadResponse.uploadedFiles.head + uploadedFile.originalFilename should ===(minimalWavOriginalFilename) + + // Ask Knora to create the resource. + + val jsonLdEntity = + s"""{ + | "@type" : "knora-api:AudioRepresentation", + | "knora-api:hasAudioFileValue" : { + | "@type" : "knora-api:AudioFileValue", + | "knora-api:fileValueHasFilename" : "${uploadedFile.internalFilename}" + | }, + | "knora-api:attachedToProject" : { + | "@id" : "http://rdfh.ch/projects/0001" + | }, + | "rdfs:label" : "test audio representation", + | "@context" : { + | "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + | "xsd" : "http://www.w3.org/2001/XMLSchema#" + | } + |}""".stripMargin + + val request = Post(s"$baseApiUrl/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials( + BasicHttpCredentials(anythingUserEmail, password)) + val responseJsonDoc: JsonLDDocument = getResponseJsonLD(request) + wavResourceIri.set(responseJsonDoc.body.requireIDAsKnoraDataIri.toString) + + // Get the resource from Knora. + val knoraGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(wavResourceIri.get, "UTF-8")}") + val resource: JsonLDDocument = getResponseJsonLD(knoraGetRequest) + assert( + resource.requireTypeAsKnoraTypeIri.toString == "http://api.knora.org/ontology/knora-api/v2#AudioRepresentation") + + // Get the new file value from the resource. + + val savedValues: JsonLDArray = getValuesFromResource( + resource = resource, + propertyIriInResult = OntologyConstants.KnoraApiV2Complex.HasAudioFileValue.toSmartIri + ) + + val savedValue: JsonLDValue = if (savedValues.value.size == 1) { + savedValues.value.head + } else { + throw AssertionException(s"Expected one file value, got ${savedValues.value.size}") + } + + val savedValueObj: JsonLDObject = savedValue match { + case jsonLDObject: JsonLDObject => jsonLDObject + case other => throw AssertionException(s"Invalid value object: $other") + } + + wavValueIri.set(savedValueObj.requireIDAsKnoraDataIri.toString) + + val savedAudioFile: SavedAudioFile = savedValueToSavedAudioFile(savedValueObj) + assert(savedAudioFile.internalFilename == uploadedFile.internalFilename) + assert(savedAudioFile.duration.forall(_ == minimalWavDuration)) + + // Request the permanently stored file from Sipi. + val sipiGetFileRequest = Get(savedAudioFile.url.replace("http://0.0.0.0:1024", baseInternalSipiUrl)) + checkResponseOK(sipiGetFileRequest) + } + + "change a WAV file value" in { + // Upload the file to Sipi. + val sipiUploadResponse: SipiUploadResponse = uploadToSipi( + loginToken = loginToken, + filesToUpload = Seq(FileToUpload(path = pathToTestWav, mimeType = MediaTypes.`audio/wav`)) + ) + + val uploadedFile: SipiUploadResponseEntry = sipiUploadResponse.uploadedFiles.head + uploadedFile.originalFilename should ===(testWavOriginalFilename) + + // Ask Knora to update the value. + + val jsonLdEntity = + s"""{ + | "@id" : "${wavResourceIri.get}", + | "@type" : "knora-api:AudioRepresentation", + | "knora-api:hasAudioFileValue" : { + | "@type" : "knora-api:AudioFileValue", + | "@id" : "${wavValueIri.get}", + | "knora-api:fileValueHasFilename" : "${uploadedFile.internalFilename}" + | }, + | "@context" : { + | "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + | "xsd" : "http://www.w3.org/2001/XMLSchema#" + | } + |}""".stripMargin + + val request = Put(s"$baseApiUrl/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials( + BasicHttpCredentials(anythingUserEmail, password)) + val responseJsonDoc: JsonLDDocument = getResponseJsonLD(request) + wavValueIri.set(responseJsonDoc.body.requireIDAsKnoraDataIri.toString) + + // Get the resource from Knora. + val knoraGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(wavResourceIri.get, "UTF-8")}") + val resource = getResponseJsonLD(knoraGetRequest) + + // Get the new file value from the resource. + val savedValue: JsonLDObject = getValueFromResource( + resource = resource, + propertyIriInResult = OntologyConstants.KnoraApiV2Complex.HasAudioFileValue.toSmartIri, + expectedValueIri = wavValueIri.get + ) + + val savedAudioFile: SavedAudioFile = savedValueToSavedAudioFile(savedValue) + assert(savedAudioFile.internalFilename == uploadedFile.internalFilename) + + // Request the permanently stored file from Sipi. + val sipiGetFileRequest = Get(savedAudioFile.url.replace("http://0.0.0.0:1024", baseInternalSipiUrl)) + checkResponseOK(sipiGetFileRequest) + } } } diff --git a/webapi/src/test/scala/org/knora/webapi/store/iiif/MockSipiConnector.scala b/webapi/src/test/scala/org/knora/webapi/store/iiif/MockSipiConnector.scala index 77e34427a9..3315defd95 100644 --- a/webapi/src/test/scala/org/knora/webapi/store/iiif/MockSipiConnector.scala +++ b/webapi/src/test/scala/org/knora/webapi/store/iiif/MockSipiConnector.scala @@ -70,7 +70,8 @@ class MockSipiConnector extends Actor with ActorLogging { internalMimeType = "image/jp2", width = Some(512), height = Some(256), - pageCount = None + pageCount = None, + duration = None ) }