diff --git a/it/src/main/scala/org/mbari/oni/etc/jdk/Strings.scala b/it-postgres/src/test/scala/org/mbari/oni/endpoints/PostgresMediaEndpointsSuite.scala similarity index 63% rename from it/src/main/scala/org/mbari/oni/etc/jdk/Strings.scala rename to it-postgres/src/test/scala/org/mbari/oni/endpoints/PostgresMediaEndpointsSuite.scala index 12984be..9aaca72 100644 --- a/it/src/main/scala/org/mbari/oni/etc/jdk/Strings.scala +++ b/it-postgres/src/test/scala/org/mbari/oni/endpoints/PostgresMediaEndpointsSuite.scala @@ -14,15 +14,10 @@ * limitations under the License. */ -package org.mbari.oni.etc.jdk +package org.mbari.oni.endpoints -import scala.util.Random +import org.mbari.oni.PostgresMixin -object Strings: +class PostgresMediaEndpointsSuite extends MediaEndpointsSuite with PostgresMixin { - private val chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - private val random = new Random - - def random(length: Int): String = - val xs = for (_ <- 0 until length) yield chars.charAt(random.nextInt(chars.length)) - new String(xs.toArray) +} diff --git a/it-postgres/src/test/scala/org/mbari/oni/services/PostgresMediaServiceSuite.scala b/it-postgres/src/test/scala/org/mbari/oni/services/PostgresMediaServiceSuite.scala new file mode 100644 index 0000000..1644979 --- /dev/null +++ b/it-postgres/src/test/scala/org/mbari/oni/services/PostgresMediaServiceSuite.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Monterey Bay Aquarium Research Institute + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mbari.oni.services + +import org.mbari.oni.PostgresMixin + +class PostgresMediaServiceSuite extends MediaServiceSuite with PostgresMixin { + +} diff --git a/it-sqlserver/src/test/scala/org/mbari/oni/endpoints/SqlServerMediaEndpointsSuite.scala b/it-sqlserver/src/test/scala/org/mbari/oni/endpoints/SqlServerMediaEndpointsSuite.scala new file mode 100644 index 0000000..4c20711 --- /dev/null +++ b/it-sqlserver/src/test/scala/org/mbari/oni/endpoints/SqlServerMediaEndpointsSuite.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Monterey Bay Aquarium Research Institute + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mbari.oni.endpoints + +import org.mbari.oni.SqlServerMixin + +class SqlServerMediaEndpointsSuite extends MediaEndpointsSuite with SqlServerMixin { + +} diff --git a/it/src/main/scala/org/mbari/oni/endpoints/MediaEndpointsSuite.scala b/it/src/main/scala/org/mbari/oni/endpoints/MediaEndpointsSuite.scala new file mode 100644 index 0000000..1868c53 --- /dev/null +++ b/it/src/main/scala/org/mbari/oni/endpoints/MediaEndpointsSuite.scala @@ -0,0 +1,158 @@ +/* + * Copyright 2024 Monterey Bay Aquarium Research Institute + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mbari.oni.endpoints + +import org.mbari.oni.domain.{Media, MediaCreate, MediaTypes, MediaUpdate} +import org.mbari.oni.etc.jdk.Strings +import org.mbari.oni.etc.jwt.JwtService +import org.mbari.oni.jdbc.FastPhylogenyService +import org.mbari.oni.jpa.DataInitializer +import org.mbari.oni.services.{ConceptService, UserAuthMixin} +import sttp.model.StatusCode +import org.mbari.oni.etc.circe.CirceCodecs.{*, given} + +import java.net.URI +import scala.jdk.CollectionConverters.* + +trait MediaEndpointsSuite extends EndpointsSuite with DataInitializer with UserAuthMixin { + + given jwtService: JwtService = JwtService("mbari", "foo", "bar") + lazy val fastPhylogenyService = FastPhylogenyService(entityManagerFactory) + lazy val endpoints: MediaEndpoints = MediaEndpoints(entityManagerFactory, fastPhylogenyService, conceptService) + private val password = Strings.random(10) + + def createMedia(): Seq[Media] = { + val root = init(1, 6) + root.getDescendants + .asScala + .flatMap(_.getConceptMetadata.getMedias.asScala) + .toSeq + .map(Media.from) + .sortBy(_.url.toExternalForm) + } + + test("mediaForConcept") { + val expected = createMedia() + val opt = expected.head.conceptName + assert(opt.isDefined) + val name = opt.get + runGet( + endpoints.mediaForConceptEndpointImpl, + s"http://test.com/v1/media/${name}", + response => + assertEquals(response.code, StatusCode.Ok) + val obtained = checkResponse[Seq[Media]](response.body) + .sortBy(_.url.toExternalForm) + assertEquals(obtained, expected) + ) + } + + test("createMedia") { + val root = init(2, 0) + assert(root != null) + val mediaCreate = MediaCreate( + conceptName = root.getName, + url = URI.create(s"http://www.mbari.org/${Strings.random(10)}.png").toURL, + caption = Some(Strings.random(1000)), + credit = Some(Strings.random(255)), + mediaType = Some(MediaTypes.IMAGE.name), + isPrimary = Some(true) + ) + val attempt = testWithUserAuth( + user => + runPost( + endpoints.createMediaEndpointImpl, + "http://test.com/v1/media", + mediaCreate.stringify, + response => + assertEquals(response.code, StatusCode.Ok) + val obtained = checkResponse[Media](response.body) + assertEquals(mediaCreate.conceptName, obtained.conceptName.getOrElse("")) + assertEquals(mediaCreate.url, obtained.url) + assertEquals(mediaCreate.caption, obtained.caption) + assertEquals(mediaCreate.credit, obtained.credit) + val t = Media.resolveMimeType(mediaCreate.mediaType.getOrElse(""), obtained.url.toExternalForm) + assertEquals(t, obtained.mimeType) + assertEquals(mediaCreate.isPrimary.getOrElse(false), obtained.isPrimary) + , + jwt = jwtService.login(user.username, password, user.toEntity) + ), + password + ) + attempt match + case Left(value) => fail(value.toString) + case Right(value) => assert(true) + } + + test("updateMedia") { + val media = createMedia().head + val mediaUpdate = MediaUpdate( + url = Some(URI.create(s"http://www.mbari.org/${Strings.random(10)}.png").toURL), + caption = Some(Strings.random(1000)), + credit = Some(Strings.random(255)), + mediaType = Some(MediaTypes.IMAGE.name), + isPrimary = Some(true) + ) + val attempt = testWithUserAuth( + user => + runPut( + endpoints.updateMediaEndpointImpl, + s"http://test.com/v1/media/${media.id.get}", + mediaUpdate.stringify, + response => + assertEquals(response.code, StatusCode.Ok) + val obtained = checkResponse[Media](response.body) + assertEquals(mediaUpdate.url.orNull, obtained.url) + assertEquals(mediaUpdate.caption, obtained.caption) + assertEquals(mediaUpdate.credit, obtained.credit) + val t = Media.resolveMimeType(mediaUpdate.mediaType.getOrElse(""), obtained.url.toExternalForm) + assertEquals(t, obtained.mimeType) + assertEquals(mediaUpdate.isPrimary.getOrElse(false), obtained.isPrimary) + , + jwt = jwtService.login(user.username, password, user.toEntity) + ), + password + ) + attempt match + case Left(value) => fail(value.toString) + case Right(value) => assert(true) + } + + test("deleteMedia") { + val media = createMedia().head + val attempt = testWithUserAuth( + user => + runDelete( + endpoints.deleteMediaEndpointImpl, + s"http://test.com/v1/media/${media.id.get}", + response => assertEquals(response.code, StatusCode.Ok), + jwt = jwtService.login(user.username, password, user.toEntity) + ), + password + ) + attempt match + case Left(value) => fail(value.toString) + case Right(value) => assert(true) + } + + + + + + + +} diff --git a/it/src/main/scala/org/mbari/oni/services/MediaServiceSuite.scala b/it/src/main/scala/org/mbari/oni/services/MediaServiceSuite.scala index ea30df1..397e7bc 100644 --- a/it/src/main/scala/org/mbari/oni/services/MediaServiceSuite.scala +++ b/it/src/main/scala/org/mbari/oni/services/MediaServiceSuite.scala @@ -16,9 +16,11 @@ package org.mbari.oni.services -import org.mbari.oni.domain.MediaCreate +import org.mbari.oni.domain.{Media, MediaCreate, MediaTypes, MediaUpdate} import org.mbari.oni.jdbc.FastPhylogenyService import org.mbari.oni.jpa.DataInitializer +import org.mbari.oni.etc.circe.CirceCodecs.{*, given} +import org.mbari.oni.etc.jdk.Strings import java.net.URI @@ -30,16 +32,118 @@ trait MediaServiceSuite extends DataInitializer with UserAuthMixin: test("create") { val root = init(2, 0) assert(root != null) - val a = conceptService.findByName(root.getName) val mediaCreate = MediaCreate( conceptName = root.getName, - url = URI.create("http://www.mbari.org").toURL + url = URI.create(s"http://www.mbari.org/${Strings.random(10)}.png").toURL, + caption = Some(Strings.random(1000)), + credit = Some(Strings.random(255)), + mediaType = Some(MediaTypes.IMAGE.name), + isPrimary = Some(true) ) -// mediaService.create(mediaCreate) match -// case Left(e) => fail(e.getMessage) -// case Right(media) => -// assertEquals(mediaCreate.conceptName, media.conceptName) -// assertEquals(mediaCreate.url, media.url) -// assertEquals(mediaCreate.user, media.user) -// assertEquals(mediaCreate.timestamp, media.timestamp) + val attempt = runWithUserAuth(user => mediaService.create(mediaCreate, user.username)) + + attempt match + case Left(e) => fail(e.getMessage) + case Right(media) => + assert(media.id.isDefined) + assertEquals(mediaCreate.conceptName, media.conceptName.getOrElse("")) + assertEquals(mediaCreate.url, media.url) + assertEquals(mediaCreate.caption, media.caption) + assertEquals(mediaCreate.credit, media.credit) + val t = Media.resolveMimeType(mediaCreate.mediaType.getOrElse(""), media.url.toExternalForm) + assertEquals(t, media.mimeType) + assertEquals(mediaCreate.isPrimary.getOrElse(false), media.isPrimary) + + } + + test("update") { + val root = init(2, 0) + assert(root != null) + val mediaCreate = MediaCreate( + conceptName = root.getName, + url = URI.create(s"http://www.mbari.org/${Strings.random(10)}.png").toURL, + caption = Some(Strings.random(1000)), + credit = Some(Strings.random(255)), + mediaType = Some(MediaTypes.IMAGE.name), + isPrimary = Some(true) + ) + val mediaUpdate = MediaUpdate( + url = Some(URI.create(s"http://www.mbari.org/${Strings.random(10)}.png").toURL), + caption = Some(Strings.random(1000)), + credit = Some(Strings.random(255)), + mediaType = Some(MediaTypes.IMAGE.name), + isPrimary = Some(true) + ) + + val attempt = runWithUserAuth(user => + for + m0 <- mediaService.create(mediaCreate, user.username) + m1 <- mediaService.update(m0.id.getOrElse(0L), mediaUpdate, user.username) + yield m1 + ) + + attempt match + case Left(e) => fail(e.getMessage) + case Right(media) => + assertEquals(mediaUpdate.url.orNull, media.url) + assertEquals(mediaUpdate.caption, media.caption) + assertEquals(mediaUpdate.credit, media.credit) + val t = Media.resolveMimeType(mediaUpdate.mediaType.getOrElse(""), media.url.toExternalForm) + assertEquals(t, media.mimeType) + assertEquals(mediaUpdate.isPrimary.getOrElse(false), media.isPrimary) + } + + test("delete") { + val root = init(2, 0) + assert(root != null) + val mediaCreate = MediaCreate( + conceptName = root.getName, + url = URI.create(s"http://www.mbari.org/${Strings.random(10)}.png").toURL, + caption = Some(Strings.random(1000)), + credit = Some(Strings.random(255)), + mediaType = Some(MediaTypes.IMAGE.name), + isPrimary = Some(true) + ) + val attempt = runWithUserAuth(user => mediaService.create(mediaCreate, user.username)) + + attempt match + case Left(e) => fail(e.getMessage) + case Right(media) => + val attempt = runWithUserAuth(user => mediaService.deleteById(media.id.getOrElse(0L), user.username)) + attempt match + case Left(e) => fail(e.getMessage) + case Right(_) => + mediaService.findById(media.id.getOrElse(0L)) match + case Left(e) => fail(e.getMessage) + case Right(opt) => assert(opt.isEmpty) + } + + test("findById") { + val root = init(2, 0) + assert(root != null) + val mediaCreate = MediaCreate( + conceptName = root.getName, + url = URI.create(s"http://www.mbari.org/${Strings.random(10)}.png").toURL, + caption = Some(Strings.random(1000)), + credit = Some(Strings.random(255)), + mediaType = Some(MediaTypes.IMAGE.name), + isPrimary = Some(true) + ) + val attempt0 = runWithUserAuth(user => mediaService.create(mediaCreate, user.username)) + + attempt0 match + case Left(e) => fail(e.getMessage) + case Right(media) => + val attempt1 = runWithUserAuth(user => mediaService.findById(media.id.getOrElse(0L))) + attempt1 match + case Left(e) => fail(e.getMessage) + case Right(opt) => + assert(opt.isDefined) + val m = opt.get + assertEquals(media.id, m.id) + assertEquals(media.url, m.url) + assertEquals(media.caption, m.caption) + assertEquals(media.credit, m.credit) + assertEquals(media.mimeType, m.mimeType) + assertEquals(media.isPrimary, m.isPrimary) } diff --git a/oni/src/main/scala/org/mbari/oni/domain/Media.scala b/oni/src/main/scala/org/mbari/oni/domain/Media.scala index 86e58bd..1ff93e5 100644 --- a/oni/src/main/scala/org/mbari/oni/domain/Media.scala +++ b/oni/src/main/scala/org/mbari/oni/domain/Media.scala @@ -7,6 +7,8 @@ package org.mbari.oni.domain +import org.mbari.oni.etc.jdk.Strings + import java.net.{URI, URL} import java.util.regex.Pattern import org.mbari.oni.jpa.entities.MediaEntity @@ -59,8 +61,9 @@ object Media: ) def resolveMimeType(t: String, url: String): String = - val ext = url.split(Pattern.quote(".")).last - Try(MediaType.valueOf(t)) match + val ext = url.split(Pattern.quote(".")).last.toLowerCase + val mediaType = Strings.initCap(t) + Try(MediaType.valueOf(mediaType)) match case Success(MediaType.Image) => s"image/$ext" case Success(MediaType.Video) => ext match diff --git a/oni/src/main/scala/org/mbari/oni/domain/MediaCreate.scala b/oni/src/main/scala/org/mbari/oni/domain/MediaCreate.scala index c0ec4b5..a60a974 100644 --- a/oni/src/main/scala/org/mbari/oni/domain/MediaCreate.scala +++ b/oni/src/main/scala/org/mbari/oni/domain/MediaCreate.scala @@ -16,7 +16,7 @@ case class MediaCreate( url: URL, caption: Option[String] = None, credit: Option[String] = None, - mimeType: Option[String] = None, + mediaType: Option[String] = Some(MediaTypes.IMAGE.name), isPrimary: Option[Boolean] = None ): @@ -25,6 +25,6 @@ case class MediaCreate( entity.setUrl(url.toExternalForm) entity.setCaption(caption.orNull) entity.setCredit(credit.orNull) - entity.setType(mimeType.orNull) + entity.setType(mediaType.getOrElse(MediaTypes.IMAGE.name)) entity.setPrimary(isPrimary.getOrElse(false)) entity diff --git a/oni/src/main/scala/org/mbari/oni/endpoints/Endpoints.scala b/oni/src/main/scala/org/mbari/oni/endpoints/Endpoints.scala index f7b6481..4e2443e 100644 --- a/oni/src/main/scala/org/mbari/oni/endpoints/Endpoints.scala +++ b/oni/src/main/scala/org/mbari/oni/endpoints/Endpoints.scala @@ -52,6 +52,8 @@ trait Endpoints: implicit lazy val sLinkCreate: Schema[LinkCreate] = Schema.derived[LinkCreate] implicit lazy val sLinkUpdate: Schema[LinkUpdate] = Schema.derived[LinkUpdate] implicit lazy val sMedia: Schema[Media] = Schema.derived[Media] + implicit lazy val sMediaCreate: Schema[MediaCreate] = Schema.derived[MediaCreate] + implicit lazy val sMediaUpdate: Schema[MediaUpdate] = Schema.derived[MediaUpdate] implicit lazy val sPaging: Schema[Paging] = Schema.derived[Paging] implicit lazy val sPrefNode: Schema[PrefNode] = Schema.derived[PrefNode] implicit lazy val sPrefNodeUpdate: Schema[PrefNodeUpdate] = Schema.derived[PrefNodeUpdate] diff --git a/oni/src/main/scala/org/mbari/oni/etc/circe/CirceCodecs.scala b/oni/src/main/scala/org/mbari/oni/etc/circe/CirceCodecs.scala index f74a461..4b5a46a 100644 --- a/oni/src/main/scala/org/mbari/oni/etc/circe/CirceCodecs.scala +++ b/oni/src/main/scala/org/mbari/oni/etc/circe/CirceCodecs.scala @@ -102,6 +102,12 @@ object CirceCodecs: given Decoder[Media] = deriveDecoder given Encoder[Media] = deriveEncoder + given Decoder[MediaCreate] = deriveDecoder + given Encoder[MediaCreate] = deriveEncoder + + given Decoder[MediaUpdate] = deriveDecoder + given Encoder[MediaUpdate] = deriveEncoder + given Decoder[ConceptMetadata] = deriveDecoder given Encoder[ConceptMetadata] = deriveEncoder diff --git a/oni/src/main/scala/org/mbari/oni/etc/jdk/Strings.scala b/oni/src/main/scala/org/mbari/oni/etc/jdk/Strings.scala new file mode 100644 index 0000000..122073f --- /dev/null +++ b/oni/src/main/scala/org/mbari/oni/etc/jdk/Strings.scala @@ -0,0 +1,28 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + +package org.mbari.oni.etc.jdk + +import scala.util.Random + +object Strings: + + private val chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + private val random = new Random + + def random(length: Int): String = + val xs = for (_ <- 0 until length) yield chars.charAt(random.nextInt(chars.length)) + new String(xs.toArray) + + /** + * Change case of a string to init Cap. That is the first letter is capitalized and the rest are lower case. + * @param s the string to convert + * @return the init cap version of the string + */ + def initCap(s: String): String = + val a = s.toLowerCase() + a.substring(0, 1).toUpperCase() + a.substring(1) \ No newline at end of file diff --git a/oni/src/main/scala/org/mbari/oni/services/MediaService.scala b/oni/src/main/scala/org/mbari/oni/services/MediaService.scala index e54368c..6f2518b 100644 --- a/oni/src/main/scala/org/mbari/oni/services/MediaService.scala +++ b/oni/src/main/scala/org/mbari/oni/services/MediaService.scala @@ -9,7 +9,7 @@ package org.mbari.oni.services import jakarta.persistence.{EntityManager, EntityManagerFactory} import org.mbari.oni.{ConceptNameNotFound, ItemNotFound} -import org.mbari.oni.domain.{Media, MediaCreate} +import org.mbari.oni.domain.{Media, MediaCreate, MediaUpdate} import org.mbari.oni.jpa.EntityManagerFactories.* import org.mbari.oni.etc.jdk.Loggers.given import org.mbari.oni.jdbc.FastPhylogenyService @@ -65,6 +65,7 @@ class MediaService(entityManagerFactory: EntityManagerFactory, fastPhylogenyServ val entity = mediaCreate.toEntity concept.getConceptMetadata.addMedia(entity) + repo.create(entity) // Add history val history = HistoryEntityFactory.add(userEntity, entity) concept.getConceptMetadata.addHistory(history) @@ -94,6 +95,7 @@ class MediaService(entityManagerFactory: EntityManagerFactory, fastPhylogenyServ val conceptMetadata = media.getConceptMetadata conceptMetadata.removeMedia(media) repo.delete(media) + ) for @@ -101,7 +103,25 @@ class MediaService(entityManagerFactory: EntityManagerFactory, fastPhylogenyServ media <- txn(user.toEntity) yield () - def update() = ??? + def update(id: Long, mediaUpdate: MediaUpdate, userName: String) = + def txn(userEntity: UserAccountEntity): Either[Throwable, Media] = + entityManagerFactory.transaction(entityManager => + val repo = MediaRepository(entityManager, fastPhylogenyService) + repo.findByPrimaryKey(classOf[MediaEntity], id).toScala match + case None => throw ItemNotFound(s"Media with id '${id}' not found") + case Some(media) => + mediaUpdate.caption.foreach(media.setCaption) + mediaUpdate.credit.foreach(media.setCredit) + mediaUpdate.isPrimary.foreach(b => media.setPrimary(b.booleanValue())) + mediaUpdate.url.foreach(url => media.setUrl(url.toExternalForm)) + mediaUpdate.mediaType.foreach(media.setType) + Media.from(media) + ) + + for + user <- userAccountService.verifyWriteAccess(Option(userName)) + media <- txn(user.toEntity) + yield media def inTxnRejectAdd( history: HistoryEntity, diff --git a/oni/src/test/scala/org/mbari/oni/domain/MediaSuite.scala b/oni/src/test/scala/org/mbari/oni/domain/MediaSuite.scala new file mode 100644 index 0000000..774f57e --- /dev/null +++ b/oni/src/test/scala/org/mbari/oni/domain/MediaSuite.scala @@ -0,0 +1,30 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + +package org.mbari.oni.domain + +class MediaSuite extends munit.FunSuite { + + test("resolveMimeType (image)") { + val a = Media.resolveMimeType("image", "http://foo.com/bar.jpg") + assertEquals(a, "image/jpg") + + val b = Media.resolveMimeType("IMAGE", "http://foo.com/bax/bar.PNG") + assertEquals(b, "image/png") + } + + test("resolveMimeType (video)") { + val a = Media.resolveMimeType("video", "http://foo.com/bar.mov") + assertEquals(a, "video/quicktime") + + val b = Media.resolveMimeType("VIDEO", "http://foo.com/bax/bar.mp4") + assertEquals(b, "video/mp4") + } + + + +}