diff --git a/.gitignore b/.gitignore index 82c679e..d531475 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,6 @@ /.idea/ /.bsp/ /project/target/ -/target/ +target/ /esw-segment-db/target/ /esw-segment-db/xxx.json diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..eb34ee1 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,10 @@ +version = 2.6.4 + +project.excludeFilters = ["BuildInfo.scala", "target/"] + +align.preset = more + +docstrings = JavaDoc +indentOperator.preset = spray +maxColumn = 130 +newlines.alwaysBeforeElseAfterCurlyIf = true diff --git a/build.sbt b/build.sbt index 883b7e2..2e728ec 100644 --- a/build.sbt +++ b/build.sbt @@ -4,7 +4,20 @@ import sbt._ import Dependencies._ import Settings._ +lazy val `esw-segment-shared` = project + .settings(buildSettings: _*) + .settings(libraryDependencies ++= `esw-segment-shared-deps`) + lazy val `esw-segment-db` = project .enablePlugins(DeployApp) + .enablePlugins(BuildInfoPlugin) + .settings(appSettings: _*) + .settings(libraryDependencies ++= `esw-segment-db-deps`) + .dependsOn(`esw-segment-shared` % "compile->compile;test->test") + +lazy val `esw-segment-client` = project + .enablePlugins(DeployApp) + .enablePlugins(BuildInfoPlugin) .settings(appSettings: _*) - .settings(libraryDependencies ++= `databaseTests-deps`) + .settings(libraryDependencies ++= `esw-segment-client-deps`) + .dependsOn(`esw-segment-shared` % "compile->compile;test->test") diff --git a/esw-segment-client/src/main/scala/esw/segment/client/EswSegmentClientOptions.scala b/esw-segment-client/src/main/scala/esw/segment/client/EswSegmentClientOptions.scala new file mode 100644 index 0000000..5d59536 --- /dev/null +++ b/esw-segment-client/src/main/scala/esw/segment/client/EswSegmentClientOptions.scala @@ -0,0 +1,27 @@ +package esw.segment.client + +import java.util.Date + +object EswSegmentClientOptions { + val defaultPort = 9192 +} + +case class EswSegmentClientOptions( + host: String = "localhost", + port: Int = EswSegmentClientOptions.defaultPort, + date: Date = new Date(), + from: Date = new Date(), + to: Date = new Date(), + segmentId: Option[String] = None, + position: Option[Int] = None, + setPosition: Option[Unit] = None, + segmentPositions: Option[Unit] = None, + segmentIds: Option[Unit] = None, + newlyInstalledSegments: Option[Unit] = None, + currentPositions: Option[Unit] = None, + currentSegmentPosition: Option[Unit] = None, + currentSegmentAtPosition: Option[Unit] = None, + positionsOnDate: Option[Unit] = None, + segmentPositionOnDate: Option[Unit] = None, + segmentAtPositionOnDate: Option[Unit] = None +) diff --git a/esw-segment-client/src/main/scala/esw/segment/client/EswSegmentDbClient.scala b/esw-segment-client/src/main/scala/esw/segment/client/EswSegmentDbClient.scala new file mode 100644 index 0000000..f6c76b9 --- /dev/null +++ b/esw-segment-client/src/main/scala/esw/segment/client/EswSegmentDbClient.scala @@ -0,0 +1,180 @@ +package esw.segment.client + +import java.util.Date + +import akka.actor.ActorSystem +import buildinfo.BuildInfo +import esw.segment.shared.EswSegmentData._ +import EswSegmentClientOptions._ +import scopt.Read.reads +import scopt.Read + +import scala.async.Async.{async, await} +import scala.concurrent.{Await, Future} +import scala.concurrent.duration._ + +object EswSegmentDbClient extends App { + val dateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd") + implicit val system: ActorSystem = ActorSystem() + + import system._ + + implicit val durationRead: Read[Date] = + reads { + dateFormat.parse + } + + // Parser for the command line options + private val parser = new scopt.OptionParser[EswSegmentClientOptions]("esw-segment-db-client") { + head("esw-segment-db-client", BuildInfo.version) + + opt[String]("host") valueName "" action { (x, c) => + c.copy(host = x) + } text s"The host name where the ESW Segment DB HTTP server is running (default: localhost)" + + opt[Int]("port") valueName "" action { (x, c) => + c.copy(port = x) + } text s"The port number to use for the server (default: $defaultPort)" + + opt[Date]('d', "date") valueName dateFormat.toPattern action { (x, c) => + c.copy(date = x) + } text s"The date to use (default: current date)" + + opt[Date]("from") valueName dateFormat.toPattern action { (x, c) => + c.copy(from = x) + } text s"The starting date to use for a date range (default: current date)" + + opt[Date]("to") valueName dateFormat.toPattern action { (x, c) => + c.copy(to = x) + } text s"The ending date to use for a date range (default: current date)" + + opt[String]('s', "segmentId") valueName "" action { (x, c) => + c.copy(segmentId = Some(x)) + } text s"The segment id to use" + + opt[Int]('p', "position") valueName "" action { (x, c) => + c.copy(position = Some(x)) + } text s"The segment position to use (number in range 1 to 492)" + + opt[Unit]("setPosition") action { (_, c) => + c.copy(setPosition = Some(())) + } text s"Sets or updates the date and position of the given segment (Requires --position, --segmentId if segment is present)" + + opt[Unit]("segmentPositions") action { (_, c) => + c.copy(segmentPositions = Some(())) + } text s"Gets a list of segments positions for the given segment id in the given date range (Requires --segmentId)" + + opt[Unit]("segmentIds") action { (_, c) => + c.copy(segmentIds = Some(())) + } text s"Gets a list of segment ids that were in the given position in the given date range (Requires --position)" + + opt[Unit]("newlyInstalledSegments") action { (_, c) => + c.copy(newlyInstalledSegments = Some(())) + } text s"Gets a list of segments that were installed since the given date (Requires --date)" + + opt[Unit]("currentPositions") action { (_, c) => + c.copy(currentPositions = Some(())) + } text s"Gets the current segment positions, sorted by position" + + opt[Unit]("currentSegmentPosition") action { (_, c) => + c.copy(currentSegmentPosition = Some(())) + } text s"Gets the current segment position for the given segment id (Requires --segmentId)" + + opt[Unit]("currentSegmentAtPosition") action { (_, c) => + c.copy(currentSegmentAtPosition = Some(())) + } text s"Gets the id of the segment currently in the given position (Requires --position)" + + opt[Unit]("positionsOnDate") action { (_, c) => + c.copy(positionsOnDate = Some(())) + } text s"Gets the segment positions as they were on the given date, sorted by position" + + opt[Unit]("segmentPositionOnDate") action { (_, c) => + c.copy(segmentPositionOnDate = Some(())) + } text s"Gets the segment position for the given segment id on the given date (Requires --segmentId)" + + opt[Unit]("segmentAtPositionOnDate") action { (_, c) => + c.copy(segmentAtPositionOnDate = Some(())) + } text s"Gets the id of the segment that was installed in the given position on the given date (Requires --position)" + + } + + // Parse the command line options + parser.parse(args, EswSegmentClientOptions()) match { + case Some(options) => + try { + Await.ready(run(options), 60.seconds) + } + catch { + case e: Throwable => + e.printStackTrace() + System.exit(1) + } + case None => System.exit(1) + } + + private def error(msg: String): Unit = { + println(msg) + System.exit(1) + } + + private def showResults(result: List[SegmentToM1Pos]): Unit = { + result.foreach(r => println(s"${dateFormat.format(r.date)} ${r.maybeId.getOrElse("------")} ${r.pos}")) + } + + // Run the application + private def run(options: EswSegmentClientOptions): Future[Unit] = + async { + import options._ + val client = new EswSegmentHttpClient(host, port) + + if (options.setPosition.isDefined) { + if (position.isEmpty) error("--position option is required") + val segmentToM1Pos = SegmentToM1Pos(date, segmentId, position.get) + val result = await(client.setPosition(segmentToM1Pos)) + if (!result) + error(s"setPosition failed for date: $date, segmentId: ${segmentId.getOrElse("None")}, position: ${position.get}") + } + + if (options.segmentPositions.isDefined) { + if (segmentId.isEmpty) error("--segmentId option is required") + showResults(await(client.segmentPositions(DateRange(from, to), segmentId.get))) + } + + if (options.segmentIds.isDefined) { + if (position.isEmpty) error("--position option is required") + showResults(await(client.segmentIds(DateRange(from, to), position.get))) + } + + if (options.newlyInstalledSegments.isDefined) { + showResults(await(client.newlyInstalledSegments(date))) + } + + if (options.currentPositions.isDefined) { + showResults(await(client.currentPositions())) + } + + if (options.currentSegmentPosition.isDefined) { + if (segmentId.isEmpty) error("--segmentId option is required") + showResults(await(client.currentSegmentPosition(segmentId.get)).toList) + } + + if (options.currentSegmentAtPosition.isDefined) { + if (position.isEmpty) error("--position option is required") + showResults(await(client.currentSegmentAtPosition(position.get)).toList) + } + + if (options.positionsOnDate.isDefined) { + showResults(await(client.positionsOnDate(date))) + } + + if (options.segmentPositionOnDate.isDefined) { + if (segmentId.isEmpty) error("--segmentId option is required") + showResults(await(client.segmentPositionOnDate(date, segmentId.get)).toList) + } + + if (options.segmentAtPositionOnDate.isDefined) { + if (position.isEmpty) error("--position option is required") + showResults(await(client.segmentAtPositionOnDate(date, position.get)).toList) + } + } +} diff --git a/esw-segment-client/src/main/scala/esw/segment/client/EswSegmentHttpClient.scala b/esw-segment-client/src/main/scala/esw/segment/client/EswSegmentHttpClient.scala new file mode 100644 index 0000000..4baa4bb --- /dev/null +++ b/esw-segment-client/src/main/scala/esw/segment/client/EswSegmentHttpClient.scala @@ -0,0 +1,124 @@ +package esw.segment.client + +import java.sql.Date + +import esw.segment.shared.{EswSegmentData, JsonSupport, SegmentToM1Api} +import EswSegmentData._ +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.model.StatusCodes.OK +import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpMethods, HttpRequest, Uri} +import spray.json._ +import akka.http.scaladsl.unmarshalling.Unmarshal +import EswSegmentClientOptions._ + +import scala.async.Async.{async, await} +import scala.concurrent.{ExecutionContextExecutor, Future} + +/** + * HTTP Client for ESW Segment DB HTTP Server + */ +class EswSegmentHttpClient(host: String = "localhost", port: Int = defaultPort)(implicit + actorSystem: ActorSystem, + ec: ExecutionContextExecutor +) extends SegmentToM1Api + with JsonSupport { + + private val baseUri = s"http://$host:$port" + + private def postSet(uri: Uri, json: String): Future[Boolean] = + async { + val entity = HttpEntity(ContentTypes.`application/json`, json) + val request = HttpRequest(HttpMethods.POST, uri = uri, entity = entity) + val response = await(Http().singleRequest(request)) + response.status == OK + } + + private def postGetList(uri: Uri, json: String): Future[List[SegmentToM1Pos]] = + async { + val entity = HttpEntity(ContentTypes.`application/json`, json) + val request = HttpRequest(HttpMethods.POST, uri = uri, entity = entity) + val response = await(Http().singleRequest(request)) + await(Unmarshal(response).to[List[SegmentToM1Pos]]) + } + + private def postGetOption(uri: Uri, json: String): Future[Option[SegmentToM1Pos]] = + async { + val entity = HttpEntity(ContentTypes.`application/json`, json) + val request = HttpRequest(HttpMethods.POST, uri = uri, entity = entity) + val response = await(Http().singleRequest(request)) + await(Unmarshal(response).to[Option[SegmentToM1Pos]]) + } + + private def getOption(uri: Uri): Future[Option[SegmentToM1Pos]] = + async { + val request = HttpRequest(HttpMethods.GET, uri = uri) + val response = await(Http().singleRequest(request)) + await(Unmarshal(response).to[Option[SegmentToM1Pos]]) + } + + // -- + + override def setPosition(segmentToM1Pos: SegmentToM1Pos): Future[Boolean] = { + postSet(Uri(s"$baseUri/setPosition"), segmentToM1Pos.toJson.compactPrint) + } + + override def setPositions(date: Date, positions: List[(Option[String], Int)]): Future[Boolean] = { + postSet(Uri(s"$baseUri/setPositions"), SegmentToM1Positions(date, positions).toJson.compactPrint) + } + + override def setAllPositions(date: Date, allPositions: List[Option[String]]): Future[Boolean] = { + postSet(Uri(s"$baseUri/setAllPositions"), AllSegmentPositions(date, allPositions).toJson.compactPrint) + } + + override def segmentPositions(dateRange: DateRange, segmentId: String): Future[List[SegmentToM1Pos]] = { + postGetList(Uri(s"$baseUri/segmentPositions/$segmentId"), dateRange.toJson.compactPrint) + } + + override def segmentIds(dateRange: DateRange, pos: Int): Future[List[SegmentToM1Pos]] = { + postGetList(Uri(s"$baseUri/segmentIds/$pos"), dateRange.toJson.compactPrint) + } + + override def newlyInstalledSegments(since: Date): Future[List[SegmentToM1Pos]] = { + postGetList(Uri(s"$baseUri/newlyInstalledSegments"), since.toJson.compactPrint) + } + + override def positionsOnDate(date: Date): Future[List[SegmentToM1Pos]] = { + postGetList(Uri(s"$baseUri/positionsOnDate"), date.toJson.compactPrint) + } + + override def segmentPositionOnDate(date: Date, segmentId: String): Future[Option[SegmentToM1Pos]] = { + postGetOption(Uri(s"$baseUri/segmentPositionOnDate/$segmentId"), date.toJson.compactPrint) + } + + override def segmentAtPositionOnDate(date: Date, pos: Int): Future[Option[SegmentToM1Pos]] = { + postGetOption(Uri(s"$baseUri/segmentAtPositionOnDate/$pos"), date.toJson.compactPrint) + } + + override def currentPositions(): Future[List[SegmentToM1Pos]] = + async { + val uri = Uri(s"$baseUri/currentPositions") + val request = HttpRequest(HttpMethods.GET, uri = uri) + val response = await(Http().singleRequest(request)) + await(Unmarshal(response).to[List[SegmentToM1Pos]]) + } + + override def currentSegmentPosition(segmentId: String): Future[Option[SegmentToM1Pos]] = { + getOption(Uri(s"$baseUri/currentSegmentPosition/$segmentId")) + } + + override def currentSegmentAtPosition(pos: Int): Future[Option[SegmentToM1Pos]] = { + getOption(Uri(s"$baseUri/currentSegmentAtPosition/$pos")) + } + + /** + * Drops and recreates the database tables (for testing) + */ + override def resetTables(): Future[Boolean] = + async { + val uri = Uri(s"$baseUri/resetTables") + val request = HttpRequest(HttpMethods.POST, uri = uri) + val response = await(Http().singleRequest(request)) + response.status == OK + } +} diff --git a/esw-segment-client/src/test/scala/esw/segment/client/EswSegmentClientTest.scala b/esw-segment-client/src/test/scala/esw/segment/client/EswSegmentClientTest.scala new file mode 100644 index 0000000..be15151 --- /dev/null +++ b/esw-segment-client/src/test/scala/esw/segment/client/EswSegmentClientTest.scala @@ -0,0 +1,13 @@ +package esw.segment.client + +import akka.actor.ActorSystem +import esw.segment.shared.SegmentToM1ApiTestBase +import EswSegmentClientTest._ + +object EswSegmentClientTest { + implicit val system: ActorSystem = ActorSystem() + import system._ + val client = new EswSegmentHttpClient() +} + +class EswSegmentClientTest extends SegmentToM1ApiTestBase(client) diff --git a/esw-segment-db/src/main/resources/reference.conf b/esw-segment-db/src/main/resources/reference.conf index ad8e9a8..65f56cc 100644 --- a/esw-segment-db/src/main/resources/reference.conf +++ b/esw-segment-db/src/main/resources/reference.conf @@ -4,15 +4,11 @@ akka { loglevel = info } -server { - port = 9192 - port = ${?port} -} - csw-logging { - stdout { - pretty = true - oneLine = true + appender-config { + stdout { + oneLine = true + } } component-log-levels { CSW { diff --git a/esw-segment-db/src/main/scala/esw/segment/db/DbWiring.scala b/esw-segment-db/src/main/scala/esw/segment/db/DbWiring.scala index 9daa4b8..877cd36 100644 --- a/esw-segment-db/src/main/scala/esw/segment/db/DbWiring.scala +++ b/esw-segment-db/src/main/scala/esw/segment/db/DbWiring.scala @@ -4,32 +4,36 @@ import java.net.InetAddress import akka.actor.typed.{ActorSystem, SpawnProtocol} import csw.database.DatabaseServiceFactory -import csw.database.scaladsl.JooqExtentions.RichQuery import csw.location.api.scaladsl.LocationService import csw.location.client.scaladsl.HttpLocationServiceFactory import csw.logging.api.scaladsl.Logger import csw.logging.client.scaladsl.{GenericLoggerFactory, LoggingSystemFactory} import org.jooq.DSLContext -import scala.async.Async.{async, await} -import scala.concurrent.{Await, ExecutionContextExecutor, Future} +import scala.concurrent.{Await, ExecutionContextExecutor} import scala.concurrent.duration._ +import DbWiring._ + +object DbWiring { + val defaultDbName = "esw_segment_db" + val testDbName = "test_segment_db" +} /** * Sets up the connection to the database. * * @param dbName The database name */ -class DbWiring(dbName: String = "esw_segment_db") { - lazy val host: String = InetAddress.getLocalHost.getHostName +class DbWiring(dbName: String = defaultDbName) { + lazy val host: String = InetAddress.getLocalHost.getHostName implicit lazy val typedSystem: ActorSystem[SpawnProtocol.Command] = ActorSystem(SpawnProtocol(), "EswSegmentDb") - implicit lazy val ec: ExecutionContextExecutor = typedSystem.executionContext + implicit lazy val ec: ExecutionContextExecutor = typedSystem.executionContext LoggingSystemFactory.start("EswSegmentDb", "0.1", host, typedSystem) - lazy val log: Logger = GenericLoggerFactory.getLogger + lazy val log: Logger = GenericLoggerFactory.getLogger lazy val locationService: LocationService = HttpLocationServiceFactory.makeLocalClient(typedSystem) - lazy val dbFactory = new DatabaseServiceFactory(typedSystem) - lazy val timeout: FiniteDuration = 60.seconds - lazy val dsl: DSLContext = Await.result(dbFactory.makeDsl(locationService, dbName, - "DB_WRITE_USERNAME", "DB_WRITE_PASSWORD"), timeout) + lazy val dbFactory = new DatabaseServiceFactory(typedSystem) + lazy val timeout: FiniteDuration = 60.seconds + lazy val dsl: DSLContext = + Await.result(dbFactory.makeDsl(locationService, dbName, "DB_WRITE_USERNAME", "DB_WRITE_PASSWORD"), timeout) lazy val segmentToM1PosTable = new SegmentToM1PosTable(dsl) } diff --git a/esw-segment-db/src/main/scala/esw/segment/db/SegmentToM1PosTable.scala b/esw-segment-db/src/main/scala/esw/segment/db/SegmentToM1PosTable.scala index 26a2c27..d96cce2 100644 --- a/esw-segment-db/src/main/scala/esw/segment/db/SegmentToM1PosTable.scala +++ b/esw-segment-db/src/main/scala/esw/segment/db/SegmentToM1PosTable.scala @@ -4,17 +4,14 @@ import java.sql.Date import org.jooq.DSLContext import csw.database.scaladsl.JooqExtentions._ +import esw.segment.shared.EswSegmentData._ import SegmentToM1PosTable._ +import esw.segment.shared.SegmentToM1Api import scala.async.Async._ import scala.concurrent.{ExecutionContext, Future} object SegmentToM1PosTable { - /** - * Number of segments in the TMT primary mirror. - * This is also the size of the positions array in the segment_to_m1_pos table. - */ - val numSegments = 492 // Table and column names private[db] val tableName = "segment_to_m1_pos" @@ -26,55 +23,17 @@ object SegmentToM1PosTable { private def quoted(s: String) = "\"" + s + "\"" - /** - * Position of a segment on a given date - * - * @param date date of record - * @param maybeId the segment id, if the segment is present, None if it is missing - * @param pos position of segment - */ - case class SegmentToM1Pos(date: Date, maybeId: Option[String], pos: Int) { - def this(date: Date, id: String, pos: Int) = { - this(date, if (id.startsWith(missingSegmentId)) None else Some(id), pos) - } - } - - /** - * Positions of a number of segments on a given date. - * - * @param date date of record - * @param positions a list of pairs of (segment-id, maybe-position) for zero or more segments - * to be set/updated for the given date. - * index+1 is the position. The value is Some(segment-id) or None, if - * a segment is missing. - */ - case class SegmentToM1Positions(date: Date, positions: List[(Option[String], Int)]) - - /** - * All of the segment positions for the given date. - * The first item in the positions list is taken to be the segment position 1 and so on. - * - * @param date the date corresponding to the positions - * @param allPositions list of all 492 segment positions (Missing segments should be None, - * present segments should be Some(segment-id) - */ - case class AllSegmentPositions(date: Date, allPositions: List[Option[String]]) - - /** - * A range of dates - * - * @param from start of the date range - * @param to end of the date range - */ - case class DateRange(from: Date, to: Date) + private def currentDate(): Date = new Date(System.currentTimeMillis()) - def currentDate(): Date = new Date(System.currentTimeMillis()) + private def makeSegmentToM1Pos(date: Date, id: String, pos: Int): SegmentToM1Pos = { + SegmentToM1Pos(date, if (id.startsWith(missingSegmentId)) None else Some(id), pos) + } } /** * Provides operations on the segment_to_m1_pos database table. */ -class SegmentToM1PosTable(dsl: DSLContext)(implicit ec: ExecutionContext) { +class SegmentToM1PosTable(dsl: DSLContext)(implicit ec: ExecutionContext) extends SegmentToM1Api { /** * Returns true if there is an entry for the given date in the table @@ -291,7 +250,7 @@ class SegmentToM1PosTable(dsl: DSLContext)(implicit ec: ExecutionContext) { val list = queryResult.flatMap { result => val date = result._1 val positions = result._2 - positions.zipWithIndex.find(segmentId == _._1).map(p => new SegmentToM1Pos(date, segmentId, p._2 + 1)) + positions.zipWithIndex.find(segmentId == _._1).map(p => makeSegmentToM1Pos(date, segmentId, p._2 + 1)) } val fList = list.map(s => withInstallDate(dateRange.to, s)) await(Future.sequence(fList)).distinct.sortWith(_.pos < _.pos) @@ -315,7 +274,7 @@ class SegmentToM1PosTable(dsl: DSLContext)(implicit ec: ExecutionContext) { val list = queryResult.map { result => val date = result._1 val segmentId = result._2 - new SegmentToM1Pos(date, segmentId, pos) + makeSegmentToM1Pos(date, segmentId, pos) } val fList = list.map(s => withInstallDate(dateRange.to, s)) await(Future.sequence(fList)).distinct.sortWith(_.maybeId.get < _.maybeId.get) @@ -379,7 +338,7 @@ class SegmentToM1PosTable(dsl: DSLContext)(implicit ec: ExecutionContext) { positions .zipWithIndex .filter(p => !p._1.startsWith(missingSegmentId)) - .map(p => new SegmentToM1Pos(date, p._1, p._2 + 1)) + .map(p => makeSegmentToM1Pos(date, p._1, p._2 + 1)) } val fList = list .map(s => withInstallDate(currentDate(), s)) @@ -408,5 +367,13 @@ class SegmentToM1PosTable(dsl: DSLContext)(implicit ec: ExecutionContext) { await(positionsOnDate(date)).find(_.pos == pos) } + /** + * Drops and recreates the database tables (for testing) + * @return true if OK + */ + def resetTables(): Future[Boolean] = async { + await(dsl.truncate(SegmentToM1PosTable.tableName).executeAsyncScala()) >= 0 + } + } diff --git a/esw-segment-db/src/main/scala/esw/segment/server/Configs.scala b/esw-segment-db/src/main/scala/esw/segment/server/Configs.scala deleted file mode 100644 index 7bc1d53..0000000 --- a/esw-segment-db/src/main/scala/esw/segment/server/Configs.scala +++ /dev/null @@ -1,10 +0,0 @@ -package esw.segment.server - -import akka.actor.typed.{ActorSystem, SpawnProtocol} -import com.typesafe.config.Config - -class Configs(_port: Option[Int])(implicit typedSystem: ActorSystem[SpawnProtocol.Command]) { - private lazy val config: Config = typedSystem.settings.config - - lazy val port: Int = _port.getOrElse(config.getConfig("server").getInt("port")) -} diff --git a/esw-segment-db/src/main/scala/esw/segment/server/EswSegmentDbHttpServer.scala b/esw-segment-db/src/main/scala/esw/segment/server/EswSegmentDbHttpServer.scala new file mode 100644 index 0000000..49bbc04 --- /dev/null +++ b/esw-segment-db/src/main/scala/esw/segment/server/EswSegmentDbHttpServer.scala @@ -0,0 +1,42 @@ +package esw.segment.server + +import buildinfo.BuildInfo + +object EswSegmentDbHttpServer extends App { + + case class Options(port: Int = 9192, testMode: Boolean = false) + + private val parser = new scopt.OptionParser[Options]("esw-segment-db-http-server") { + head("esw-segment-db-http-server", BuildInfo.version) + + opt[Int]("port") valueName "" action { (x, c) => + c.copy(port = x) + } text s"The port number to use for the server (default: 9192)" + + opt[Unit]('t', "testMode") action { (_, c) => + c.copy(testMode = true) + } text s"Use a test database instead of the normal one" + } + + // Parse the command line options + parser.parse(args, Options()) match { + case Some(options) => + try { + run(options) + } + catch { + case e: Throwable => + e.printStackTrace() + System.exit(1) + } + case None => System.exit(1) + } + + // Run the application + private def run(options: Options): Unit = { + import esw.segment.db.DbWiring._ + val dbName = if (options.testMode) testDbName else defaultDbName + val wiring = new ServerWiring(options.port, dbName) + wiring.server.start() + } +} diff --git a/esw-segment-db/src/main/scala/esw/segment/server/Main.scala b/esw-segment-db/src/main/scala/esw/segment/server/Main.scala deleted file mode 100644 index 327d5f2..0000000 --- a/esw-segment-db/src/main/scala/esw/segment/server/Main.scala +++ /dev/null @@ -1,6 +0,0 @@ -package esw.segment.server - -object Main extends App { - val wiring = new ServerWiring() - wiring.server.start() -} diff --git a/esw-segment-db/src/main/scala/esw/segment/server/Routes.scala b/esw-segment-db/src/main/scala/esw/segment/server/Routes.scala index 32fdc03..69151f1 100644 --- a/esw-segment-db/src/main/scala/esw/segment/server/Routes.scala +++ b/esw-segment-db/src/main/scala/esw/segment/server/Routes.scala @@ -4,8 +4,9 @@ import java.sql.Date import akka.http.scaladsl.model.StatusCodes._ import akka.http.scaladsl.server.{Directives, Route} -import esw.segment.db.SegmentToM1PosTable.{AllSegmentPositions, DateRange, SegmentToM1Pos, SegmentToM1Positions} -import esw.segment.db.{JsonSupport, SegmentToM1PosTable} +import esw.segment.db.SegmentToM1PosTable +import esw.segment.shared.EswSegmentData._ +import esw.segment.shared.JsonSupport import scala.concurrent.ExecutionContext @@ -29,9 +30,7 @@ class Routes(posTable: SegmentToM1PosTable)(implicit ec: ExecutionContext) exten entity(as[AllSegmentPositions]) { p => complete(posTable.setAllPositions(p.date, p.allPositions).map(if (_) OK else BadRequest)) } - } - } ~ - get { + } ~ // Gets a list of segments positions for the given segment id in the given date range. path("segmentPositions" / Segment) { segmentId => @@ -40,25 +39,54 @@ class Routes(posTable: SegmentToM1PosTable)(implicit ec: ExecutionContext) exten complete(posTable.segmentPositions(dateRange, segmentId)) } } ~ - // Gets a list of segment ids that were in the given position in the given date range. - path("segmentIds" / IntNumber) { - pos => - entity(as[DateRange]) { - dateRange => - complete(posTable.segmentIds(dateRange, pos)) - } - } ~ - // Returns a list of segments that were installed since the given date - path("newlyInstalledSegments") { + // Gets a list of segment ids that were in the given position in the given date range. + path("segmentIds" / IntNumber) { + pos => + entity(as[DateRange]) { + dateRange => + complete(posTable.segmentIds(dateRange, pos)) + } + } ~ + // Returns a list of segments that were installed since the given date + path("newlyInstalledSegments") { + entity(as[Date]) { + date => + complete(posTable.newlyInstalledSegments(date)) + } + } ~ + // Returns the segment positions as they were on the given date, sorted by position + path("positionsOnDate") { + entity(as[Date]) { + date => + complete(posTable.positionsOnDate(date)) + } + } ~ + // Gets the segment position for the given segment id on the given date. + path("segmentPositionOnDate" / Segment) { + segmentId => entity(as[Date]) { date => - complete(posTable.newlyInstalledSegments(date)) + complete(posTable.segmentPositionOnDate(date, segmentId)) } - } ~ - // Returns the current segment positions, sorted by position - path("currentPositions") { - complete(posTable.currentPositions()) - } ~ + } ~ + // Gets the id of the segment that was installed in the given position on the given date + path("segmentAtPositionOnDate" / IntNumber) { + pos => + entity(as[Date]) { + date => + complete(posTable.segmentAtPositionOnDate(date, pos)) + } + } ~ + // Drops and recreates the database tables (for testing) + path("resetTables") { + complete(posTable.resetTables().map(if (_) OK else BadRequest)) + } + } ~ + get { + // Returns the current segment positions, sorted by position + path("currentPositions") { + complete(posTable.currentPositions()) + } ~ // Gets the current segment position for the given segment id. path("currentSegmentPosition" / Segment) { segmentId => @@ -68,31 +96,6 @@ class Routes(posTable: SegmentToM1PosTable)(implicit ec: ExecutionContext) exten path("currentSegmentAtPosition" / IntNumber) { pos => complete(posTable.currentSegmentAtPosition(pos)) - } ~ - // Returns the segment positions as they were on the given date, sorted by position - path("positionsOnDate") { - entity(as[Date]) { - date => - complete(posTable.positionsOnDate(date)) - } - } ~ - get { - // Gets the segment position for the given segment id on the given date. - path("segmentPositionOnDate" / Segment) { - segmentId => - entity(as[Date]) { - date => - complete(posTable.segmentPositionOnDate(date, segmentId)) - } - } ~ - // Gets the id of the segment that was installed in the given position on the given date - path("segmentAtPositionOnDate" / IntNumber) { - pos => - entity(as[Date]) { - date => - complete(posTable.segmentAtPositionOnDate(date, pos)) - } - } } } } diff --git a/esw-segment-db/src/main/scala/esw/segment/server/Server.scala b/esw-segment-db/src/main/scala/esw/segment/server/Server.scala index 15bacac..a32f9f4 100644 --- a/esw-segment-db/src/main/scala/esw/segment/server/Server.scala +++ b/esw-segment-db/src/main/scala/esw/segment/server/Server.scala @@ -6,11 +6,11 @@ import akka.http.scaladsl.Http import scala.concurrent.Future import scala.util.{Failure, Success} -class Server(configs: Configs, routes: Routes)(implicit typedSystem: ActorSystem[SpawnProtocol.Command]) { +class Server(port: Int, routes: Routes)(implicit typedSystem: ActorSystem[SpawnProtocol.Command]) { import typedSystem._ def start(): Future[Http.ServerBinding] = { - val f = Http().newServerAt("0.0.0.0", configs.port).bind(routes.route) + val f = Http().newServerAt("0.0.0.0", port).bind(routes.route) f.onComplete { case Success(b) => println(s"Server online at http://${b.localAddress.getHostName}:${b.localAddress.getPort}") diff --git a/esw-segment-db/src/main/scala/esw/segment/server/ServerWiring.scala b/esw-segment-db/src/main/scala/esw/segment/server/ServerWiring.scala index eb7a2ba..241835d 100644 --- a/esw-segment-db/src/main/scala/esw/segment/server/ServerWiring.scala +++ b/esw-segment-db/src/main/scala/esw/segment/server/ServerWiring.scala @@ -1,9 +1,9 @@ package esw.segment.server import esw.segment.db.DbWiring +import DbWiring._ -class ServerWiring(maybePort: Option[Int] = None) extends DbWiring { +class ServerWiring(port: Int, dbName: String = defaultDbName) extends DbWiring(dbName) { lazy val routes = new Routes(segmentToM1PosTable) - lazy val configs = new Configs(maybePort) - lazy val server = new Server(configs, routes) + lazy val server = new Server(port, routes) } diff --git a/esw-segment-db/src/test/scala/esw/segment/db/EswSegmentToM1PosTableTest.scala b/esw-segment-db/src/test/scala/esw/segment/db/EswSegmentToM1PosTableTest.scala index 48e2623..710cc33 100644 --- a/esw-segment-db/src/test/scala/esw/segment/db/EswSegmentToM1PosTableTest.scala +++ b/esw-segment-db/src/test/scala/esw/segment/db/EswSegmentToM1PosTableTest.scala @@ -1,171 +1,12 @@ package esw.segment.db -import java.sql.Date - -import csw.database.scaladsl.JooqExtentions.RichQuery -import csw.logging.api.scaladsl.Logger -import esw.segment.db.SegmentToM1PosTable.{DateRange, SegmentToM1Pos} -import org.scalatest.funsuite.AsyncFunSuite - -import scala.async.Async.{async, await} -import scala.concurrent.Future - -class EswSegmentToM1PosTableTest extends AsyncFunSuite { - val wiring = new DbWiring("test_segment_db") - import wiring.ec +import esw.segment.shared.SegmentToM1ApiTestBase +import EswSegmentToM1PosTableTest._ +object EswSegmentToM1PosTableTest { + import DbWiring._ + val wiring = new DbWiring(testDbName) lazy val posTable: SegmentToM1PosTable = wiring.segmentToM1PosTable - val log: Logger = wiring.log - - private val dateRange1 = DateRange(Date.valueOf("2020-10-21"), Date.valueOf("2020-10-21")) - private val dateRange2 = DateRange(Date.valueOf("2020-10-21"), Date.valueOf("2020-10-22")) - private val dateRange3 = DateRange(Date.valueOf("2020-10-05"), Date.valueOf("2020-10-06")) - private val dateRange4 = DateRange(Date.valueOf("2020-10-01"), Date.valueOf("2020-10-06")) - private val dateRange5 = DateRange(Date.valueOf("2020-10-07"), Date.valueOf("2020-10-07")) -// private val dateRange6 = DateRange(Date.valueOf("2020-10-23"), Date.valueOf("2020-10-23")) - - // Drops and recreates the database tables for testing - private def resetTables(): Future[Boolean] = async { - await(wiring.dsl.truncate(SegmentToM1PosTable.tableName).executeAsyncScala()) >= 0 - } - - private def populateSomeSegments(): Future[Unit] = async { - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-01"), "SN0002", 4)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-02"), "SN0003", 4)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-03"), "SN0003", 4)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-04"), "SN0003", 4)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-05"), "SN0004", 4)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-06"), "SN0004", 4)))) - assert(await(posTable.setPosition(SegmentToM1Pos(Date.valueOf("2020-10-07"), None, 4)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-09"), "SN0004", 4)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0004", 4)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0005", 5)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0004", 4)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0005", 23)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0007", 123)))) - assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-23"), "SN0008", 12)))) - } - - private def populateAllSegments(date: Date): Future[Unit] = async { - val positions = (1 to SegmentToM1PosTable.numSegments).toList.map(n => Some(f"SN$n%04d")) - assert(await(posTable.setAllPositions(date, positions))) - } - - test("Test with all segment positions") { - async { - assert(await(resetTables())) - await(populateAllSegments(dateRange1.from)) - - assert(await(posTable.segmentPositions(dateRange1, "SN0007")) == List( - new SegmentToM1Pos(dateRange1.from, "SN0007", 7))) - assert(await(posTable.segmentPositions(dateRange1, "SN0492")) == List( - new SegmentToM1Pos(dateRange1.from, "SN0492", 492))) - - assert(await(posTable.segmentIds(dateRange1, 5)) == List( - new SegmentToM1Pos(dateRange1.from, "SN0005", 5))) - assert(await(posTable.segmentIds(dateRange1, 6)) == List( - new SegmentToM1Pos(dateRange1.from, "SN0006", 6))) - - // Set some more segment positions (the previous positions are automatically inherited/removed as needed) - await(populateSomeSegments()) - assert(await(posTable.segmentPositions(dateRange1, "SN0007")) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2))) - // "SN0002" was replaced with "SN0007" - assert(await(posTable.segmentPositions(dateRange1, "SN0002")).isEmpty) - // Previous "SN0007" position now empty - assert(await(posTable.segmentIds(dateRange1, 7)).isEmpty) - } - } - - test("Test with single segment positions") { - async { - assert(await(resetTables())) - await(populateSomeSegments()) - - assert(await(posTable.segmentPositions(dateRange1, "SN0007")) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2))) - assert(await(posTable.segmentIds(dateRange1, 2)) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2))) - assert(await(posTable.segmentPositions(dateRange1, "SN0005")) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0005", 5))) - - assert(await(posTable.segmentIds(dateRange1, 5)) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0005", 5))) - assert(await(posTable.segmentIds(dateRange1, 8)).isEmpty) - assert(await(posTable.segmentIds(dateRange1, 4)) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4))) - assert(await(posTable.segmentIds(dateRange3, 4)) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-05"), "SN0004", 4))) - assert(await(posTable.segmentIds(dateRange4, 4)) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-01"), "SN0002", 4), - new SegmentToM1Pos(Date.valueOf("2020-10-02"), "SN0003", 4), - new SegmentToM1Pos(Date.valueOf("2020-10-05"), "SN0004", 4) - )) - assert(await(posTable.segmentIds(dateRange4, 13)).isEmpty) - assert(await(posTable.segmentIds(dateRange5, 4)).isEmpty) - - assert(await(posTable.segmentPositions(dateRange1, "SN0008")).isEmpty) - assert(await(posTable.segmentPositions(dateRange5, "SN0004")).isEmpty) - assert(await(posTable.segmentPositions(dateRange2, "SN0005")) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0005", 5), - new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0005", 23))) - assert(await(posTable.segmentPositions(dateRange2, "SN0007")) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2), - new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0007", 123))) - assert(await(posTable.segmentPositions(dateRange2, "SN0004")) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4))) - - assert(await(posTable.newlyInstalledSegments(Date.valueOf("2020-10-21"))) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2), - new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0005", 5), - new SegmentToM1Pos(Date.valueOf("2020-10-23"), "SN0008", 12), - new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0005", 23), - new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0007", 123), - )) - - assert(await(posTable.currentPositions()) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4), - new SegmentToM1Pos(Date.valueOf("2020-10-23"), "SN0008", 12), - new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0005", 23), - new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0007", 123) - )) - - assert(await(posTable.currentSegmentPosition("SN0007")) - .contains(new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0007", 123))) - - assert(await(posTable.currentSegmentPosition("SN0004")) - .contains(new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4))) - - assert(await(posTable.currentSegmentPosition("SN0005")) - .contains(new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0005", 23))) - - assert(await(posTable.positionsOnDate(Date.valueOf("2020-10-04"))) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-02"), "SN0003", 4), - )) - assert(await(posTable.positionsOnDate(Date.valueOf("2020-10-21"))) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2), - new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4), - new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0005", 5) - )) - - assert(await(posTable.positionsOnDate(Date.valueOf("2020-10-23"))) == List( - new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4), - new SegmentToM1Pos(Date.valueOf("2020-10-23"), "SN0008", 12), - new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0005", 23), - new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0007", 123) - )) - - assert(await(posTable.segmentPositionOnDate(Date.valueOf("2020-10-04"), "SN0003")) - .contains(new SegmentToM1Pos(Date.valueOf("2020-10-02"), "SN0003", 4))) - assert(await(posTable.segmentPositionOnDate(Date.valueOf("2020-10-21"), "SN0007")) - .contains(new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2))) - - assert(await(posTable.segmentAtPositionOnDate(Date.valueOf("2020-10-04"), 4)) - .contains(new SegmentToM1Pos(Date.valueOf("2020-10-02"), "SN0003", 4))) - assert(await(posTable.segmentAtPositionOnDate(Date.valueOf("2020-10-21"), 2)) - .contains(new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2))) - } - } } + +class EswSegmentToM1PosTableTest extends SegmentToM1ApiTestBase(posTable) diff --git a/esw-segment-shared/src/main/scala/esw/segment/shared/EswSegmentData.scala b/esw-segment-shared/src/main/scala/esw/segment/shared/EswSegmentData.scala new file mode 100644 index 0000000..3b65899 --- /dev/null +++ b/esw-segment-shared/src/main/scala/esw/segment/shared/EswSegmentData.scala @@ -0,0 +1,61 @@ +package esw.segment.shared + +import java.sql.Date +import scala.language.implicitConversions + +object EswSegmentData { + + /** + * Number of segments in the TMT primary mirror. + * This is also the size of the positions array in the segment_to_m1_pos table. + */ + val numSegments = 492 + + + implicit def toSqlDate(date: java.util.Date): java.sql.Date = { + new java.sql.Date(date.getTime) + } + + /** + * Position of a segment on a given date + * + * @param date date of record + * @param maybeId the segment id, if the segment is present, None if it is missing + * @param pos position of segment + */ + case class SegmentToM1Pos(date: Date, maybeId: Option[String], pos: Int) { + def this(date: Date, id: String, pos: Int) = { + this(date, Some(id), pos) + } + } + + /** + * Positions of a number of segments on a given date. + * + * @param date date of record + * @param positions a list of pairs of (segment-id, maybe-position) for zero or more segments + * to be set/updated for the given date. + * index+1 is the position. The value is Some(segment-id) or None, if + * a segment is missing. + */ + case class SegmentToM1Positions(date: Date, positions: List[(Option[String], Int)]) + + /** + * All of the segment positions for the given date. + * The first item in the positions list is taken to be the segment position 1 and so on. + * + * @param date the date corresponding to the positions + * @param allPositions list of all 492 segment positions (Missing segments should be None, + * present segments should be Some(segment-id) + */ + case class AllSegmentPositions(date: Date, allPositions: List[Option[String]]) + + /** + * A range of dates + * + * @param from start of the date range + * @param to end of the date range + */ + case class DateRange(from: Date, to: Date) + +} diff --git a/esw-segment-db/src/main/scala/esw/segment/db/JsonSupport.scala b/esw-segment-shared/src/main/scala/esw/segment/shared/JsonSupport.scala similarity index 76% rename from esw-segment-db/src/main/scala/esw/segment/db/JsonSupport.scala rename to esw-segment-shared/src/main/scala/esw/segment/shared/JsonSupport.scala index 673cec1..3bea469 100644 --- a/esw-segment-db/src/main/scala/esw/segment/db/JsonSupport.scala +++ b/esw-segment-shared/src/main/scala/esw/segment/shared/JsonSupport.scala @@ -1,11 +1,10 @@ -package esw.segment.db +package esw.segment.shared import java.sql.Date import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -import esw.segment.db.SegmentToM1PosTable.{AllSegmentPositions, DateRange, SegmentToM1Pos, SegmentToM1Positions} -import spray.json.{DefaultJsonProtocol, DeserializationException, JsNumber, JsValue, RootJsonFormat} - +import esw.segment.shared.EswSegmentData._ +import spray.json._ //noinspection TypeAnnotation trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { diff --git a/esw-segment-shared/src/main/scala/esw/segment/shared/SegmentToM1Api.scala b/esw-segment-shared/src/main/scala/esw/segment/shared/SegmentToM1Api.scala new file mode 100644 index 0000000..e1b75da --- /dev/null +++ b/esw-segment-shared/src/main/scala/esw/segment/shared/SegmentToM1Api.scala @@ -0,0 +1,117 @@ +package esw.segment.shared + +import java.sql.Date + +import esw.segment.shared.EswSegmentData.{DateRange, SegmentToM1Pos} + +import scala.concurrent.Future + +trait SegmentToM1Api { + + /** + * Sets or updates the date and position of the given segment in the table and returns true if successful + * + * @param segmentToM1Pos holds the date, id and pos + */ + def setPosition(segmentToM1Pos: SegmentToM1Pos): Future[Boolean] + + /** + * Sets or updates the positions of the given segments for the given date in the table and returns true if successful. + * + * @param date the date corresponding to the positions + * @param positions a list of pairs of (segment-id, position) for zero or more segments to be set/updated + * for the given date + * @return true if there were no problems + */ + def setPositions(date: Date, positions: List[(Option[String], Int)]): Future[Boolean] + + /** + * Sets all of the segment positions for the given date and returns true if successful. + * The first item in the positions list is taken to be the segment position 1 and so on. + * + * @param date the date corresponding to the positions + * @param allPositions list of all 492 segment positions (Missing segments should be None, + * present segments should be Some(segment-id) + * @return true if all is OK, false if the number of positions is less than 492 or the table row + * could not be added or updated + */ + def setAllPositions(date: Date, allPositions: List[Option[String]]): Future[Boolean] + + /** + * Gets a list of segments positions for the given segment id in the given date range. + * + * @param dateRange the range of dates to search + * @param segmentId the segment id to search for + * @return a list of objects indicating the positions of the given segment id in the given date range (sorted by position) + */ + def segmentPositions(dateRange: DateRange, segmentId: String): Future[List[SegmentToM1Pos]] + + /** + * Gets a list of segment ids that were in the given position in the given date range. + * + * @param dateRange the range of dates to search + * @param pos the segment position to search for + * @return a list of segments at the given position in the given date range (sorted by id) + */ + def segmentIds(dateRange: DateRange, pos: Int): Future[List[SegmentToM1Pos]] + + /** + * Returns a list of segments that were installed since the given date + * + * @param since the cutoff date for newly installed segments + */ + def newlyInstalledSegments(since: Date): Future[List[SegmentToM1Pos]] + + /** + * Returns the current segment positions, sorted by position + * (Missing segments are not included in the returned list). + */ + def currentPositions(): Future[List[SegmentToM1Pos]] + + /** + * Gets the current segment position for the given segment id. + * + * @param segmentId the segment id to search for + * @return Some object indicating the positions of the given segment, or None if the segment is not installed + */ + def currentSegmentPosition(segmentId: String): Future[Option[SegmentToM1Pos]] + + /** + * Gets the id of the segment currently in the given position. + * + * @param pos the segment position to search for + * @return Some object indicating the id of the segment, or None if no segment is installed at that position + */ + def currentSegmentAtPosition(pos: Int): Future[Option[SegmentToM1Pos]] + + /** + * Returns the segment positions as they were on the given date, sorted by position + * (Missing segments are not included in the returned list). + */ + def positionsOnDate(date: Date): Future[List[SegmentToM1Pos]] + + /** + * Gets the segment position for the given segment id on the given date. + * + * @param date the date that the segment was in the position + * @param segmentId the segment id to search for + * @return Some object indicating the positions of the given segment, or None if the segment is not installed + */ + def segmentPositionOnDate(date: Date, segmentId: String): Future[Option[SegmentToM1Pos]] + + /** + * Gets the id of the segment that was installed in the given position on the given date. + * + * @param date the date that the segment was in the position + * @param pos the segment position to search for + * @return Some object indicating the id of the segment, or None if no segment was installed at that position on the given date + */ + def segmentAtPositionOnDate(date: Date, pos: Int): Future[Option[SegmentToM1Pos]] + + /** + * Drops and recreates the database tables (for testing) + * + * @return true if OK + */ + def resetTables(): Future[Boolean] +} \ No newline at end of file diff --git a/esw-segment-shared/src/test/scala/esw/segment/shared/SegmentToM1ApiTestBase.scala b/esw-segment-shared/src/test/scala/esw/segment/shared/SegmentToM1ApiTestBase.scala new file mode 100644 index 0000000..297091a --- /dev/null +++ b/esw-segment-shared/src/test/scala/esw/segment/shared/SegmentToM1ApiTestBase.scala @@ -0,0 +1,159 @@ +package esw.segment.shared + +import java.sql.Date + +import esw.segment.shared.EswSegmentData.{DateRange, SegmentToM1Pos} +import EswSegmentData._ +import org.scalatest.funsuite.AsyncFunSuite + +import scala.async.Async.{async, await} +import scala.concurrent.Future + +class SegmentToM1ApiTestBase(posTable: SegmentToM1Api) extends AsyncFunSuite { + private val dateRange1 = DateRange(Date.valueOf("2020-10-21"), Date.valueOf("2020-10-21")) + private val dateRange2 = DateRange(Date.valueOf("2020-10-21"), Date.valueOf("2020-10-22")) + private val dateRange3 = DateRange(Date.valueOf("2020-10-05"), Date.valueOf("2020-10-06")) + private val dateRange4 = DateRange(Date.valueOf("2020-10-01"), Date.valueOf("2020-10-06")) + private val dateRange5 = DateRange(Date.valueOf("2020-10-07"), Date.valueOf("2020-10-07")) + + private def populateSomeSegments(): Future[Unit] = async { + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-01"), "SN0002", 4)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-02"), "SN0003", 4)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-03"), "SN0003", 4)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-04"), "SN0003", 4)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-05"), "SN0004", 4)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-06"), "SN0004", 4)))) + assert(await(posTable.setPosition(SegmentToM1Pos(Date.valueOf("2020-10-07"), None, 4)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-09"), "SN0004", 4)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0004", 4)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0005", 5)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0004", 4)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0005", 23)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0007", 123)))) + assert(await(posTable.setPosition(new SegmentToM1Pos(Date.valueOf("2020-10-23"), "SN0008", 12)))) + } + + private def populateAllSegments(date: Date): Future[Unit] = async { + val positions = (1 to numSegments).toList.map(n => Some(f"SN$n%04d")) + assert(await(posTable.setAllPositions(date, positions))) + } + + test("Test with all segment positions") { + async { + assert(await(posTable.resetTables())) + await(populateAllSegments(dateRange1.from)) + + assert(await(posTable.segmentPositions(dateRange1, "SN0007")) == List( + new SegmentToM1Pos(dateRange1.from, "SN0007", 7))) + assert(await(posTable.segmentPositions(dateRange1, "SN0492")) == List( + new SegmentToM1Pos(dateRange1.from, "SN0492", 492))) + + assert(await(posTable.segmentIds(dateRange1, 5)) == List( + new SegmentToM1Pos(dateRange1.from, "SN0005", 5))) + assert(await(posTable.segmentIds(dateRange1, 6)) == List( + new SegmentToM1Pos(dateRange1.from, "SN0006", 6))) + + // Set some more segment positions (the previous positions are automatically inherited/removed as needed) + await(populateSomeSegments()) + assert(await(posTable.segmentPositions(dateRange1, "SN0007")) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2))) + // "SN0002" was replaced with "SN0007" + assert(await(posTable.segmentPositions(dateRange1, "SN0002")).isEmpty) + // Previous "SN0007" position now empty + assert(await(posTable.segmentIds(dateRange1, 7)).isEmpty) + } + } + + test("Test with single segment positions") { + async { + assert(await(posTable.resetTables())) + await(populateSomeSegments()) + + assert(await(posTable.segmentPositions(dateRange1, "SN0007")) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2))) + assert(await(posTable.segmentIds(dateRange1, 2)) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2))) + assert(await(posTable.segmentPositions(dateRange1, "SN0005")) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0005", 5))) + + assert(await(posTable.segmentIds(dateRange1, 5)) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0005", 5))) + assert(await(posTable.segmentIds(dateRange1, 8)).isEmpty) + assert(await(posTable.segmentIds(dateRange1, 4)) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4))) + assert(await(posTable.segmentIds(dateRange3, 4)) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-05"), "SN0004", 4))) + assert(await(posTable.segmentIds(dateRange4, 4)) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-01"), "SN0002", 4), + new SegmentToM1Pos(Date.valueOf("2020-10-02"), "SN0003", 4), + new SegmentToM1Pos(Date.valueOf("2020-10-05"), "SN0004", 4) + )) + assert(await(posTable.segmentIds(dateRange4, 13)).isEmpty) + assert(await(posTable.segmentIds(dateRange5, 4)).isEmpty) + + assert(await(posTable.segmentPositions(dateRange1, "SN0008")).isEmpty) + assert(await(posTable.segmentPositions(dateRange5, "SN0004")).isEmpty) + assert(await(posTable.segmentPositions(dateRange2, "SN0005")) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0005", 5), + new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0005", 23))) + assert(await(posTable.segmentPositions(dateRange2, "SN0007")) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2), + new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0007", 123))) + assert(await(posTable.segmentPositions(dateRange2, "SN0004")) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4))) + + assert(await(posTable.newlyInstalledSegments(Date.valueOf("2020-10-21"))) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2), + new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0005", 5), + new SegmentToM1Pos(Date.valueOf("2020-10-23"), "SN0008", 12), + new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0005", 23), + new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0007", 123), + )) + + assert(await(posTable.currentPositions()) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4), + new SegmentToM1Pos(Date.valueOf("2020-10-23"), "SN0008", 12), + new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0005", 23), + new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0007", 123) + )) + + assert(await(posTable.currentSegmentPosition("SN0007")) + .contains(new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0007", 123))) + + assert(await(posTable.currentSegmentPosition("SN0004")) + .contains(new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4))) + + assert(await(posTable.currentSegmentPosition("SN0005")) + .contains(new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0005", 23))) + + assert(await(posTable.positionsOnDate(Date.valueOf("2020-10-04"))) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-02"), "SN0003", 4), + )) + assert(await(posTable.positionsOnDate(Date.valueOf("2020-10-21"))) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2), + new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4), + new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0005", 5) + )) + + assert(await(posTable.positionsOnDate(Date.valueOf("2020-10-23"))) == List( + new SegmentToM1Pos(Date.valueOf("2020-10-08"), "SN0004", 4), + new SegmentToM1Pos(Date.valueOf("2020-10-23"), "SN0008", 12), + new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0005", 23), + new SegmentToM1Pos(Date.valueOf("2020-10-22"), "SN0007", 123) + )) + + assert(await(posTable.segmentPositionOnDate(Date.valueOf("2020-10-04"), "SN0003")) + .contains(new SegmentToM1Pos(Date.valueOf("2020-10-02"), "SN0003", 4))) + assert(await(posTable.segmentPositionOnDate(Date.valueOf("2020-10-21"), "SN0007")) + .contains(new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2))) + + assert(await(posTable.segmentAtPositionOnDate(Date.valueOf("2020-10-04"), 4)) + .contains(new SegmentToM1Pos(Date.valueOf("2020-10-02"), "SN0003", 4))) + assert(await(posTable.segmentAtPositionOnDate(Date.valueOf("2020-10-21"), 2)) + .contains(new SegmentToM1Pos(Date.valueOf("2020-10-21"), "SN0007", 2))) + } + } + +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index d6cd096..655f723 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -2,12 +2,31 @@ import sbt._ object Dependencies { - val `databaseTests-deps` = Seq( + val `esw-segment-db-deps` = Seq( CSW.`csw-location-client`, CSW.`csw-database`, AkkaHttp.`akka-http`, AkkaHttp.`akka-http-core`, AkkaHttp.`akka-http-spray-json`, - Libs.`scalaTest` % Test + Libs.`scopt`, + Libs.`scalaTest` % Test + ) + + val `esw-segment-shared-deps` = Seq( + AkkaHttp.`akka-http-spray-json`, + Libs.`scala-async` % Test, + Libs.`scalaTest` % Test, + Akka.`akka-stream` + ) + + val `esw-segment-client-deps` = Seq( + AkkaHttp.`akka-http`, + AkkaHttp.`akka-http-core`, + AkkaHttp.`akka-http-spray-json`, + Akka.`akka-actor`, + Akka.`akka-stream`, + Libs.`scala-async`, + Libs.`scopt`, + Libs.`scalaTest` % Test ) } diff --git a/project/Libs.scala b/project/Libs.scala index 93b6c5f..98ef7c9 100644 --- a/project/Libs.scala +++ b/project/Libs.scala @@ -3,6 +3,7 @@ import sbt._ object Libs { val `scopt` = "com.github.scopt" %% "scopt" % "3.7.1" //MIT License val `scalaTest` = "org.scalatest" %% "scalatest" % "3.1.4" // ApacheV2 + val `scala-async` = "org.scala-lang.modules" %% "scala-async" % "1.0.0-M1" //BSD 3-clause "New" or "Revised" License } object CSW { @@ -24,3 +25,22 @@ object AkkaHttp { //ApacheV2 val `akka-http-spray-json` = "com.typesafe.akka" %% "akka-http-spray-json" % Version } +object Akka { + val Version = "2.6.10" //all akka is Apache License 2.0 + + val `akka-stream` = "com.typesafe.akka" %% "akka-stream" % Version + val `akka-stream-typed` = "com.typesafe.akka" %% "akka-stream-typed" % Version + val `akka-remote` = "com.typesafe.akka" %% "akka-remote" % Version + val `akka-stream-testkit` = "com.typesafe.akka" %% "akka-stream-testkit" % Version + val `akka-actor` = "com.typesafe.akka" %% "akka-actor" % Version + val `akka-actor-typed` = "com.typesafe.akka" %% "akka-actor-typed" % Version + val `akka-actor-testkit-typed` = "com.typesafe.akka" %% "akka-actor-testkit-typed" % Version + val `akka-distributed-data` = "com.typesafe.akka" %% "akka-distributed-data" % Version + val `akka-multi-node-testkit` = "com.typesafe.akka" %% "akka-multi-node-testkit" % Version + val `akka-cluster-tools` = "com.typesafe.akka" %% "akka-cluster-tools" % Version + val `akka-cluster` = "com.typesafe.akka" %% "akka-cluster" % Version + val `akka-cluster-typed` = "com.typesafe.akka" %% "akka-cluster-typed" % Version + val `akka-slf4j` = "com.typesafe.akka" %% "akka-slf4j" % Version + val `cluster-sharding` = "com.typesafe.akka" %% "akka-cluster-sharding" % Version + val `akka-persistence` = "com.typesafe.akka" %% "akka-persistence" % Version +} diff --git a/project/plugins.sbt b/project/plugins.sbt index eff4a1d..4e8763c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,4 @@ -addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.7.5") +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.7.5") addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.9.0") diff --git a/scripts/init-db.sh b/scripts/init-db.sh new file mode 100755 index 0000000..1a4493a --- /dev/null +++ b/scripts/init-db.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +# Initialize the ESW Segment DB database, test database and tables +# (Do this once on installation. Edit ~/.pgpass to avoid retyping Postgres password.) + +psql postgres -h localhost -f init-db.sql +psql postgres -h localhost -f init-test-db.sql +psql esw_segment_db -h localhost -f init-tables.sql +psql test_segment_db -h localhost -f init-tables.sql diff --git a/scripts/init-db.sql b/scripts/init-db.sql index 45eab63..0af8862 100644 --- a/scripts/init-db.sql +++ b/scripts/init-db.sql @@ -4,4 +4,3 @@ DROP DATABASE IF EXISTS esw_segment_db; CREATE DATABASE esw_segment_db; -CREATE TABLE segment_to_m1_pos (date DATE NOT NULL PRIMARY KEY, positions CHARACTER(6)[492]); diff --git a/scripts/init-tables.sql b/scripts/init-tables.sql new file mode 100644 index 0000000..60bfa0a --- /dev/null +++ b/scripts/init-tables.sql @@ -0,0 +1,5 @@ +/* + * Initialize the ESW Segment Database tables + */ + +CREATE TABLE segment_to_m1_pos (date DATE NOT NULL PRIMARY KEY, positions CHARACTER(6)[492]); diff --git a/scripts/init-test-db.sql b/scripts/init-test-db.sql index 4d43993..201dd89 100644 --- a/scripts/init-test-db.sql +++ b/scripts/init-test-db.sql @@ -4,4 +4,3 @@ DROP DATABASE IF EXISTS test_segment_db; CREATE DATABASE test_segment_db; -CREATE TABLE test_segment_db (date DATE NOT NULL PRIMARY KEY, positions CHARACTER(6)[492]);