diff --git a/src/main/scala/org/mbari/vars/annotation/api/v1/ImageV1Api.scala b/src/main/scala/org/mbari/vars/annotation/api/v1/ImageV1Api.scala index d8205a01..006b43fb 100644 --- a/src/main/scala/org/mbari/vars/annotation/api/v1/ImageV1Api.scala +++ b/src/main/scala/org/mbari/vars/annotation/api/v1/ImageV1Api.scala @@ -48,7 +48,9 @@ class ImageV1Api(controller: ImageController)(implicit val executor: ExecutionCo .map({ case None => halt( - NotFound(toJson(ErrorMsg(404, s"an Image with an image_reference_uuid of $uuid was not found"))) + NotFound( + toJson(ErrorMsg(404, s"an Image with an image_reference_uuid of $uuid was not found")) + ) ) case Some(v) => toJson(v) }) @@ -57,7 +59,9 @@ class ImageV1Api(controller: ImageController)(implicit val executor: ExecutionCo get("/videoreference/:uuid") { val uuid = params .getAs[UUID]("uuid") - .getOrElse(halt(BadRequest(toJson(ErrorMsg(400, "A video reference 'uuid' parameter is required"))))) + .getOrElse( + halt(BadRequest(toJson(ErrorMsg(400, "A video reference 'uuid' parameter is required")))) + ) val limit = params.getAs[Int]("limit") val offset = params.getAs[Int]("offset") controller @@ -69,7 +73,9 @@ class ImageV1Api(controller: ImageController)(implicit val executor: ExecutionCo get("/name/:name") { val name = params .get("name") - .getOrElse(halt(BadRequest(toJson(ErrorMsg(400, "An image name is required as part of the path"))))) + .getOrElse( + halt(BadRequest(toJson(ErrorMsg(400, "An image name is required as part of the path")))) + ) controller .findByImageName(name) .map(_.asJava) @@ -77,7 +83,7 @@ class ImageV1Api(controller: ImageController)(implicit val executor: ExecutionCo } // URL should be encoded e.g. URLEncoder.encode(...) - get("/url/:url") { + get("/url/*") { // val url = params.get("url") // .map(URLDecoder.decode(_, "UTF-8")) // .map(new URL(_)) @@ -85,12 +91,13 @@ class ImageV1Api(controller: ImageController)(implicit val executor: ExecutionCo // ErrorMsg(400, "Please provide a URL") // ))) val url = params - .getAs[URL]("url") + .getAs[URL]("splat") .getOrElse(halt(BadRequest(toJson(ErrorMsg(400, "Please provide a URL"))))) controller .findByURL(url) .map({ - case None => halt(NotFound(toJson(ErrorMsg(404, "an Image with a URL of $url was not found")))) + case None => + halt(NotFound(toJson(ErrorMsg(404, "an Image with a URL of $url was not found")))) case Some(i) => toJson(i) }) } @@ -99,9 +106,13 @@ class ImageV1Api(controller: ImageController)(implicit val executor: ExecutionCo validateRequest() // Apply API security val videoReferenceUUID = params .getAs[UUID]("video_reference_uuid") - .getOrElse(halt(BadRequest(toJson(ErrorMsg(400, "A 'video_reference_uuid' parameter is required"))))) + .getOrElse( + halt(BadRequest(toJson(ErrorMsg(400, "A 'video_reference_uuid' parameter is required")))) + ) val url = - params.getAs[URL]("url").getOrElse(halt(BadRequest(toJson(ErrorMsg(400, "A 'url' parameter is required"))))) + params + .getAs[URL]("url") + .getOrElse(halt(BadRequest(toJson(ErrorMsg(400, "A 'url' parameter is required"))))) val timecode = params.getAs[Timecode]("timecode") val elapsedTime = params.getAs[Duration]("elapsed_time_millis") val recordedDate = params.getAs[Instant]("recorded_timestamp") @@ -109,10 +120,12 @@ class ImageV1Api(controller: ImageController)(implicit val executor: ExecutionCo if (timecode.isEmpty && elapsedTime.isEmpty && recordedDate.isEmpty) { halt( BadRequest( - toJson(ErrorMsg( - 400, - "An valid index of timecode, elapsed_time_millis, or recorded_timestamp is required" - )) + toJson( + ErrorMsg( + 400, + "An valid index of timecode, elapsed_time_millis, or recorded_timestamp is required" + ) + ) ) ) } @@ -140,7 +153,9 @@ class ImageV1Api(controller: ImageController)(implicit val executor: ExecutionCo validateRequest() // Apply API security val uuid = params .getAs[UUID]("uuid") - .getOrElse(halt(BadRequest(toJson(ErrorMsg(400, "A image reference 'uuid' parameter is required"))))) + .getOrElse( + halt(BadRequest(toJson(ErrorMsg(400, "A image reference 'uuid' parameter is required")))) + ) val videoReferenceUUID = params.getAs[UUID]("video_reference_uuid") val url = params.getAs[URL]("url") val timecode = params.getAs[Timecode]("timecode") @@ -166,7 +181,9 @@ class ImageV1Api(controller: ImageController)(implicit val executor: ExecutionCo .map({ case None => halt( - NotFound(toJson(ErrorMsg(404, s"an Image with an image_reference_uuid of $uuid was not found"))) + NotFound( + toJson(ErrorMsg(404, s"an Image with an image_reference_uuid of $uuid was not found")) + ) ) case Some(v) => toJson(v) }) diff --git a/src/test/scala/org/mbari/vars/annotation/api/v1/AssociationV1ApiSpec.scala b/src/test/scala/org/mbari/vars/annotation/api/v1/AssociationV1ApiSpec.scala index 6e2b792b..0c096c40 100644 --- a/src/test/scala/org/mbari/vars/annotation/api/v1/AssociationV1ApiSpec.scala +++ b/src/test/scala/org/mbari/vars/annotation/api/v1/AssociationV1ApiSpec.scala @@ -178,18 +178,21 @@ class AssociationV1ApiSpec extends WebApiStack { } it should "create with a defined association UUID" in { + val uuid = UUID.randomUUID() post( s"/v1/associations/", "observation_uuid" -> annotation.observationUuid.toString, "link_name" -> "bounding box", "to_concept" -> "cool thing", - "link_value" -> "{" \ x \ ": 10}", - "association_uuid" + "link_value" -> """{"x": 10}""", + "association_uuid" -> uuid.toString() ) { status should be(200) association = gson.fromJson(body, classOf[AssociationImpl]) - association.linkName should be("color") - association.linkValue should be("red") + association.linkName should be("bounding box") + association.toConcept should be("cool thing") + association.linkValue should be("""{"x": 10}""") + association.uuid should be(uuid) } } diff --git a/src/test/scala/org/mbari/vars/annotation/api/v1/ObservationV1ApiSpec.scala b/src/test/scala/org/mbari/vars/annotation/api/v1/ObservationV1ApiSpec.scala index 7a79bdc8..a7f223e8 100644 --- a/src/test/scala/org/mbari/vars/annotation/api/v1/ObservationV1ApiSpec.scala +++ b/src/test/scala/org/mbari/vars/annotation/api/v1/ObservationV1ApiSpec.scala @@ -21,22 +21,30 @@ import java.time.{Duration, Instant} import java.util.UUID import java.util.concurrent.TimeUnit +import org.mbari.vars.annotation.controllers.BasicDAOFactory import org.mbari.vars.annotation.Constants import org.mbari.vars.annotation.api.WebApiStack import org.mbari.vars.annotation.controllers.ObservationController -import org.mbari.vars.annotation.dao.jpa.{AnnotationImpl, AssociationImpl, ImagedMomentImpl, ObservationImpl} +import org.mbari.vars.annotation.dao.jpa.{ + AnnotationImpl, + AssociationImpl, + ImagedMomentImpl, + ObservationImpl +} import org.mbari.vars.annotation.model.Observation import scala.collection.JavaConverters._ import scala.concurrent.Await import scala.concurrent.duration.{Duration => SDuration} +import org.mbari.vars.annotation.controllers.AnnotationController +import scala.concurrent.Future /** - * - * - * @author Brian Schlining - * @since 2016-09-13T14:31:00 - */ + * + * + * @author Brian Schlining + * @since 2016-09-13T14:31:00 + */ class ObservationV1ApiSpec extends WebApiStack { private[this] val timeout = SDuration(3000, TimeUnit.MILLISECONDS) @@ -46,21 +54,31 @@ class ObservationV1ApiSpec extends WebApiStack { new ObservationV1Api(controller) } - protected[this] override val gson = Constants.GSON_FOR_ANNOTATION + private[this] val annoController = new AnnotationController( + daoFactory.asInstanceOf[BasicDAOFactory] + ) + + override protected[this] val gson = Constants.GSON_FOR_ANNOTATION private[this] val path = "/v1/observations" addServlet(observationV1Api, path) + def exec[R](fn: () => Future[R]): R = Await.result(fn.apply(), timeout) + var observation: Observation = _ "ObservationV1Api" should "find by uuid" in { // --- create an observation - val dao = daoFactory.newObservationDAO() + val dao = daoFactory.newObservationDAO() val imagedMoment = ImagedMomentImpl(Some(UUID.randomUUID()), Some(Instant.now())) - observation = ObservationImpl("rocketship", observer = Some("brian"), group = Some("ROV"), - activity = Some("transect")) + observation = ObservationImpl( + "rocketship", + observer = Some("brian"), + group = Some("ROV"), + activity = Some("transect") + ) imagedMoment.addObservation(observation) val f = dao.runTransaction(d => d.create(observation)) f.onComplete(_ => dao.close()) @@ -81,11 +99,15 @@ class ObservationV1ApiSpec extends WebApiStack { it should "find by videoreference" in { val dao = daoFactory.newObservationDAO() - val newObs = ObservationImpl("submarine", observer = Some("schlin"), group = Some("AUV"), - activity = Some("descent")) + val newObs = ObservationImpl( + "submarine", + observer = Some("schlin"), + group = Some("AUV"), + activity = Some("descent") + ) val f = dao.runTransaction(d => { dao.findByUUID(observation.uuid) match { - case None => fail(s"Unable to find observation with uuid of ${observation.uuid}") + case None => fail(s"Unable to find observation with uuid of ${observation.uuid}") case Some(obs) => obs.imagedMoment.addObservation(newObs) } }) @@ -100,12 +122,12 @@ class ObservationV1ApiSpec extends WebApiStack { } it should "find by association" in { - val dao = daoFactory.newObservationDAO() - val assDao = daoFactory.newAssociationDAO(dao) + val dao = daoFactory.newObservationDAO() + val assDao = daoFactory.newAssociationDAO(dao) val association = assDao.newPersistentObject("eating", Some("cake")) val f = dao.runTransaction(d => { dao.findByUUID(observation.uuid) match { - case None => fail(s"Unable to find observation with uuid of ${observation.uuid}") + case None => fail(s"Unable to find observation with uuid of ${observation.uuid}") case Some(obs) => obs.addAssociation(association) } }) @@ -121,10 +143,14 @@ class ObservationV1ApiSpec extends WebApiStack { it should "find all names" in { // --- create another imagedmoment with a different video-reference uuid - val dao = daoFactory.newObservationDAO() + val dao = daoFactory.newObservationDAO() val imagedMoment = ImagedMomentImpl(Some(UUID.randomUUID()), Some(Instant.now())) - val obs = ObservationImpl("squid", observer = Some("aine"), group = Some("Image:Benthic Rover"), - activity = Some("transit")) + val obs = ObservationImpl( + "squid", + observer = Some("aine"), + group = Some("Image:Benthic Rover"), + activity = Some("transit") + ) imagedMoment.addObservation(obs) val f = dao.runTransaction(d => d.create(obs)) f.onComplete(_ => dao.close()) @@ -151,16 +177,17 @@ class ObservationV1ApiSpec extends WebApiStack { it should "update" in { put( s"$path/${observation.uuid}", - "concept" -> "shoe", + "concept" -> "shoe", "duration_millis" -> "3200", - "activity" -> "ascent") { - status should be(200) - val obs = gson.fromJson(body, classOf[ObservationImpl]) - obs.concept should be("shoe") - obs.duration should be(Duration.ofMillis(3200)) - obs.activity should be("ascent") - obs.uuid should be(observation.uuid) - } + "activity" -> "ascent" + ) { + status should be(200) + val obs = gson.fromJson(body, classOf[ObservationImpl]) + obs.concept should be("shoe") + obs.duration should be(Duration.ofMillis(3200)) + obs.activity should be("ascent") + obs.uuid should be(observation.uuid) + } } it should "delete" in { @@ -170,12 +197,20 @@ class ObservationV1ApiSpec extends WebApiStack { } it should "bulk delete" in { - val dao = daoFactory.newObservationDAO() + val dao = daoFactory.newObservationDAO() val imagedMoment = ImagedMomentImpl(Some(UUID.randomUUID()), Some(Instant.now())) - val obs0 = ObservationImpl("rocketship", observer = Some("brian"), group = Some("ROV"), - activity = Some("transect")) - val obs1 = ObservationImpl("giant dragon", observer = Some("brian"), group = Some("AUV"), - activity = Some("transect")) + val obs0 = ObservationImpl( + "rocketship", + observer = Some("brian"), + group = Some("ROV"), + activity = Some("transect") + ) + val obs1 = ObservationImpl( + "giant dragon", + observer = Some("brian"), + group = Some("AUV"), + activity = Some("transect") + ) imagedMoment.addObservation(obs0) imagedMoment.addObservation(obs1) val f = dao.runTransaction(d => { @@ -185,18 +220,45 @@ class ObservationV1ApiSpec extends WebApiStack { f.onComplete(t => dao.close()) Await.result(f, timeout) - val annos = Seq(obs0, obs1).map(AnnotationImpl(_)) + val annos = Seq(obs0, obs1) + .map(AnnotationImpl(_)) .map(_.observationUuid) .asJava - val json = Constants.GSON_FOR_ANNOTATION + val json = Constants + .GSON_FOR_ANNOTATION .toJson(annos) .getBytes(StandardCharsets.UTF_8) - post( - s"$path/delete", - body = json, - headers = Map("Content-Type" -> "application/json")) { - status should be(200) - } + post(s"$path/delete", body = json, headers = Map("Content-Type" -> "application/json")) { + status should be(200) + } + } + + it should "delete duration" in { + // create an annotation + val recordedDate = Instant.now() + val duration = Duration.ofSeconds(5) + val a = exec(() => + annoController + .create( + UUID.randomUUID(), + "Nanomia bijuga", + "brian", + recordedDate = Some(recordedDate), + duration = Some(Duration.ofSeconds(5)) + ) + ) + a.concept should be("Nanomia bijuga") + a.observer should be("brian") + a.recordedTimestamp should be(recordedDate) + a.duration should be(duration) + + put(s"$path/delete/duration/${a.observationUuid}") { + status should be(200) + val obs = gson.fromJson(body, classOf[ObservationImpl]) + obs.concept should be(a.concept) + obs.uuid should be(a.observationUuid) + obs.duration should be(null) + } } } diff --git a/src/test/scala/org/mbari/vars/annotation/controllers/ObservationControllerSpec.scala b/src/test/scala/org/mbari/vars/annotation/controllers/ObservationControllerSpec.scala new file mode 100644 index 00000000..70e8eedc --- /dev/null +++ b/src/test/scala/org/mbari/vars/annotation/controllers/ObservationControllerSpec.scala @@ -0,0 +1,90 @@ +/* + * Copyright 2017 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.vars.annotation.controllers + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import java.util.concurrent.TimeUnit +import java.time.Instant +import scala.concurrent.duration.{Duration => SDuration} +import scala.concurrent.ExecutionContext.Implicits.global +import org.mbari.vars.annotation.dao.jpa.{TestDAOFactory} +import java.{util => ju} +import scala.concurrent.Await +import scala.concurrent.Future +import java.time.Duration + +class ObservationControllerSpec extends AnyFunSpec with Matchers { + + private[this] val daoFactory = TestDAOFactory.Instance + private[this] val annoController = new AnnotationController( + daoFactory.asInstanceOf[BasicDAOFactory] + ) + + private[this] val controller = new ObservationController( + daoFactory.asInstanceOf[BasicDAOFactory] + ) + private[this] val timeout = SDuration(200, TimeUnit.SECONDS) + + def exec[R](fn: () => Future[R]): R = Await.result(fn.apply(), timeout) + + describe("ObservationController") { + describe("deleteDuration") { + it("should delete duration") { + + // create an annotation + val recordedDate = Instant.now() + val duration = Duration.ofSeconds(5) + val a = exec(() => + annoController + .create( + ju.UUID.randomUUID(), + "Nanomia bijuga", + "brian", + recordedDate = Some(recordedDate), + duration = Some(Duration.ofSeconds(5)) + ) + ) + a.concept should be("Nanomia bijuga") + a.observer should be("brian") + a.recordedTimestamp should be(recordedDate) + a.duration should be(duration) + + // look up and verify no duration + val opt0 = exec(() => controller.findByUUID(a.observationUuid)) + opt0 should not be None + val obs0 = opt0.get + obs0.duration should be(duration) + + // delete duration + val opt = exec(() => controller.deleteDuration(a.observationUuid)) + opt should not be None + val obs = opt.get + obs.duration should be(null) + obs.imagedMoment.recordedDate.toEpochMilli should be(recordedDate.toEpochMilli()) + + // look up and verify no duration + val opt1 = exec(() => controller.findByUUID(obs.uuid)) + opt1 should not be None + val obs1 = opt1.get + obs1.duration should be(null) + + } + } + } + +}