From 1863107786e2b9658e5dd81e98d4fee1f9d11a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Sat, 19 Jul 2025 11:38:10 +0200 Subject: [PATCH 1/3] init support for scala 2.13 --- build.sbt | 130 ++- common/build.sbt | 16 +- .../mustache/package.scala | 0 .../main/scala-2.13/mustache/package.scala | 666 +++++++++++++++ .../app/softnetwork/concurrent/package.scala | 2 +- .../test/scala/mustache/MustacheSpec.scala | 4 - core/build.sbt | 2 +- .../persistence/typed/scaladsl/Patterns.scala | 4 +- .../service/SingletonServiceSpec.scala | 2 +- .../typed/scaladsl/SingletonPatternSpec.scala | 2 +- core/testkit/src/main/resources/logback.xml | 2 + counter/src/test/resources/application.conf | 3 + elastic/build.sbt | 34 - .../main/resources/mapping/default.mustache | 16 - .../main/resources/softnetwork-elastic.conf | 21 - .../elastic/client/ElasticClientApi.scala | 507 ------------ .../elastic/client/jest/JestClientApi.scala | 680 ---------------- .../client/jest/JestClientCompanion.scala | 160 ---- .../client/jest/JestClientResultHandler.scala | 44 - .../elastic/client/jest/JestProvider.scala | 11 - .../softnetwork/elastic/client/package.scala | 169 ---- .../persistence/query/ElasticProvider.scala | 175 ---- .../query/State2ElasticProcessorStream.scala | 24 - ...asticProcessorStreamWithJestProvider.scala | 10 - .../elastic/persistence/typed/Elastic.scala | 31 - .../elastic/sql/ElasticFilters.scala | 138 ---- .../elastic/sql/ElasticQuery.scala | 174 ---- .../elastic/sql/SQLImplicits.scala | 30 - .../softnetwork/elastic/sql/SQLParser.scala | 303 ------- .../app/softnetwork/elastic/sql/package.scala | 406 ---------- .../elastic/sql/ElasticFiltersSpec.scala | 756 ------------------ .../elastic/sql/ElasticQuerySpec.scala | 467 ----------- .../elastic/sql/SQLLiteralSpec.scala | 18 - .../elastic/sql/SQLParserSpec.scala | 271 ------- elastic/testkit/build.sbt | 31 - .../elastic/client/MockElasticClientApi.scala | 238 ------ .../scalatest/ElasticDockerTestKit.scala | 22 - .../elastic/scalatest/ElasticTestKit.scala | 313 -------- .../scalatest/EmbeddedElasticTestKit.scala | 34 - .../person/JdbcPersonToElasticTestKit.scala | 28 - .../person/MySQLPersonToElasticTestKit.scala | 5 - .../person/PersonToElasticTestKit.scala | 30 - .../PostgresPersonToElasticTestKit.scala | 5 - .../PersonToElasticProcessorStream.scala | 15 - .../src/test/resources/application.conf | 3 - elastic/testkit/src/test/resources/avatar.jpg | Bin 34169 -> 0 bytes elastic/testkit/src/test/resources/avatar.pdf | Bin 112041 -> 0 bytes elastic/testkit/src/test/resources/avatar.png | Bin 106122 -> 0 bytes .../test/resources/mapping/sample.mustache | 16 - .../elastic/client/ElasticClientSpec.scala | 468 ----------- .../elastic/client/JestProviders.scala | 38 - .../softnetwork/elastic/model/Binary.scala | 13 - .../softnetwork/elastic/model/Sample.scala | 13 - .../MySQLPersonToElasticHandlerSpec.scala | 7 - .../person/PersonToElasticHandlerSpec.scala | 31 - .../PostgresPersonToElasticHandlerSpec.scala | 7 - jdbc/build.sbt | 2 +- .../query/JdbcEventProcessorOffsets.scala | 6 +- .../jdbc/query/JdbcStateProvider.scala | 10 +- project/Versions.scala | 24 +- session/common/build.sbt | 2 +- .../session/model/JwtClaimsEncoder.scala | 4 +- .../scalatest/RefreshableSessionTestKit.scala | 2 +- .../session/scalatest/SessionTestKit.scala | 2 +- 64 files changed, 802 insertions(+), 5845 deletions(-) rename common/src/main/{scala => scala-2.12}/mustache/package.scala (100%) create mode 100644 common/src/main/scala-2.13/mustache/package.scala create mode 100644 counter/src/test/resources/application.conf delete mode 100644 elastic/build.sbt delete mode 100644 elastic/src/main/resources/mapping/default.mustache delete mode 100644 elastic/src/main/resources/softnetwork-elastic.conf delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/client/ElasticClientApi.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestClientApi.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestClientCompanion.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestClientResultHandler.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestProvider.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/client/package.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticProvider.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStream.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJestProvider.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/persistence/typed/Elastic.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/sql/ElasticFilters.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/sql/ElasticQuery.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/sql/SQLImplicits.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/sql/SQLParser.scala delete mode 100644 elastic/src/main/scala/app/softnetwork/elastic/sql/package.scala delete mode 100644 elastic/src/test/scala/app/softnetwork/elastic/sql/ElasticFiltersSpec.scala delete mode 100644 elastic/src/test/scala/app/softnetwork/elastic/sql/ElasticQuerySpec.scala delete mode 100644 elastic/src/test/scala/app/softnetwork/elastic/sql/SQLLiteralSpec.scala delete mode 100644 elastic/src/test/scala/app/softnetwork/elastic/sql/SQLParserSpec.scala delete mode 100644 elastic/testkit/build.sbt delete mode 100644 elastic/testkit/src/main/scala/app/softnetwork/elastic/client/MockElasticClientApi.scala delete mode 100644 elastic/testkit/src/main/scala/app/softnetwork/elastic/scalatest/ElasticDockerTestKit.scala delete mode 100644 elastic/testkit/src/main/scala/app/softnetwork/elastic/scalatest/ElasticTestKit.scala delete mode 100644 elastic/testkit/src/main/scala/app/softnetwork/elastic/scalatest/EmbeddedElasticTestKit.scala delete mode 100644 elastic/testkit/src/main/scala/app/softnetwork/persistence/person/JdbcPersonToElasticTestKit.scala delete mode 100644 elastic/testkit/src/main/scala/app/softnetwork/persistence/person/MySQLPersonToElasticTestKit.scala delete mode 100644 elastic/testkit/src/main/scala/app/softnetwork/persistence/person/PersonToElasticTestKit.scala delete mode 100644 elastic/testkit/src/main/scala/app/softnetwork/persistence/person/PostgresPersonToElasticTestKit.scala delete mode 100644 elastic/testkit/src/main/scala/app/softnetwork/persistence/person/query/PersonToElasticProcessorStream.scala delete mode 100644 elastic/testkit/src/test/resources/application.conf delete mode 100644 elastic/testkit/src/test/resources/avatar.jpg delete mode 100644 elastic/testkit/src/test/resources/avatar.pdf delete mode 100644 elastic/testkit/src/test/resources/avatar.png delete mode 100644 elastic/testkit/src/test/resources/mapping/sample.mustache delete mode 100644 elastic/testkit/src/test/scala/app/softnetwork/elastic/client/ElasticClientSpec.scala delete mode 100644 elastic/testkit/src/test/scala/app/softnetwork/elastic/client/JestProviders.scala delete mode 100644 elastic/testkit/src/test/scala/app/softnetwork/elastic/model/Binary.scala delete mode 100644 elastic/testkit/src/test/scala/app/softnetwork/elastic/model/Sample.scala delete mode 100644 elastic/testkit/src/test/scala/app/softnetwork/persistence/person/MySQLPersonToElasticHandlerSpec.scala delete mode 100644 elastic/testkit/src/test/scala/app/softnetwork/persistence/person/PersonToElasticHandlerSpec.scala delete mode 100644 elastic/testkit/src/test/scala/app/softnetwork/persistence/person/PostgresPersonToElasticHandlerSpec.scala diff --git a/build.sbt b/build.sbt index e5744267..34e86a4b 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,8 @@ import app.softnetwork.* +lazy val scala212 = "2.12.20" +lazy val scala213 = "2.13.16" + ///////////////////////////////// // Defaults ///////////////////////////////// @@ -8,11 +11,22 @@ ThisBuild / organization := "app.softnetwork" name := "generic-persistence-api" -ThisBuild / version := "0.7.3" +ThisBuild / version := "0.8-SNAPSHOT" + +lazy val moduleSettings = Seq( + crossScalaVersions := Seq(scala212, scala213), + scalacOptions ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, 12)) => Seq("-deprecation", "-feature", "-target:jvm-1.8", "-Ypartial-unification") + case Some((2, 13)) => Seq("-deprecation", "-feature", "-target:jvm-1.8") + case _ => Seq.empty + } + } +) -ThisBuild / scalaVersion := "2.12.18" +ThisBuild / scalaVersion := scala212 -ThisBuild / scalacOptions ++= Seq("-deprecation", "-feature", "-target:jvm-1.8", "-Ypartial-unification") +//ThisBuild / versionScheme := Some("early-semver") ThisBuild / javacOptions ++= Seq("-source", "1.8", "-target", "1.8") @@ -24,20 +38,32 @@ ThisBuild / resolvers ++= Seq( ThisBuild / libraryDependencies ++= Seq( "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf", - "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.1" + "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.2" ) ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always +ThisBuild / dependencyOverrides ++= Seq( + "com.github.jnr" % "jnr-ffi" % "2.2.17", + "com.github.jnr" % "jffi" % "1.3.13" classifier "native", + "org.lmdbjava" % "lmdbjava" % "0.9.1" exclude("org.slf4j", "slf4j-api"), +) + Test / parallelExecution := false lazy val common = project.in(file("common")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) lazy val commonTestkit = project.in(file("common/testkit")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .dependsOn( common % "compile->compile;test->test;it->it" ) @@ -45,14 +71,21 @@ lazy val commonTestkit = project.in(file("common/testkit")) lazy val core = project.in(file("core")) .configs(IntegrationTest) .enablePlugins(BuildInfoPlugin) - .settings(Defaults.itSettings, app.softnetwork.Info.infoSettings) + .settings( + Defaults.itSettings, + app.softnetwork.Info.infoSettings, + moduleSettings + ) .dependsOn( common % "compile->compile;test->test;it->it" ) lazy val coreTestkit = project.in(file("core/testkit")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .dependsOn( core % "compile->compile;test->test;it->it" ) @@ -62,7 +95,10 @@ lazy val coreTestkit = project.in(file("core/testkit")) lazy val server = project.in(file("server")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .enablePlugins(AkkaGrpcPlugin) .dependsOn( core % "compile->compile;test->test;it->it" @@ -70,7 +106,10 @@ lazy val server = project.in(file("server")) lazy val serverTestkit = project.in(file("server/testkit")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .dependsOn( server % "compile->compile;test->test;it->it" ) @@ -80,7 +119,10 @@ lazy val serverTestkit = project.in(file("server/testkit")) lazy val sessionCommon = project.in(file("session/common")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .enablePlugins(AkkaGrpcPlugin) .dependsOn( server % "compile->compile;test->test;it->it" @@ -88,14 +130,20 @@ lazy val sessionCommon = project.in(file("session/common")) lazy val sessionCore = project.in(file("session/core")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .dependsOn( sessionCommon % "compile->compile;test->test;it->it" ) lazy val sessionTestkit = project.in(file("session/testkit")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .dependsOn( sessionCore % "compile->compile;test->test;it->it" ) @@ -105,14 +153,20 @@ lazy val sessionTestkit = project.in(file("session/testkit")) lazy val jdbc = project.in(file("jdbc")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .dependsOn( core % "compile->compile;test->test;it->it" ) lazy val jdbcTestkit = project.in(file("jdbc/testkit")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .dependsOn( jdbc % "compile->compile;test->test;it->it" ) @@ -122,47 +176,33 @@ lazy val jdbcTestkit = project.in(file("jdbc/testkit")) lazy val cassandra = project.in(file("cassandra")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .dependsOn( core % "compile->compile;test->test;it->it" ) lazy val counter = project.in(file("counter")) .configs(IntegrationTest) - .settings(Defaults.itSettings) - .dependsOn( - core % "compile->compile;test->test;it->it" + .settings( + Defaults.itSettings, + moduleSettings ) - .dependsOn( - coreTestkit % "test->test;it->it" - ) - -lazy val elastic = project.in(file("elastic")) - .configs(IntegrationTest) - .settings(Defaults.itSettings) .dependsOn( core % "compile->compile;test->test;it->it" ) - -lazy val elasticTestkit = project.in(file("elastic/testkit")) - .configs(IntegrationTest) - .settings(Defaults.itSettings) - .dependsOn( - elastic % "compile->compile;test->test;it->it" - ) .dependsOn( - commonTestkit % "compile->compile;test->test;it->it" - ) - .dependsOn( - coreTestkit % "compile->compile;test->test;it->it" - ) - .dependsOn( - jdbcTestkit % "compile->compile;test->test;it->it" + coreTestkit % "test->test;it->it" ) lazy val kv = project.in(file("kv")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .enablePlugins(AkkaGrpcPlugin) .dependsOn( core % "compile->compile;test->test;it->it" @@ -181,8 +221,6 @@ lazy val root = project.in(file(".")) jdbcTestkit, // cassandra, counter, - elastic, - elasticTestkit, kv, server, serverTestkit, @@ -191,7 +229,11 @@ lazy val root = project.in(file(".")) sessionTestkit ) .configs(IntegrationTest) - .settings(Defaults.itSettings, Publish.noPublishSettings) + .settings( + Defaults.itSettings, + Publish.noPublishSettings, + crossScalaVersions := Nil + ) Test / envVars := Map( "POSTGRES_USER" -> "admin", diff --git a/common/build.sbt b/common/build.sbt index 24886f0d..75679b45 100644 --- a/common/build.sbt +++ b/common/build.sbt @@ -10,14 +10,22 @@ val configDependencies = Seq( ) val jackson = Seq( - "com.fasterxml.jackson.core" % "jackson-databind" % Versions.jackson, - "com.fasterxml.jackson.core" % "jackson-core" % Versions.jackson, - "com.fasterxml.jackson.core" % "jackson-annotations" % Versions.jackson, - "com.fasterxml.jackson.module" % "jackson-module-scala_2.12" % Versions.jackson + "com.fasterxml.jackson.core" % "jackson-databind" % Versions.jackson, + "com.fasterxml.jackson.core" % "jackson-core" % Versions.jackson, + "com.fasterxml.jackson.core" % "jackson-annotations" % Versions.jackson, + "com.fasterxml.jackson.dataformat" % "jackson-dataformat-cbor" % Versions.jackson, + "com.fasterxml.jackson.dataformat" % "jackson-dataformat-yaml" % Versions.jackson, + "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % Versions.jackson, + "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % Versions.jackson, + "com.fasterxml.jackson.module" % "jackson-module-parameter-names" % Versions.jackson, + "com.fasterxml.jackson.module" %% "jackson-module-scala" % Versions.jackson, ) val jacksonExclusions = Seq( ExclusionRule(organization = "com.fasterxml.jackson.core"), + ExclusionRule(organization = "com.fasterxml.jackson.dataformat"), + ExclusionRule(organization = "com.fasterxml.jackson.datatype"), + ExclusionRule(organization = "com.fasterxml.jackson.module"), ExclusionRule(organization = "org.codehaus.jackson") ) diff --git a/common/src/main/scala/mustache/package.scala b/common/src/main/scala-2.12/mustache/package.scala similarity index 100% rename from common/src/main/scala/mustache/package.scala rename to common/src/main/scala-2.12/mustache/package.scala diff --git a/common/src/main/scala-2.13/mustache/package.scala b/common/src/main/scala-2.13/mustache/package.scala new file mode 100644 index 00000000..b5ab62e6 --- /dev/null +++ b/common/src/main/scala-2.13/mustache/package.scala @@ -0,0 +1,666 @@ +package mustache + +import scala.annotation.tailrec +import scala.io.Source +import scala.concurrent.{Await, Awaitable} +import scala.concurrent.duration._ + +/** @author + * vspy - https://github.com/vspy/scala-mustache + */ +import java.io.{File => JFile} + + import app.softnetwork.config.Settings + import org.apache.commons.text.StringEscapeUtils + + case class MustacheParseException(line: Int, msg: String) + extends Exception("Line " + line + ": " + msg) + + /** view helper trait + */ + trait MustacheHelperSupport { + private val contextLocal = new java.lang.ThreadLocal[Any]() + private val renderLocal = new java.lang.ThreadLocal[Function1[String, String]]() + + protected def context: Any = contextLocal.get + protected def render(template: String): Any = + renderLocal.get()(template) + + def withContextAndRenderFn[A](context: Any, render: String => String)(fn: => A): A = { + contextLocal.set(context) + renderLocal.set(render) + try { fn } + finally { + contextLocal.set(null) + renderLocal.set(null) + } + } + } + + /** template + */ + class Mustache( + root: Token + ) extends MustacheHelperSupport { + + def this(source: Source, open: String = "{{", close: String = "}}") = + this(new Parser { + val src: Source = source + var otag: String = open + var ctag: String = close + }.parse()) + + def this(str: String) = this(Source.fromString(str)) + + def this( + str: String, + open: String, + close: String + ) = this(Source.fromString(str), open, close) + + private val compiledTemplate = root + + val globals: Map[String, Any] = { + val excludedGlobals = List("wait", "toString", "hashCode", "getClass", "notify", "notifyAll") + Map( + this.getClass.getMethods + .filter(x => { + val name = x.getName + val pt = x.getParameterTypes + (!name.startsWith("render$default")) && ( + !name.startsWith("product$default") + ) && ( + !name.startsWith("init$default") + ) && ( + !excludedGlobals.contains(name) + ) && (pt.isEmpty || ( + pt.length == 1 + && pt(0) == classOf[String] + )) + }) + .toIndexedSeq.map(x => { + x.getName -> + (if (x.getParameterTypes.isEmpty) () => { + x.invoke(this) + } + else + (str: String) => { + x.invoke(this, str) + }) + }): _* + ) + } + + def renderHtml4( + context: Any = null, + partials: Map[String, Mustache] = Map(), + callstack: List[Any] = List(this) + ): String = { + context match { + case m: Map[String, Any] => + product( + m.map(kv => + kv._2 match { + case s: String => kv._1 -> StringEscapeUtils.escapeHtml4(s) + case v => kv._1 -> v + } + ), + partials, + callstack + ).toString + case _ => product(context, partials, callstack).toString + } + } + + def render( + context: Any = null, + partials: Map[String, Mustache] = Map(), + callstack: List[Any] = List(this) + ): String = product(context, partials, callstack).toString + + def product( + context: Any = null, + partials: Map[String, Mustache] = Map(), + callstack: List[Any] = List(this) + ): TokenProduct = compiledTemplate.render(context, partials, callstack) + + } + + private class ParserState + private object Text extends ParserState + private object OTag extends ParserState + private object Tag extends ParserState + private object CTag extends ParserState + + private abstract class Parser { + val src: Source + + var state: ParserState = Text + var otag: String + var ctag: String + var tagPosition: Int = 0 + var line: Int = 1 + var prev: Char = '\uffff' + var cur: Char = '\uffff' + var curlyBraceTag: Boolean = false + var stack: List[Token] = List() + + val buf = new StringBuilder(8192) + + def parse(): Token = { + while (consume) { + state match { + case Text => + if (cur == otag.charAt(0)) + if (otag.length > 1) { tagPosition = 1; state = OTag } + else { staticText(); state = Tag } + else buf.append(cur) + + case OTag => + if (cur == otag.charAt(tagPosition)) + if (tagPosition == otag.length - 1) { staticText(); state = Tag } + else { tagPosition = tagPosition + 1 } + else { notOTag(); buf.append(cur) } + + case Tag => + if (buf.isEmpty && cur == '{') { + curlyBraceTag = true + buf.append(cur) + } else if (curlyBraceTag && cur == '}') { + curlyBraceTag = false + buf.append(cur) + } else if (cur == ctag.charAt(0)) { + if (ctag.length > 1) { tagPosition = 1; state = CTag } + else tag() + } else buf.append(cur) + + case CTag => + if (cur == ctag.charAt(tagPosition)) { + if (tagPosition == ctag.length - 1) tag() + else { tagPosition = tagPosition + 1 } + } else { notCTag(); buf.append(cur) } + } + } + state match { + case Text => staticText() + case OTag => notOTag(); staticText() + case Tag => fail("Unclosed tag \"" + buf.toString + "\"") + case CTag => notCTag(); staticText() + } + stack.foreach { + case IncompleteSection(key, _, _, _) => fail("Unclosed mustache section \"" + key + "\"") + case _ => + } + val result = stack.reverse + + if (result.size == 1) result.head + else RootToken(result) + } + + private def fail[A](msg: String): A = throw MustacheParseException(line, msg) + + private def consume = { + prev = cur + + if (src.hasNext) { + cur = src.next() + // \n, \r\n, \r + if ( + cur == '\r' || + (cur == '\n' && prev != '\r') + ) line = line + 1 + true + } else false + } + + private def notOTag(): Unit = { + buf.append(otag.substring(0, tagPosition)) + state = Text + } + private def notCTag(): Unit = { + buf.append(ctag.substring(0, tagPosition)) + state = Tag + } + private def reduce: String = { val r = buf.toString; buf.clear(); r } + + private def staticText(): Unit = { + val r = reduce + if (r.nonEmpty) stack = StaticTextToken(r) :: stack + } + + private def checkContent(content: String): String = { + val trimmed = content.trim + if (trimmed.isEmpty) fail("Empty tag") + else trimmed + } + + private def tag(): Unit = { + state = Text + val content = checkContent(reduce) + def skipFirst = checkContent(content substring 1) + def skipBoth = checkContent(content substring (1, content.length - 1)) + + content.charAt(0) match { + case '!' => // ignore comments + case '&' => + stack = UnescapedToken(skipFirst, otag, ctag) :: stack + case '{' => + if (content endsWith "}") + stack = UnescapedToken(skipBoth, otag, ctag) :: stack + else fail("Unbalanced \"{\" in tag \"" + content + "\"") + case '^' => + stack = IncompleteSection(skipFirst, inverted = true, otag = otag, ctag = ctag) :: stack + case '#' => + stack = IncompleteSection(skipFirst, inverted = false, otag, ctag) :: stack + case '/' => + val name = skipFirst + + @tailrec + def addSection( + children: List[Token], + s: List[Token] + ): List[Token] = s.headOption match { + case None => fail("Closing unopened section \"" + name + "\"") + + case Some(IncompleteSection(key, inverted, startOTag, startCTag)) if key == name => + SectionToken(inverted, name, children, startOTag, startCTag, otag, ctag) :: s.tail + + case Some(IncompleteSection(key, _, _, _)) if key != name => + fail("Unclosed section \"" + key + "\"") + + case Some(other) => + addSection(other :: children, s.tail) + } + stack = addSection(List[Token](), stack) + case '>' | '<' => + stack = PartialToken(skipFirst, otag, ctag) :: stack + case '=' => + if (content.length > 2 && content.endsWith("=")) { + val changeDelimiter = skipBoth + changeDelimiter.split("""\s+""", -1).toSeq match { + case Seq(o, c) => + stack = ChangeDelimitersToken(o, c, otag, ctag) :: stack + otag = o + ctag = c + case _ => fail("Invalid change delimiter tag content: \"" + changeDelimiter + "\"") + } + } else + fail("Invalid change delimiter tag content: \"" + content + "\"") + case _ => stack = EscapedToken(content, otag, ctag) :: stack + } + } + } + + // mustache tokens ------------------------------------------ + trait TokenProduct { + val maxLength: Int + def write(out: StringBuilder): Unit + + override def toString: String = { + val b = new StringBuilder(maxLength) + write(b) + b.toString + } + } + + object EmptyProduct extends TokenProduct { + val maxLength = 0 + def write(out: StringBuilder): Unit = {} + } + + case class StringProduct(str: String) extends TokenProduct { + val maxLength: Int = str.length + def write(out: StringBuilder): Unit = out.append(str) + } + + trait Token { + def render(context: Any, partials: Map[String, Mustache], callstack: List[Any]): TokenProduct + def templateSource: String + } + + trait CompositeToken { + def composite( + tokens: List[Token], + context: Any, + partials: Map[String, Mustache], + callstack: List[Any] + ): TokenProduct = + composite(tokens.map { (_, context) }, partials, callstack) + + def composite( + tasks: Seq[(Token, Any)], + partials: Map[String, Mustache], + callstack: List[Any] + ): TokenProduct = { + val result = tasks.map(t => { t._1.render(t._2, partials, callstack) }) + val len = result.foldLeft(0) { _ + _.maxLength } + new TokenProduct { + val maxLength: Int = len + def write(out: StringBuilder): Unit = result.foreach { _.write(out) } + } + } + } + + case class RootToken(children: List[Token]) extends Token with CompositeToken { + private val childrenSource = children.map(_.templateSource).mkString + + def render(context: Any, partials: Map[String, Mustache], callstack: List[Any]): TokenProduct = + composite(children, context, partials, callstack) + + def templateSource: String = childrenSource + } + + case class IncompleteSection(key: String, inverted: Boolean, otag: String, ctag: String) + extends Token { + def render(context: Any, partials: Map[String, Mustache], callstack: List[Any]): TokenProduct = + fail + def templateSource: String = fail + + private def fail = + throw new Exception("Weird thing happened. There is incomplete section in compiled template.") + + } + + case class StaticTextToken(staticText: String) extends Token { + private val product = StringProduct(staticText) + + def render(context: Any, partials: Map[String, Mustache], callstack: List[Any]): TokenProduct = + product + + def templateSource: String = staticText + + } + + case class ChangeDelimitersToken( + newOTag: String, + newCTag: String, + otag: String, + ctag: String + ) extends Token { + private val source = otag + "=" + newOTag + " " + newCTag + "=" + ctag + + def render(context: Any, partials: Map[String, Mustache], callstack: List[Any]): TokenProduct = + EmptyProduct + + def templateSource: String = source + + } + + case class PartialToken(key: String, otag: String, ctag: String) extends Token { + def render(context: Any, partials: Map[String, Mustache], callstack: List[Any]): TokenProduct = + partials.get(key) match { + case Some(template) => template.product(context, partials, template :: callstack) + case _ => throw new IllegalArgumentException("Partial \"" + key + "\" is not defined.") + } + def templateSource: String = otag + ">" + key + ctag + } + + trait ContextHandler { + + protected def defaultRender( + otag: String, + ctag: String + ): (Any, Map[String, Mustache], List[Any]) => String => String = + (context: Any, partials: Map[String, Mustache], callstack: List[Any]) => + (str: String) => { + val t = new Mustache(str, otag, ctag) + t.render(context, partials, callstack) + } + + def valueOf( + key: String, + context: Any, + partials: Map[String, Mustache], + callstack: List[Any], + childrenString: String, + render: (Any, Map[String, Mustache], List[Any]) => String => String + ): Any = { + val r = render(context, partials, callstack) + + val wrappedEval = + callstack + .filter(_.isInstanceOf[Mustache]) + .asInstanceOf[List[Mustache]] + .foldLeft(() => { eval(findInContext(context :: callstack, key), childrenString, r) })( + (f, e) => { () => { e.withContextAndRenderFn(context, r)(f()) } } + ) + wrappedEval() match { + case None if key == "." => context + case other => other + } + } + + @tailrec + private def eval( + value: Any, + childrenString: String, + render: String => String + ): Any = + value match { + case Some(someValue) => eval(someValue, childrenString, render) + + case a: Awaitable[_] => + eval(Await.result(a, Duration.Inf), childrenString, render) + + case f: Function0[_] => + eval(f(), childrenString, render) + + case s: Seq[_] => s + + case m: Map[_, _] => m + + case f: Function1[String, _] => + eval(f(childrenString), childrenString, render) + + case f: Function2[String, Function1[String, String], _] => + eval(f(childrenString, render), childrenString, render) + + case other => other + } + + @tailrec + private def findInContext(stack: List[Any], key: String): Any = + stack.headOption match { + case None => None + case Some(head) => + (head match { + case null => None + case m: Map[String, _] => + m.get(key) match { + case Some(v) => v + case None => None + } + case m: Mustache => + m.globals.get(key) match { + case Some(v) => v + case None => None + } + case any => reflection(any, key) + }) match { + case None => findInContext(stack.tail, key) + case x => x + } + } + + private def reflection(x: Any, key: String): Any = { + val w = wrapped(x) + (methods(w).get(key), fields(w).get(key)) match { + case (Some(m), _) => m.invoke(w) + case (None, Some(f)) => f.get(w) + case _ => None + } + } + + private def fields(w: AnyRef) = Map( + w.getClass.getFields.toIndexedSeq.map(x => { x.getName -> x }): _* + ) + + private def methods(w: AnyRef) = Map( + w.getClass.getMethods + .filter(x => { x.getParameterTypes.isEmpty }) + .toIndexedSeq.map(x => { x.getName -> x }): _* + ) + + private def wrapped(x: Any): AnyRef = + x match { + case x: Byte => byte2Byte(x) + case x: Short => short2Short(x) + case x: Char => char2Character(x) + case x: Int => int2Integer(x) + case x: Long => long2Long(x) + case x: Float => float2Float(x) + case x: Double => double2Double(x) + case x: Boolean => boolean2Boolean(x) + case _ => x.asInstanceOf[AnyRef] + } + } + + trait ValuesFormatter { + @tailrec + final def format(value: Any): String = + value match { + case null => "" + case None => "" + case Some(v) => format(v) + case x => x.toString + } + } + + case class SectionToken( + inverted: Boolean, + key: String, + children: List[Token], + startOTag: String, + startCTag: String, + endOTag: String, + endCTag: String + ) extends Token + with ContextHandler + with CompositeToken { + + private val childrenSource = children.map(_.templateSource).mkString + + private val source = startOTag + (if (inverted) "^" else "#") + key + + startCTag + childrenSource + endOTag + "/" + key + endCTag + + private val childrenTemplate = { + val root = + if (children.size == 1) children.head + else RootToken(children) + new Mustache(root) + } + + def render(context: Any, partials: Map[String, Mustache], callstack: List[Any]): TokenProduct = + valueOf(key, context, partials, callstack, childrenSource, renderContent) match { + case null => + if (inverted) composite(children, context, partials, context :: callstack) + else EmptyProduct + case None => + if (inverted) composite(children, context, partials, context :: callstack) + else EmptyProduct + case b: Boolean => + if (b ^ inverted) composite(children, context, partials, context :: callstack) + else EmptyProduct + case s: Seq[_] if inverted => + if (s.isEmpty) composite(children, context, partials, context :: callstack) + else EmptyProduct + case s: Seq[_] if !inverted => + val tasks = for (element <- s; token <- children) yield (token, element) + composite(tasks, partials, context :: callstack) + case str: String => + if (!inverted) StringProduct(str) + else EmptyProduct + + case other => + if (!inverted) composite(children, other, partials, context :: callstack) + else EmptyProduct + } + + private def renderContent(context: Any, partials: Map[String, Mustache], callstack: List[Any])( + template: String + ): String = + // it will be children nodes in most cases + // TODO: some cache for dynamically generated templates? + if (template == childrenSource) + childrenTemplate.render(context, partials, context :: callstack) + else { + val t = new Mustache(template, startOTag, startCTag) + t.render(context, partials, context :: callstack) + } + + def templateSource: String = source + } + + case class UnescapedToken(key: String, otag: String, ctag: String) + extends Token + with ContextHandler + with ValuesFormatter { + private val source = otag + "&" + key + ctag + + def render( + context: Any, + partials: Map[String, Mustache], + callstack: List[Any] + ): TokenProduct = { + val v = format(valueOf(key, context, partials, callstack, "", defaultRender(otag, ctag))) + new TokenProduct { + val maxLength: Int = v.length + def write(out: StringBuilder): Unit = { out.append(v) } + } + } + + def templateSource: String = source + } + + case class EscapedToken(key: String, otag: String, ctag: String) + extends Token + with ContextHandler + with ValuesFormatter { + private val source = otag + key + ctag + + val transcode: Map[Char, String] = Map.empty +// Map( +// '<' -> "<", +// '>' -> ">", +// '"' -> """, +// '&' -> "&" +// ) + + def render( + context: Any, + partials: Map[String, Mustache], + callstack: List[Any] + ): TokenProduct = { + val v = format(valueOf(key, context, partials, callstack, "", defaultRender(otag, ctag))) + new TokenProduct { + val maxLength: Int = (v.length * 1.2).toInt + def write(out: StringBuilder): Unit = + v.foreach { + case t if transcode.contains(t) => out.append(transcode.get(t)) + case c => out.append(c) + } + } + } + + def templateSource: String = source + } + + object Mustache { + + def apply(path: String): Mustache = { + new Mustache( + Settings.MustacheRootPath match { + case Some(mustacheRootPath) => + val file = s"$mustacheRootPath/$path" + if (new JFile(file).exists) { + Source.fromFile(file) + } else { + Source.fromInputStream(getClass.getClassLoader.getResourceAsStream(path)) + } + case None => + Source.fromInputStream(getClass.getClassLoader.getResourceAsStream(path)) + } + ) + } + + } diff --git a/common/src/main/scala/app/softnetwork/concurrent/package.scala b/common/src/main/scala/app/softnetwork/concurrent/package.scala index 71602525..b76cb2e2 100644 --- a/common/src/main/scala/app/softnetwork/concurrent/package.scala +++ b/common/src/main/scala/app/softnetwork/concurrent/package.scala @@ -69,7 +69,7 @@ package object concurrent { def retry(fn: => Future[T])(implicit ec: ExecutionContext): Future[T] = retry(nbTries)(fn) def retry(n: Int)(fn: => Future[T])(implicit ec: ExecutionContext): Future[T] = { - val p = Promise[T] + val p = Promise[T]() fn onComplete { case Success(x) => p.success(x) case _ if n > 1 => p.completeWith(retry(n - 1)(fn)) diff --git a/common/src/test/scala/mustache/MustacheSpec.scala b/common/src/test/scala/mustache/MustacheSpec.scala index 5101f11f..b6def31f 100644 --- a/common/src/test/scala/mustache/MustacheSpec.scala +++ b/common/src/test/scala/mustache/MustacheSpec.scala @@ -3,10 +3,6 @@ package mustache import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.should.Matchers -import mustache._ - -import scala.io.Source - /** Created by smanciot on 08/04/2018. */ class MustacheSpec extends AnyWordSpec with Matchers { diff --git a/core/build.sbt b/core/build.sbt index 35a0b568..b8f3eda2 100644 --- a/core/build.sbt +++ b/core/build.sbt @@ -40,7 +40,7 @@ val kryo = Seq( ) val chill = Seq( - "com.twitter" % "chill-akka_2.12" % Versions.chill excludeAll ExclusionRule(organization = "com.typesafe.akka") + "com.twitter" %% "chill-akka" % Versions.chill excludeAll ExclusionRule(organization = "com.typesafe.akka") ) val logback = Seq( diff --git a/core/src/main/scala/app/softnetwork/persistence/typed/scaladsl/Patterns.scala b/core/src/main/scala/app/softnetwork/persistence/typed/scaladsl/Patterns.scala index cb24f2ab..e1ee42d6 100644 --- a/core/src/main/scala/app/softnetwork/persistence/typed/scaladsl/Patterns.scala +++ b/core/src/main/scala/app/softnetwork/persistence/typed/scaladsl/Patterns.scala @@ -151,7 +151,7 @@ trait SingletonPattern[C <: Command, R <: CommandResult] val maybeSingletonRef = Option(singletonRef) if (maybeSingletonRef.isEmpty) { log.warn(s"actorRef for [$name] is undefined") - system.receptionist ? Find(key) complete () match { + (system.receptionist ? Find(key)).complete() match { case Success(s) => maybeActorRef = s.serviceInstances(key).headOption case Failure(f) => log.error(f.getMessage, f) @@ -164,7 +164,7 @@ trait SingletonPattern[C <: Command, R <: CommandResult] log.info(s"spawn supervisor for singleton [$name]") import app.softnetwork.persistence._ val supervisorRef = system.systemActorOf(supervisor, generateUUID()) - supervisorRef ? SingletonRef complete () match { + (supervisorRef ? SingletonRef).complete() match { case Success(s) => maybeActorRef = Some(s.singletonRef) log.info(s"actorRef for [$name] has been loaded -> ${s.singletonRef.path}") diff --git a/core/src/test/scala/app/softnetwork/persistence/service/SingletonServiceSpec.scala b/core/src/test/scala/app/softnetwork/persistence/service/SingletonServiceSpec.scala index 0388ee81..582eddd2 100644 --- a/core/src/test/scala/app/softnetwork/persistence/service/SingletonServiceSpec.scala +++ b/core/src/test/scala/app/softnetwork/persistence/service/SingletonServiceSpec.scala @@ -38,7 +38,7 @@ class SingletonServiceSpec "SingletonService" must { "run commands" in { - run(TestSample) complete () match { + run(TestSample).complete() match { case Success(s) => s match { case SampleTested => log.info("sample tested !") diff --git a/core/src/test/scala/app/softnetwork/persistence/typed/scaladsl/SingletonPatternSpec.scala b/core/src/test/scala/app/softnetwork/persistence/typed/scaladsl/SingletonPatternSpec.scala index 339cddeb..78b60820 100644 --- a/core/src/test/scala/app/softnetwork/persistence/typed/scaladsl/SingletonPatternSpec.scala +++ b/core/src/test/scala/app/softnetwork/persistence/typed/scaladsl/SingletonPatternSpec.scala @@ -27,7 +27,7 @@ class SingletonPatternSpec extends SamplePattern with AnyWordSpecLike with Befor implicit lazy val system: ActorSystem[Nothing] = testKit.system - def ask(): Unit = this ? TestSample complete () match { + def ask(): Unit = (this ? TestSample).complete() match { case Success(s) => s match { case SampleTested => log.info("sample tested !") diff --git a/core/testkit/src/main/resources/logback.xml b/core/testkit/src/main/resources/logback.xml index 67b5ddff..a38273bf 100644 --- a/core/testkit/src/main/resources/logback.xml +++ b/core/testkit/src/main/resources/logback.xml @@ -32,6 +32,8 @@ + + diff --git a/counter/src/test/resources/application.conf b/counter/src/test/resources/application.conf new file mode 100644 index 00000000..f01c7112 --- /dev/null +++ b/counter/src/test/resources/application.conf @@ -0,0 +1,3 @@ +akka.cluster.distributed-data.durable { + keys = [] +} \ No newline at end of file diff --git a/elastic/build.sbt b/elastic/build.sbt deleted file mode 100644 index 1e43fe17..00000000 --- a/elastic/build.sbt +++ /dev/null @@ -1,34 +0,0 @@ -Test / parallelExecution := false - -organization := "app.softnetwork.persistence" - -name := "persistence-elastic" - -val elastic = Seq( - "com.sksamuel.elastic4s" %% "elastic4s-core" % Versions.elastic4s exclude ("org.elasticsearch", "elasticsearch"), - "com.sksamuel.elastic4s" %% "elastic4s-http" % Versions.elastic4s exclude ("org.elasticsearch", "elasticsearch"), - "org.elasticsearch" % "elasticsearch" % Versions.elasticSearch exclude ("org.apache.logging.log4j", "log4j-api"), - "com.sksamuel.elastic4s" %% "elastic4s-testkit" % Versions.elastic4s % Test exclude ("org.elasticsearch", "elasticsearch"), - "com.sksamuel.elastic4s" %% "elastic4s-embedded" % Versions.elastic4s % Test exclude ("org.elasticsearch", "elasticsearch"), - "com.sksamuel.elastic4s" %% "elastic4s-http" % Versions.elastic4s % Test exclude ("org.elasticsearch", "elasticsearch"), - "org.elasticsearch" % "elasticsearch" % Versions.elasticSearch % Test exclude ("org.apache.logging.log4j", "log4j-api"), - "org.apache.logging.log4j" % "log4j-api" % Versions.log4j % Test, - "org.apache.logging.log4j" % "log4j-slf4j-impl" % Versions.log4j % Test, - "org.apache.logging.log4j" % "log4j-core" % Versions.log4j % Test -) - -val httpComponentsExclusions = Seq( - ExclusionRule(organization = "org.apache.httpcomponents", name = "httpclient", artifact = "*", configurations = Vector(ConfigRef("test")), crossVersion = CrossVersion.disabled ) -) - -val guavaExclusion = ExclusionRule(organization = "com.google.guava", name="guava") - -val jest = Seq( - "io.searchbox" % "jest" % Versions.jest -).map(_.excludeAll(httpComponentsExclusions ++ Seq(guavaExclusion): _*)) - -libraryDependencies ++= elastic ++ jest ++ Seq( - "javax.activation" % "activation" % "1.1.1" % Test -) - -Compile / unmanagedResourceDirectories += baseDirectory.value / "src/main/protobuf" diff --git a/elastic/src/main/resources/mapping/default.mustache b/elastic/src/main/resources/mapping/default.mustache deleted file mode 100644 index f3e19d45..00000000 --- a/elastic/src/main/resources/mapping/default.mustache +++ /dev/null @@ -1,16 +0,0 @@ -{ - "{{type}}": { - "properties": { - "uuid": { - "type": "keyword", - "index": true - }, - "createdDate": { - "type": "date" - }, - "lastUpdated": { - "type": "date" - } - } - } -} \ No newline at end of file diff --git a/elastic/src/main/resources/softnetwork-elastic.conf b/elastic/src/main/resources/softnetwork-elastic.conf deleted file mode 100644 index d884e512..00000000 --- a/elastic/src/main/resources/softnetwork-elastic.conf +++ /dev/null @@ -1,21 +0,0 @@ -elastic { - ip = "localhost" - ip = ${?ELASTIC_IP} - port = 9200 - port = ${?ELASTIC_PORT} - - credentials { - url = "http://"${elastic.ip}":"${elastic.port} - username = "" - password = "" - - url = ${?ELASTIC_CREDENTIALS_URL} - username = ${?ELASTIC_CREDENTIALS_USERNAME} - password = ${?ELASTIC_CREDENTIALS_PASSWORD} - - } - - multithreaded = true - discovery-enabled = false - -} \ No newline at end of file diff --git a/elastic/src/main/scala/app/softnetwork/elastic/client/ElasticClientApi.scala b/elastic/src/main/scala/app/softnetwork/elastic/client/ElasticClientApi.scala deleted file mode 100644 index 4a87f7b7..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/client/ElasticClientApi.scala +++ /dev/null @@ -1,507 +0,0 @@ -package app.softnetwork.elastic.client - -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import akka.NotUsed -import akka.actor.ActorSystem -import _root_.akka.stream.{FlowShape, Materializer} -import akka.stream.scaladsl._ -import app.softnetwork.persistence.message.CountResponse -import app.softnetwork.persistence.model.Timestamped -import app.softnetwork.serialization._ -import app.softnetwork.elastic.sql.{SQLQueries, SQLQuery} -import com.typesafe.config.{Config, ConfigFactory} -import org.json4s.{DefaultFormats, Formats} -import org.json4s.jackson.JsonMethods._ - -import scala.collection.immutable.Seq -import scala.concurrent.{Await, ExecutionContext, Future} -import scala.concurrent.duration.Duration -import scala.language.{implicitConversions, postfixOps} -import scala.reflect.ClassTag - -/** Created by smanciot on 28/06/2018. - */ -trait ElasticClientApi - extends IndicesApi - with UpdateSettingsApi - with AliasApi - with MappingApi - with CountApi - with SearchApi - with IndexApi - with UpdateApi - with GetApi - with BulkApi - with DeleteApi - with RefreshApi - with FlushApi { - - def config: Config = ConfigFactory.load() - - final lazy val elasticConfig: ElasticConfig = ElasticConfig(config) -} - -trait IndicesApi { - val defaultSettings: String = - """ - |{ - | "index": { - | "max_ngram_diff": "20", - | "mapping" : { - | "total_fields" : { - | "limit" : "2000" - | } - | }, - | "analysis": { - | "analyzer": { - | "ngram_analyzer": { - | "tokenizer": "ngram_tokenizer", - | "filter": [ - | "lowercase", - | "asciifolding" - | ] - | }, - | "search_analyzer": { - | "type": "custom", - | "tokenizer": "standard", - | "filter": [ - | "lowercase", - | "asciifolding" - | ] - | } - | }, - | "tokenizer": { - | "ngram_tokenizer": { - | "type": "ngram", - | "min_gram": 1, - | "max_gram": 20, - | "token_chars": [ - | "letter", - | "digit" - | ] - | } - | } - | } - | } - |} - """.stripMargin - - def createIndex(index: String, settings: String = defaultSettings): Boolean - - def deleteIndex(index: String): Boolean - - def closeIndex(index: String): Boolean - - def openIndex(index: String): Boolean -} - -trait AliasApi { - def addAlias(index: String, alias: String): Boolean -} - -trait UpdateSettingsApi { _: IndicesApi => - def toggleRefresh(index: String, enable: Boolean): Unit = { - updateSettings( - index, - if (!enable) """{"index" : {"refresh_interval" : -1} }""" - else """{"index" : {"refresh_interval" : "1s"} }""" - ) - } - - def setReplicas(index: String, replicas: Int): Unit = { - updateSettings(index, s"""{"index" : {"number_of_replicas" : $replicas} }""") - } - - def updateSettings(index: String, settings: String = defaultSettings): Boolean -} - -trait MappingApi { - def setMapping(index: String, _type: String, mapping: String): Boolean -} - -trait RefreshApi { - def refresh(index: String): Boolean -} - -trait FlushApi { - def flush(index: String, force: Boolean = true, wait: Boolean = true): Boolean -} - -trait IndexApi { - def index[U <: Timestamped]( - entity: U, - index: Option[String] = None, - maybeType: Option[String] = None - )(implicit u: ClassTag[U], formats: Formats): Boolean = { - val _type = maybeType.getOrElse(u.runtimeClass.getSimpleName.toLowerCase) - this.index(index.getOrElse(_type), _type, entity.uuid, serialization.write[U](entity)) - } - - def index(index: String, _type: String, id: String, source: String): Boolean - - def indexAsync[U <: Timestamped]( - entity: U, - index: Option[String] = None, - maybeType: Option[String] = None - )(implicit u: ClassTag[U], ec: ExecutionContext, formats: Formats): Future[Boolean] = { - val _type = maybeType.getOrElse(u.runtimeClass.getSimpleName.toLowerCase) - indexAsync(index.getOrElse(_type), _type, entity.uuid, serialization.write[U](entity)) - } - - def indexAsync(index: String, _type: String, id: String, source: String)(implicit - ec: ExecutionContext - ): Future[Boolean] -} - -trait UpdateApi { - def update[U <: Timestamped]( - entity: U, - index: Option[String] = None, - maybeType: Option[String] = None, - upsert: Boolean = true - )(implicit u: ClassTag[U], formats: Formats): Boolean = { - val _type = maybeType.getOrElse(u.runtimeClass.getSimpleName.toLowerCase) - this.update(index.getOrElse(_type), _type, entity.uuid, serialization.write[U](entity), upsert) - } - - def update(index: String, _type: String, id: String, source: String, upsert: Boolean): Boolean - - def updateAsync[U <: Timestamped]( - entity: U, - index: Option[String] = None, - maybeType: Option[String] = None, - upsert: Boolean = true - )(implicit u: ClassTag[U], ec: ExecutionContext, formats: Formats): Future[Boolean] = { - val _type = maybeType.getOrElse(u.runtimeClass.getSimpleName.toLowerCase) - this.updateAsync( - index.getOrElse(_type), - _type, - entity.uuid, - serialization.write[U](entity), - upsert - ) - } - - def updateAsync(index: String, _type: String, id: String, source: String, upsert: Boolean)( - implicit ec: ExecutionContext - ): Future[Boolean] -} - -trait DeleteApi { - def delete[U <: Timestamped]( - entity: U, - index: Option[String] = None, - maybeType: Option[String] = None - )(implicit u: ClassTag[U]): Boolean = { - val _type = maybeType.getOrElse(u.runtimeClass.getSimpleName.toLowerCase) - delete(entity.uuid, index.getOrElse(_type), _type) - } - - def delete(uuid: String, index: String, _type: String): Boolean - - def deleteAsync[U <: Timestamped]( - entity: U, - index: Option[String] = None, - maybeType: Option[String] = None - )(implicit u: ClassTag[U], ec: ExecutionContext): Future[Boolean] = { - val _type = maybeType.getOrElse(u.runtimeClass.getSimpleName.toLowerCase) - deleteAsync(entity.uuid, index.getOrElse(_type), _type) - } - - def deleteAsync(uuid: String, index: String, _type: String)(implicit - ec: ExecutionContext - ): Future[Boolean] - -} - -trait BulkApi { _: RefreshApi with UpdateSettingsApi => - type A - type R - - def toBulkAction(bulkItem: BulkItem): A - - implicit def toBulkElasticAction(a: A): BulkElasticAction - - implicit def toBulkElasticResult(r: R): BulkElasticResult - - def bulk(implicit bulkOptions: BulkOptions, system: ActorSystem): Flow[Seq[A], R, NotUsed] - - def bulkResult: Flow[R, Set[String], NotUsed] - - /** +----------+ - * | | - * | Source | items: Iterator[D] - * | | - * +----------+ - * | - * v - * +----------+ - * | | - * |transform | BulkableAction - * | | - * +----------+ - * | - * v - * +----------+ - * | | - * | settings | Update elasticsearch settings (refresh and replicas) - * | | - * +----------+ - * | - * v - * +----------+ - * | | - * | group | - * | | - * +----------+ - * | - * v - * +----------+ +----------+ - * | |------->| | - * | balance | | bulk | - * | |------->| | - * +----------+ +----------+ - * | | - * | | - * | | - * +---------+ | | - * | |<-----------' | - * | merge | | - * | |<----------------' - * +---------+ - * | - * v - * +----------+ - * | | - * | result | BulkResult - * | | - * +----------+ - * | - * v - * +----------+ - * | | - * | Sink | indices: Set[String] - * | | - * +----------+ - * - * Asynchronously bulk items to Elasticsearch - * - * @param items the items for which a bulk has to be performed - * @param toDocument the function to transform items to elastic documents in json format - * @param idKey the key mapping to the document id - * @param suffixDateKey the key mapping to the date used to suffix the index - * @param suffixDatePattern the date pattern used to suffix the index - * @param update whether to upsert or not the items - * @param delete whether to delete or not the items - * @param parentIdKey the key mapping to the elastic parent document id - * @param bulkOptions bulk options - * @param system actor system - * @tparam D the type of the items - * @return the indexes on which the documents have been indexed - */ - def bulk[D]( - items: Iterator[D], - toDocument: D => String, - idKey: Option[String] = None, - suffixDateKey: Option[String] = None, - suffixDatePattern: Option[String] = None, - update: Option[Boolean] = None, - delete: Option[Boolean] = None, - parentIdKey: Option[String] = None - )(implicit bulkOptions: BulkOptions, system: ActorSystem): Set[String] = { - - implicit val materializer: Materializer = Materializer(system) - - import GraphDSL.Implicits._ - - val source = Source.fromIterator(() => items) - - val sink = Sink.fold[Set[String], Set[String]](Set.empty[String])(_ ++ _) - - val g = Flow.fromGraph(GraphDSL.create() { implicit b => - val transform = - b.add( - Flow[D].map(item => - toBulkAction( - toBulkItem( - toDocument, - idKey, - suffixDateKey, - suffixDatePattern, - update, - delete, - parentIdKey, - item - ) - ) - ) - ) - - val settings = b.add(BulkSettings[A](bulkOptions.disableRefresh)(this, toBulkElasticAction)) - - val group = b.add(Flow[A].named("group").grouped(bulkOptions.maxBulkSize).map { items => -// logger.info(s"Preparing to write batch of ${items.size}...") - items - }) - - val parallelism = Math.max(1, bulkOptions.balance) - - val bulkFlow: FlowShape[Seq[A], R] = b.add(bulk) - - val result = b.add(bulkResult) - - if (parallelism > 1) { - val balancer = b.add(Balance[Seq[A]](parallelism)) - - val merge = b.add(Merge[R](parallelism)) - - transform ~> settings ~> group ~> balancer - - 1 to parallelism foreach { _ => - balancer ~> bulkFlow ~> merge - } - - merge ~> result - } else { - transform ~> settings ~> group ~> bulkFlow ~> result - } - - FlowShape(transform.in, result.out) - }) - - val future = source.via(g).toMat(sink)(Keep.right).run() - - val indices = Await.result(future, Duration.Inf) - indices.foreach(refresh) - indices - } - - def toBulkItem[D]( - toDocument: D => String, - idKey: Option[String], - suffixDateKey: Option[String], - suffixDatePattern: Option[String], - update: Option[Boolean], - delete: Option[Boolean], - parentIdKey: Option[String], - item: D - )(implicit bulkOptions: BulkOptions): BulkItem = { - - implicit val formats: DefaultFormats = org.json4s.DefaultFormats - val document = toDocument(item) - val jsonMap = parse(document, useBigDecimalForDouble = false).extract[Map[String, Any]] - // extract id - val id = idKey.flatMap { i => - jsonMap.get(i).map(_.toString) - } - - // extract final index name - val index = suffixDateKey - .flatMap { s => - // Expecting a date field YYYY-MM-dd ... - jsonMap.get(s).map { d => - val strDate = d.toString.substring(0, 10) - val date = LocalDate.parse(strDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")) - date.format( - suffixDatePattern - .map(DateTimeFormatter.ofPattern) - .getOrElse(DateTimeFormatter.ofPattern("yyyy-MM-dd")) - ) - } - } - .map(s => s"${bulkOptions.index}-$s") - // use suffix if available otherwise only index - .getOrElse(bulkOptions.index) - - // extract parent key - val parent = parentIdKey.flatMap { i => - jsonMap.get(i).map(_.toString) - } - - val action = delete match { - case Some(d) if d => BulkAction.DELETE - case _ => - update match { - case Some(u) if u => BulkAction.UPDATE - case _ => BulkAction.INDEX - } - } - - val body = action match { - case BulkAction.UPDATE => docAsUpsert(document) - case _ => document - } - - BulkItem(index, action, body, id, parent) - } - -} - -trait CountApi { - def countAsync(query: JSONQuery)(implicit ec: ExecutionContext): Future[Option[Double]] - - def count(query: JSONQuery): Option[Double] - - def countAsync(sqlQuery: SQLQuery)(implicit - ec: ExecutionContext - ): Future[_root_.scala.collection.Seq[CountResponse]] -} - -trait GetApi { - def get[U <: Timestamped]( - id: String, - index: Option[String] = None, - maybeType: Option[String] = None - )(implicit m: Manifest[U], formats: Formats): Option[U] - - def getAsync[U <: Timestamped]( - id: String, - index: Option[String] = None, - maybeType: Option[String] = None - )(implicit m: Manifest[U], ec: ExecutionContext, formats: Formats): Future[Option[U]] -} - -trait SearchApi { - - def search[U](jsonQuery: JSONQuery)(implicit m: Manifest[U], formats: Formats): List[U] - - def search[U](sqlQuery: SQLQuery)(implicit m: Manifest[U], formats: Formats): List[U] - - def searchAsync[U]( - sqlQuery: SQLQuery - )(implicit m: Manifest[U], ec: ExecutionContext, formats: Formats): Future[List[U]] - - def searchWithInnerHits[U, I](sqlQuery: SQLQuery, innerField: String)(implicit - m1: Manifest[U], - m2: Manifest[I], - formats: Formats - ): List[(U, List[I])] - - def searchWithInnerHits[U, I](jsonQuery: JSONQuery, innerField: String)(implicit - m1: Manifest[U], - m2: Manifest[I], - formats: Formats - ): List[(U, List[I])] - - def multiSearch[U]( - sqlQueries: SQLQueries - )(implicit m: Manifest[U], formats: Formats): List[List[U]] - - def multiSearch[U]( - jsonQueries: JSONQueries - )(implicit m: Manifest[U], formats: Formats): List[List[U]] - - def multiSearchWithInnerHits[U, I](sqlQueries: SQLQueries, innerField: String)(implicit - m1: Manifest[U], - m2: Manifest[I], - formats: Formats - ): List[List[(U, List[I])]] - - def multiSearchWithInnerHits[U, I](jsonQueries: JSONQueries, innerField: String)(implicit - m1: Manifest[U], - m2: Manifest[I], - formats: Formats - ): List[List[(U, List[I])]] - -} diff --git a/elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestClientApi.scala b/elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestClientApi.scala deleted file mode 100644 index 0324f69e..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestClientApi.scala +++ /dev/null @@ -1,680 +0,0 @@ -package app.softnetwork.elastic.client.jest - -import akka.NotUsed -import akka.actor.ActorSystem -import akka.stream.scaladsl.Flow -import app.softnetwork.elastic.client._ -import app.softnetwork.elastic.sql.{ElasticQuery, SQLQueries, SQLQuery} -import app.softnetwork.persistence.message.CountResponse -import app.softnetwork.persistence.model.Timestamped -import app.softnetwork.serialization._ -import io.searchbox.action.BulkableAction -import io.searchbox.core._ -import io.searchbox.core.search.aggregation.RootAggregation -import io.searchbox.indices.aliases.{AddAliasMapping, ModifyAliases} -import io.searchbox.indices.mapping.PutMapping -import io.searchbox.indices.settings.UpdateSettings -import io.searchbox.indices._ -import io.searchbox.params.Parameters -import org.json4s.Formats - -import scala.collection.JavaConverters._ -import scala.collection.immutable.Seq -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.language.implicitConversions -import scala.util.{Failure, Success, Try} - -/** Created by smanciot on 20/05/2021. - */ -trait JestClientApi - extends ElasticClientApi - with JestIndicesApi - with JestAliasApi - with JestUpdateSettingsApi - with JestMappingApi - with JestRefreshApi - with JestFlushApi - with JestCountApi - with JestIndexApi - with JestUpdateApi - with JestDeleteApi - with JestGetApi - with JestSearchApi - with JestBulkApi - -trait JestIndicesApi extends IndicesApi with JestClientCompanion { - override def createIndex(index: String, settings: String = defaultSettings): Boolean = - apply().execute(new CreateIndex.Builder(index).settings(settings).build()).isSucceeded - override def deleteIndex(index: String): Boolean = - apply().execute(new DeleteIndex.Builder(index).build()).isSucceeded - override def closeIndex(index: String): Boolean = - apply().execute(new CloseIndex.Builder(index).build()).isSucceeded - override def openIndex(index: String): Boolean = - apply().execute(new OpenIndex.Builder(index).build()).isSucceeded -} - -trait JestAliasApi extends AliasApi with JestClientCompanion { - override def addAlias(index: String, alias: String): Boolean = { - apply() - .execute( - new ModifyAliases.Builder( - new AddAliasMapping.Builder(index, alias).build() - ).build() - ) - .isSucceeded - } -} - -trait JestUpdateSettingsApi extends UpdateSettingsApi with JestClientCompanion { _: IndicesApi => - override def updateSettings(index: String, settings: String = defaultSettings): Boolean = - closeIndex(index) && - apply().execute(new UpdateSettings.Builder(settings).addIndex(index).build()).isSucceeded && - openIndex(index) -} - -trait JestMappingApi extends MappingApi with JestClientCompanion { - override def setMapping(index: String, _type: String, mapping: String): Boolean = - apply().execute(new PutMapping.Builder(index, _type, mapping).build()).isSucceeded -} - -trait JestRefreshApi extends RefreshApi with JestClientCompanion { - override def refresh(index: String): Boolean = - apply().execute(new Refresh.Builder().addIndex(index).build()).isSucceeded -} - -trait JestFlushApi extends FlushApi with JestClientCompanion { - override def flush(index: String, force: Boolean = true, wait: Boolean = true): Boolean = apply() - .execute( - new Flush.Builder().addIndex(index).force(force).waitIfOngoing(wait).build() - ) - .isSucceeded -} - -trait JestCountApi extends CountApi with JestClientCompanion { - override def countAsync( - jsonQuery: JSONQuery - )(implicit ec: ExecutionContext): Future[Option[Double]] = { - import JestClientResultHandler._ - import jsonQuery._ - val count = new Count.Builder().query(query) - for (indice <- indices) count.addIndex(indice) - for (t <- types) count.addType(t) - val promise = Promise[Option[Double]]() - apply().executeAsyncPromise(count.build()) onComplete { - case Success(result) => - if (!result.isSucceeded) - logger.error(result.getErrorMessage) - promise.success(Option(result.getCount)) - case Failure(f) => - logger.error(f.getMessage, f) - promise.failure(f) - } - promise.future - } - - override def count(jsonQuery: JSONQuery): Option[Double] = { - import jsonQuery._ - val count = new Count.Builder().query(query) - for (indice <- indices) count.addIndex(indice) - for (t <- types) count.addType(t) - val result = apply().execute(count.build()) - if (!result.isSucceeded) - logger.error(result.getErrorMessage) - Option(result.getCount) - } - - override def countAsync( - sqlQuery: SQLQuery - )(implicit ec: ExecutionContext): Future[_root_.scala.collection.Seq[CountResponse]] = { - val futures = for (elasticCount <- ElasticQuery.count(sqlQuery)) yield { - val promise: Promise[CountResponse] = Promise() - import collection.immutable.Seq - val _field = elasticCount.field - val _sourceField = elasticCount.sourceField - val _agg = elasticCount.agg - val _query = elasticCount.query - val _sources = elasticCount.sources - _sourceField match { - case "_id" => - countAsync( - JSONQuery(_query, Seq(_sources: _*), Seq.empty[String]) - ).onComplete { - case Success(result) => - promise.success(CountResponse(_field, result.getOrElse(0d).toInt, None)) - case Failure(f) => - logger.error(f.getMessage, f.fillInStackTrace()) - promise.success(CountResponse(_field, 0, Some(f.getMessage))) - } - case _ => - import JestClientApi._ - import JestClientResultHandler._ - apply() - .executeAsyncPromise(JSONQuery(_query, Seq(_sources: _*), Seq.empty[String]).search) - .onComplete { - case Success(result) => - val agg = _agg.split("\\.").last - - val itAgg = _agg.split("\\.").iterator - - var root = - if (elasticCount.nested) - result.getAggregations.getAggregation(itAgg.next(), classOf[RootAggregation]) - else - result.getAggregations - - if (elasticCount.filtered) { - root = root.getAggregation(itAgg.next(), classOf[RootAggregation]) - } - - promise.success( - CountResponse( - _field, - if (elasticCount.distinct) - root.getCardinalityAggregation(agg).getCardinality.toInt - else - root.getValueCountAggregation(agg).getValueCount.toInt, - None - ) - ) - - case Failure(f) => - logger.error(f.getMessage, f.fillInStackTrace()) - promise.success(CountResponse(_field, 0, Some(f.getMessage))) - } - } - promise.future - } - Future.sequence(futures) - } -} - -trait JestIndexApi extends IndexApi with JestClientCompanion { - override def index(index: String, _type: String, id: String, source: String): Boolean = { - Try( - apply().execute( - new Index.Builder(source).index(index).`type`(_type).id(id).build() - ) - ) match { - case Success(s) => - if (!s.isSucceeded) - logger.error(s.getErrorMessage) - s.isSucceeded - case Failure(f) => - logger.error(f.getMessage, f) - false - } - } - - override def indexAsync(index: String, _type: String, id: String, source: String)(implicit - ec: ExecutionContext - ): Future[Boolean] = { - import JestClientResultHandler._ - val promise: Promise[Boolean] = Promise() - apply().executeAsyncPromise( - new Index.Builder(source).index(index).`type`(_type).id(id).build() - ) onComplete { - case Success(s) => promise.success(s.isSucceeded) - case Failure(f) => - logger.error(f.getMessage, f) - promise.failure(f) - } - promise.future - } - -} - -trait JestUpdateApi extends UpdateApi with JestClientCompanion { - override def update( - index: String, - _type: String, - id: String, - source: String, - upsert: Boolean - ): Boolean = { - Try( - apply().execute( - new Update.Builder( - if (upsert) - docAsUpsert(source) - else - source - ).index(index).`type`(_type).id(id).build() - ) - ) match { - case Success(s) => - if (!s.isSucceeded) - logger.error(s.getErrorMessage) - s.isSucceeded - case Failure(f) => - logger.error(f.getMessage, f) - false - } - } - - override def updateAsync( - index: String, - _type: String, - id: String, - source: String, - upsert: Boolean - )(implicit ec: ExecutionContext): Future[Boolean] = { - import JestClientResultHandler._ - val promise: Promise[Boolean] = Promise() - apply().executeAsyncPromise( - new Update.Builder( - if (upsert) - docAsUpsert(source) - else - source - ).index(index).`type`(_type).id(id).build() - ) onComplete { - case Success(s) => - if (!s.isSucceeded) - logger.error(s.getErrorMessage) - promise.success(s.isSucceeded) - case Failure(f) => - logger.error(f.getMessage, f) - promise.failure(f) - } - promise.future - } - -} - -trait JestDeleteApi extends DeleteApi with JestClientCompanion { - override def delete(uuid: String, index: String, _type: String): Boolean = { - val result = apply().execute( - new Delete.Builder(uuid).index(index).`type`(_type).build() - ) - if (!result.isSucceeded) { - logger.error(result.getErrorMessage) - } - result.isSucceeded - } - - override def deleteAsync(uuid: String, index: String, _type: String)(implicit - ec: ExecutionContext - ): Future[Boolean] = { - import JestClientResultHandler._ - val promise: Promise[Boolean] = Promise() - apply().executeAsyncPromise( - new Delete.Builder(uuid).index(index).`type`(_type).build() - ) onComplete { - case Success(s) => - if (!s.isSucceeded) - logger.error(s.getErrorMessage) - promise.success(s.isSucceeded) - case Failure(f) => - logger.error(f.getMessage, f) - promise.failure(f) - } - promise.future - } - -} - -trait JestGetApi extends GetApi with JestClientCompanion { - - // GetApi - override def get[U <: Timestamped]( - id: String, - index: Option[String] = None, - maybeType: Option[String] = None - )(implicit m: Manifest[U], formats: Formats): Option[U] = { - val result = apply().execute( - new Get.Builder( - index.getOrElse( - maybeType.getOrElse( - m.runtimeClass.getSimpleName.toLowerCase - ) - ), - id - ).build() - ) - if (result.isSucceeded) { - Some(serialization.read[U](result.getSourceAsString)) - } else { - logger.error(result.getErrorMessage) - None - } - } - - override def getAsync[U <: Timestamped]( - id: String, - index: Option[String] = None, - maybeType: Option[String] = None - )(implicit m: Manifest[U], ec: ExecutionContext, formats: Formats): Future[Option[U]] = { - import JestClientResultHandler._ - val promise: Promise[Option[U]] = Promise() - apply().executeAsyncPromise( - new Get.Builder( - index.getOrElse( - maybeType.getOrElse( - m.runtimeClass.getSimpleName.toLowerCase - ) - ), - id - ).build() - ) onComplete { - case Success(result) => - if (result.isSucceeded) - promise.success(Some(serialization.read[U](result.getSourceAsString))) - else { - logger.error(result.getErrorMessage) - promise.success(None) - } - case Failure(f) => - logger.error(f.getMessage, f) - promise.failure(f) - } - promise.future - } - -} - -trait JestSearchApi extends SearchApi with JestClientCompanion { - - import JestClientApi._ - - override def search[U]( - jsonQuery: JSONQuery - )(implicit m: Manifest[U], formats: Formats): List[U] = { - import jsonQuery._ - val search = new Search.Builder(query) - for (indice <- indices) search.addIndex(indice) - for (t <- types) search.addType(t) - Try( - apply() - .execute(search.build()) - .getSourceAsStringList - .asScala - .map(source => serialization.read[U](source)) - .toList - ) match { - case Success(s) => s - case Failure(f) => - logger.error(f.getMessage, f) - List.empty - } - } - - override def search[U](sqlQuery: SQLQuery)(implicit m: Manifest[U], formats: Formats): List[U] = { - val search: Option[Search] = sqlQuery.search - (search match { - case Some(s) => - val result = apply().execute(s) - if (result.isSucceeded) { - Some(result) - } else { - logger.error(result.getErrorMessage) - None - } - case _ => None - }) match { - case Some(searchResult) => - Try( - searchResult.getSourceAsStringList.asScala - .map(source => serialization.read[U](source)) - .toList - ) match { - case Success(s) => s - case Failure(f) => - logger.error(f.getMessage, f) - List.empty - } - case _ => List.empty - } - } - - override def searchAsync[U]( - sqlQuery: SQLQuery - )(implicit m: Manifest[U], ec: ExecutionContext, formats: Formats): Future[List[U]] = { - val promise = Promise[List[U]]() - val search: Option[Search] = sqlQuery.search - search match { - case Some(s) => - import JestClientResultHandler._ - apply().executeAsyncPromise(s) onComplete { - case Success(searchResult) => - promise.success( - searchResult.getSourceAsStringList.asScala - .map(source => serialization.read[U](source)) - .toList - ) - case Failure(f) => - promise.failure(f) - } - case _ => promise.success(List.empty) - } - promise.future - } - - override def searchWithInnerHits[U, I](sqlQuery: SQLQuery, innerField: String)(implicit - m1: Manifest[U], - m2: Manifest[I], - formats: Formats - ): List[(U, List[I])] = { - val search: Option[Search] = sqlQuery.search - (search match { - case Some(s) => - val result = apply().execute(s) - if (result.isSucceeded) { - Some(result) - } else { - logger.error(result.getErrorMessage) - None - } - case _ => None - }) match { - case Some(searchResult) => - Try(searchResult.getJsonObject ~> [U, I] innerField) match { - case Success(s) => s - case Failure(f) => - logger.error(f.getMessage, f) - List.empty - } - case _ => List.empty - } - } - - override def searchWithInnerHits[U, I](jsonQuery: JSONQuery, innerField: String)(implicit - m1: Manifest[U], - m2: Manifest[I], - formats: Formats - ): List[(U, List[I])] = { - val result = apply().execute(jsonQuery.search) - (if (result.isSucceeded) { - Some(result) - } else { - logger.error(result.getErrorMessage) - None - }) match { - case Some(searchResult) => - Try(searchResult.getJsonObject ~> [U, I] innerField) match { - case Success(s) => s - case Failure(f) => - logger.error(f.getMessage, f) - List.empty - } - case _ => List.empty - } - } - - override def multiSearch[U]( - sqlQueries: SQLQueries - )(implicit m: Manifest[U], formats: Formats): List[List[U]] = { - val searches: List[Search] = sqlQueries.queries.flatMap(_.search) - (if (searches.size == sqlQueries.queries.size) { - Some(apply().execute(new MultiSearch.Builder(searches.asJava).build())) - } else { - None - }) match { - case Some(multiSearchResult) => - multiSearchResult.getResponses.asScala - .map(searchResponse => - searchResponse.searchResult.getSourceAsStringList.asScala - .map(source => serialization.read[U](source)) - .toList - ) - .toList - case _ => List.empty - } - } - - override def multiSearch[U]( - jsonQueries: JSONQueries - )(implicit m: Manifest[U], formats: Formats): List[List[U]] = { - val searches: List[Search] = jsonQueries.queries.map(_.search) - val multiSearchResult = apply().execute(new MultiSearch.Builder(searches.asJava).build()) - multiSearchResult.getResponses.asScala - .map(searchResponse => - searchResponse.searchResult.getSourceAsStringList.asScala - .map(source => serialization.read[U](source)) - .toList - ) - .toList - } - - override def multiSearchWithInnerHits[U, I](sqlQueries: SQLQueries, innerField: String)(implicit - m1: Manifest[U], - m2: Manifest[I], - formats: Formats - ): List[List[(U, List[I])]] = { - val searches: List[Search] = sqlQueries.queries.flatMap(_.search) - if (searches.size == sqlQueries.queries.size) { - nativeMultiSearchWithInnerHits(searches, innerField) - } else { - List.empty - } - } - - override def multiSearchWithInnerHits[U, I](jsonQueries: JSONQueries, innerField: String)(implicit - m1: Manifest[U], - m2: Manifest[I], - formats: Formats - ): List[List[(U, List[I])]] = { - nativeMultiSearchWithInnerHits(jsonQueries.queries.map(_.search), innerField) - } - - private[this] def nativeMultiSearchWithInnerHits[U, I]( - searches: List[Search], - innerField: String - )(implicit m1: Manifest[U], m2: Manifest[I], formats: Formats): List[List[(U, List[I])]] = { - val multiSearchResult = apply().execute(new MultiSearch.Builder(searches.asJava).build()) - if (multiSearchResult.isSucceeded) { - multiSearchResult.getResponses.asScala - .map(searchResponse => searchResponse.searchResult.getJsonObject ~> [U, I] innerField) - .toList - } else { - logger.error(multiSearchResult.getErrorMessage) - List.empty - } - } - -} - -trait JestBulkApi - extends JestRefreshApi - with JestUpdateSettingsApi - with JestIndicesApi - with BulkApi - with JestClientCompanion { - override type A = BulkableAction[DocumentResult] - override type R = BulkResult - - override implicit def toBulkElasticAction(a: A): BulkElasticAction = - new BulkElasticAction { - override def index: String = a.getIndex - } - - private[this] def toBulkElasticResultItem(i: BulkResult#BulkResultItem): BulkElasticResultItem = - new BulkElasticResultItem { - override def index: String = i.index - } - - override implicit def toBulkElasticResult(r: R): BulkElasticResult = - new BulkElasticResult { - override def items: List[BulkElasticResultItem] = - r.getItems.asScala.toList.map(toBulkElasticResultItem) - } - - override def bulk(implicit - bulkOptions: BulkOptions, - system: ActorSystem - ): Flow[Seq[A], R, NotUsed] = { - import JestClientResultHandler._ - val parallelism = Math.max(1, bulkOptions.balance) - - Flow[Seq[BulkableAction[DocumentResult]]] - .named("bulk") - .mapAsyncUnordered[BulkResult](parallelism)(items => { - logger.info(s"Starting to write batch of ${items.size}...") - val init = - new Bulk.Builder().defaultIndex(bulkOptions.index).defaultType(bulkOptions.documentType) - val bulkQuery = items.foldLeft(init) { (current, query) => - current.addAction(query) - } - apply().executeAsyncPromise(bulkQuery.build()) - }) - } - - override def bulkResult: Flow[R, Set[String], NotUsed] = - Flow[BulkResult] - .named("result") - .map(result => { - val items = result.getItems - val indices = items.asScala.map(_.index).toSet - logger.info(s"Finished to write batch of ${items.size} within ${indices.mkString(",")}.") - indices - }) - - override def toBulkAction(bulkItem: BulkItem): A = { - val builder = bulkItem.action match { - case BulkAction.DELETE => new Delete.Builder(bulkItem.body) - case BulkAction.UPDATE => new Update.Builder(bulkItem.body) - case _ => new Index.Builder(bulkItem.body) - } - bulkItem.id.foreach(builder.id) - builder.index(bulkItem.index) - bulkItem.parent.foreach(s => builder.setParameter(Parameters.PARENT, s)) - builder.build() - } - -} - -object JestClientApi { - implicit class SearchSQLQuery(sqlQuery: SQLQuery) { - def search: Option[Search] = { - import ElasticQuery._ - select(sqlQuery) match { - case Some(elasticSelect) => - import elasticSelect._ - Console.println(query) - val search = new Search.Builder(query) - for (source <- sources) search.addIndex(source) - Some(search.build()) - case _ => None - } - } - } - - implicit class SearchJSONQuery(jsonQuery: JSONQuery) { - def search: Search = { - import jsonQuery._ - val _search = new Search.Builder(query) - for (indice <- indices) _search.addIndex(indice) - for (t <- types) _search.addType(t) - _search.build() - } - } - - implicit class SearchResults(searchResult: SearchResult) { - def apply[M: Manifest]()(implicit formats: Formats): List[M] = { - searchResult.getSourceAsStringList.asScala.map(source => serialization.read[M](source)).toList - } - } - - implicit class JestBulkAction(bulkableAction: BulkableAction[DocumentResult]) { - def index: String = bulkableAction.getIndex - } -} diff --git a/elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestClientCompanion.scala b/elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestClientCompanion.scala deleted file mode 100644 index b9c78a83..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestClientCompanion.scala +++ /dev/null @@ -1,160 +0,0 @@ -package app.softnetwork.elastic.client.jest - -import java.io.IOException -import java.util -import java.util.concurrent.TimeUnit -import app.softnetwork.elastic.client.{ElasticConfig, ElasticCredentials} -import com.sksamuel.exts.Logging -import io.searchbox.action.Action -import io.searchbox.client.{JestClient, JestClientFactory, JestResult, JestResultHandler} -import io.searchbox.client.config.HttpClientConfig -import org.apache.http.HttpHost - -import scala.collection.JavaConverters._ -import scala.util.{Failure, Success, Try} - -import scala.language.reflectiveCalls - -/** Created by smanciot on 20/05/2021. - */ -trait JestClientCompanion extends Logging { - - def elasticConfig: ElasticConfig - - private[this] var jestClient: Option[InnerJestClient] = None - - private[this] val factory = new JestClientFactory() - - private[this] var httpClientConfig: HttpClientConfig = _ - - private[this] class InnerJestClient(private var _jestClient: JestClient) extends JestClient { - private[this] var nbFailures: Int = 0 - - override def shutdownClient(): Unit = { - close() - } - - private def checkClient(): Unit = { - Option(_jestClient) match { - case None => - factory.setHttpClientConfig(httpClientConfig) - _jestClient = Try(factory.getObject) match { - case Success(s) => - s - case Failure(f) => - logger.error(f.getMessage, f) - throw f - } - case _ => - } - } - - override def executeAsync[J <: JestResult]( - clientRequest: Action[J], - jestResultHandler: JestResultHandler[_ >: J] - ): Unit = { - Try(checkClient()) - Option(_jestClient) match { - case Some(s) => s.executeAsync[J](clientRequest, jestResultHandler) - case _ => - close() - jestResultHandler.failed(new Exception("JestClient not initialized")) - } - } - - override def execute[J <: JestResult](clientRequest: Action[J]): J = { - Try(checkClient()) - Option(_jestClient) match { - case Some(j) => - Try(j.execute[J](clientRequest)) match { - case Success(s) => - nbFailures = 0 - s - case Failure(f) => - f match { - case e: IOException => - nbFailures += 1 - logger.error(e.getMessage, e) - close() - if (nbFailures < 10) { - Thread.sleep(1000 * nbFailures) - execute(clientRequest) - } else { - throw f - } - case e: IllegalStateException => - nbFailures += 1 - logger.error(e.getMessage, e) - close() - if (nbFailures < 10) { - Thread.sleep(1000 * nbFailures) - execute(clientRequest) - } else { - throw f - } - case _ => - close() - throw f - } - } - case _ => - close() - throw new Exception("JestClient not initialized") - } - } - - override def setServers(servers: util.Set[String]): Unit = { - Try(checkClient()) - Option(_jestClient).foreach(_.setServers(servers)) - } - - override def close(): Unit = { - Option(_jestClient).foreach(_.close()) - _jestClient = null - } - } - - private[this] def getHttpHosts(esUrl: String): Set[HttpHost] = { - esUrl - .split(",") - .map(u => { - val url = new java.net.URL(u) - new HttpHost(url.getHost, url.getPort, url.getProtocol) - }) - .toSet - } - - def apply(): JestClient = { - apply( - elasticConfig.credentials, - multithreaded = elasticConfig.multithreaded, - discoveryEnabled = elasticConfig.discoveryEnabled - ) - } - - def apply( - esCredentials: ElasticCredentials, - multithreaded: Boolean = true, - timeout: Int = 60000, - discoveryEnabled: Boolean = false, - discoveryFrequency: Long = 60L, - discoveryFrequencyTimeUnit: TimeUnit = TimeUnit.SECONDS - ): JestClient = { - jestClient match { - case Some(s) => s - case None => - httpClientConfig = new HttpClientConfig.Builder(esCredentials.url) - .defaultCredentials(esCredentials.username, esCredentials.password) - .preemptiveAuthTargetHosts(getHttpHosts(esCredentials.url).asJava) - .multiThreaded(multithreaded) - .discoveryEnabled(discoveryEnabled) - .discoveryFrequency(discoveryFrequency, discoveryFrequencyTimeUnit) - .connTimeout(timeout) - .readTimeout(timeout) - .build() - factory.setHttpClientConfig(httpClientConfig) - jestClient = Some(new InnerJestClient(factory.getObject)) - jestClient.get - } - } -} diff --git a/elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestClientResultHandler.scala b/elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestClientResultHandler.scala deleted file mode 100644 index fc05e2e2..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestClientResultHandler.scala +++ /dev/null @@ -1,44 +0,0 @@ -package app.softnetwork.elastic.client.jest - -import io.searchbox.action.Action -import io.searchbox.client.{JestClient, JestResult, JestResultHandler} -import io.searchbox.core.BulkResult - -import scala.concurrent.{Future, Promise} - -/** Created by smanciot on 28/04/17. - */ -private class JestClientResultHandler[T <: JestResult] extends JestResultHandler[T] { - - protected val promise: Promise[T] = Promise() - - override def completed(result: T): Unit = - if (!result.isSucceeded) - promise.failure(new Exception(s"${result.getErrorMessage} - ${result.getJsonString}")) - else { - result match { - case r: BulkResult if !r.getFailedItems.isEmpty => - promise.failure( - new Exception(s"We don't allow any failed item while indexing ${result.getJsonString}") - ) - case _ => promise.success(result) - - } - } - - override def failed(exception: Exception): Unit = promise.failure(exception) - - def future: Future[T] = promise.future - -} - -object JestClientResultHandler { - - implicit class PromiseJestClient(jestClient: JestClient) { - def executeAsyncPromise[T <: JestResult](clientRequest: Action[T]): Future[T] = { - val resultHandler = new JestClientResultHandler[T]() - jestClient.executeAsync(clientRequest, resultHandler) - resultHandler.future - } - } -} diff --git a/elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestProvider.scala b/elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestProvider.scala deleted file mode 100644 index 19e12c0a..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/client/jest/JestProvider.scala +++ /dev/null @@ -1,11 +0,0 @@ -package app.softnetwork.elastic.client.jest - -import app.softnetwork.elastic.persistence.query.ElasticProvider -import app.softnetwork.persistence.ManifestWrapper -import app.softnetwork.persistence.model.Timestamped - -/** Created by smanciot on 20/05/2021. - */ -trait JestProvider[T <: Timestamped] extends ElasticProvider[T] with JestClientApi { - _: ManifestWrapper[T] => -} diff --git a/elastic/src/main/scala/app/softnetwork/elastic/client/package.scala b/elastic/src/main/scala/app/softnetwork/elastic/client/package.scala deleted file mode 100644 index 9fa533e3..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/client/package.scala +++ /dev/null @@ -1,169 +0,0 @@ -package app.softnetwork.elastic - -import akka.stream.{Attributes, FlowShape, Inlet, Outlet} -import akka.stream.stage.{GraphStage, GraphStageLogic} -import app.softnetwork.elastic.client.BulkAction.BulkAction -import app.softnetwork.serialization._ -import com.google.gson.{Gson, JsonElement, JsonObject} -import com.typesafe.config.{Config, ConfigFactory} -import com.typesafe.scalalogging.StrictLogging -import configs.Configs -import org.json4s.Formats - -import scala.collection.immutable.Seq -import scala.collection.mutable -import scala.language.reflectiveCalls -import scala.util.{Failure, Success, Try} - -/** Created by smanciot on 30/06/2018. - */ -package object client { - - case class ElasticCredentials( - url: String = "http://localhost:9200", - username: String = "", - password: String = "" - ) - - case class ElasticConfig( - credentials: ElasticCredentials = ElasticCredentials(), - multithreaded: Boolean = true, - discoveryEnabled: Boolean = false - ) - - object ElasticConfig extends StrictLogging { - def apply(config: Config): ElasticConfig = { - Configs[ElasticConfig] - .get(config.withFallback(ConfigFactory.load("softnetwork-elastic.conf")), "elastic") - .toEither match { - case Left(configError) => - logger.error(s"Something went wrong with the provided arguments $configError") - throw configError.configException - case Right(r) => r - } - } - } - - object BulkAction extends Enumeration { - type BulkAction = Value - val INDEX: client.BulkAction.Value = Value(0, "INDEX") - val UPDATE: client.BulkAction.Value = Value(1, "UPDATE") - val DELETE: client.BulkAction.Value = Value(2, "DELETE") - } - - case class BulkItem( - index: String, - action: BulkAction, - body: String, - id: Option[String], - parent: Option[String] - ) - - case class BulkOptions( - index: String, - documentType: String, - maxBulkSize: Int = 100, - balance: Int = 1, - disableRefresh: Boolean = false - ) - - trait BulkElasticAction { def index: String } - - trait BulkElasticResult { def items: List[BulkElasticResultItem] } - - trait BulkElasticResultItem { def index: String } - - case class BulkSettings[A](disableRefresh: Boolean = false)(implicit - updateSettingsApi: UpdateSettingsApi, - toBulkElasticAction: A => BulkElasticAction - ) extends GraphStage[FlowShape[A, A]] { - - val in: Inlet[A] = Inlet[A]("Filter.in") - val out: Outlet[A] = Outlet[A]("Filter.out") - - val shape: FlowShape[A, A] = FlowShape.of(in, out) - - val indices = mutable.Set.empty[String] - - override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = { - new GraphStageLogic(shape) { - setHandler( - in, - () => { - val elem = grab(in) - val index = elem.index - if (!indices.contains(index)) { - if (disableRefresh) { - updateSettingsApi.updateSettings( - index, - """{"index" : {"refresh_interval" : "-1", "number_of_replicas" : 0} }""" - ) - } - indices.add(index) - } - push(out, elem) - } - ) - setHandler( - out, - () => { - pull(in) - } - ) - } - } - } - - def docAsUpsert(doc: String): String = s"""{"doc":$doc,"doc_as_upsert":true}""" - - implicit class InnerHits(searchResult: JsonObject) { - import scala.collection.JavaConverters._ - def ~>[M, I]( - innerField: String - )(implicit formats: Formats, m: Manifest[M], i: Manifest[I]): List[(M, List[I])] = { - def innerHits(result: JsonElement) = { - result.getAsJsonObject - .get("inner_hits") - .getAsJsonObject - .get(innerField) - .getAsJsonObject - .get("hits") - .getAsJsonObject - .get("hits") - .getAsJsonArray - .iterator() - } - val gson = new Gson() - val results = searchResult.get("hits").getAsJsonObject.get("hits").getAsJsonArray.iterator() - (for (result <- results.asScala) - yield ( - result match { - case obj: JsonObject => - Try { - serialization.read[M](gson.toJson(obj.get("_source"))) - } match { - case Success(s) => s - case Failure(f) => - throw f - } - case _ => serialization.read[M](result.getAsString) - }, - (for (innerHit <- innerHits(result).asScala) yield innerHit match { - case obj: JsonObject => - Try { - serialization.read[I](gson.toJson(obj.get("_source"))) - } match { - case Success(s) => s - case Failure(f) => - throw f - } - case _ => serialization.read[I](innerHit.getAsString) - }).toList - )).toList - } - } - - case class JSONQuery(query: String, indices: Seq[String], types: Seq[String] = Seq.empty) - - case class JSONQueries(queries: List[JSONQuery]) -} diff --git a/elastic/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticProvider.scala b/elastic/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticProvider.scala deleted file mode 100644 index 955e4679..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticProvider.scala +++ /dev/null @@ -1,175 +0,0 @@ -package app.softnetwork.elastic.persistence.query - -import app.softnetwork.elastic.client.ElasticClientApi -import app.softnetwork.elastic.sql.SQLQuery -import mustache.Mustache -import org.json4s.Formats -import app.softnetwork.persistence._ -import app.softnetwork.persistence.model.Timestamped -import app.softnetwork.persistence.query.ExternalPersistenceProvider -import app.softnetwork.serialization.commonFormats -import app.softnetwork.elastic.persistence.typed.Elastic._ -import com.sksamuel.exts.Logging - -import scala.reflect.ClassTag -import scala.util.{Failure, Success, Try} - -/** Created by smanciot on 16/05/2020. - */ -trait ElasticProvider[T <: Timestamped] extends ExternalPersistenceProvider[T] with Logging { - _: ElasticClientApi with ManifestWrapper[T] => - - implicit def formats: Formats = commonFormats - - protected lazy val index: String = getIndex[T](manifestWrapper.wrapped) - - protected lazy val _type: String = getType[T](manifestWrapper.wrapped) - - protected lazy val alias: String = getAlias[T](manifestWrapper.wrapped) - - protected def mappingPath: Option[String] = None - - protected def loadMapping(path: Option[String] = None): String = { - val pathOrElse: String = path.getOrElse(s"""mapping/${_type}.mustache""") - Try(Mustache(pathOrElse).render(Map("type" -> _type))) match { - case Success(s) => - s - case Failure(f) => - logger.error(s"$pathOrElse -> f.getMessage", f) - "{}" - } - } - - protected def initIndex(): Unit = { - Try { - createIndex(index) - addAlias(index, alias) - setMapping(index, _type, loadMapping(mappingPath)) - } match { - case Success(_) => logger.info(s"index:$index type:${_type} alias:$alias created") - case Failure(f) => - logger.error(s"!!!!! index:$index type:${_type} alias:$alias -> ${f.getMessage}", f) - } - } - - // ExternalPersistenceProvider - - /** Creates the underlying document to the external system - * - * @param document - * - the document to create - * @param t - * - implicit ClassTag for T - * @return - * whether the operation is successful or not - */ - override def createDocument(document: T)(implicit t: ClassTag[T]): Boolean = { - Try(index(document, Some(index), Some(_type))) match { - case Success(_) => true - case Failure(f) => - logger.error(f.getMessage, f) - false - } - } - - /** Updates the underlying document to the external system - * - * @param document - * - the document to update - * @param upsert - * - whether or not to create the underlying document if it does not exist in the external - * system - * @param t - * - implicit ClassTag for T - * @return - * whether the operation is successful or not - */ - override def updateDocument(document: T, upsert: Boolean)(implicit t: ClassTag[T]): Boolean = { - Try(update(document, Some(index), Some(_type), upsert)) match { - case Success(_) => true - case Failure(f) => - logger.error(f.getMessage, f) - false - } - } - - /** Deletes the underlying document referenced by its uuid to the external system - * - * @param uuid - * - the uuid of the document to delete - * @return - * whether the operation is successful or not - */ - override def deleteDocument(uuid: String): Boolean = { - Try( - delete(uuid, index, _type) - ) match { - case Success(value) => value - case Failure(f) => - logger.error(f.getMessage, f) - false - } - } - - /** Upsert the underlying document referenced by its uuid to the external system - * - * @param uuid - * - the uuid of the document to upsert - * @param data - * - a map including all the properties and values tu upsert for the document - * @return - * whether the operation is successful or not - */ - override def upsertDocument(uuid: String, data: String): Boolean = { - logger.debug(s"Upserting document $uuid with $data") - Try( - update( - index, - _type, - uuid, - data, - upsert = true - ) - ) match { - case Success(_) => true - case Failure(f) => - logger.error(f.getMessage, f) - false - } - } - - /** Load the document referenced by its uuid - * - * @param uuid - * - the document uuid - * @return - * the document retrieved, None otherwise - */ - override def loadDocument(uuid: String)(implicit m: Manifest[T], formats: Formats): Option[T] = { - Try(get(uuid, Some(index), Some(_type))) match { - case Success(s) => s - case Failure(f) => - logger.error(f.getMessage, f) - None - } - } - - /** Search documents - * - * @param query - * - the search query - * @return - * the documents founds or an empty list otherwise - */ - override def searchDocuments( - query: String - )(implicit m: Manifest[T], formats: Formats): List[T] = { - Try(search(SQLQuery(query))) match { - case Success(s) => s - case Failure(f) => - logger.error(f.getMessage, f) - List.empty - } - } - -} diff --git a/elastic/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStream.scala b/elastic/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStream.scala deleted file mode 100644 index 88af3f3b..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStream.scala +++ /dev/null @@ -1,24 +0,0 @@ -package app.softnetwork.elastic.persistence.query - -import app.softnetwork.persistence.ManifestWrapper -import app.softnetwork.persistence.query.{ - JournalProvider, - OffsetProvider, - State2ExternalProcessorStream -} -import app.softnetwork.persistence.model.Timestamped -import app.softnetwork.persistence.message._ - -/** Created by smanciot on 16/05/2020. - */ -trait State2ElasticProcessorStream[T <: Timestamped, E <: CrudEvent] - extends State2ExternalProcessorStream[T, E] - with ManifestWrapper[T] { _: JournalProvider with OffsetProvider with ElasticProvider[T] => - - override val externalProcessor = "elastic" - - override protected def init(): Unit = { - initIndex() - } - -} diff --git a/elastic/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJestProvider.scala b/elastic/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJestProvider.scala deleted file mode 100644 index 21a628c2..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJestProvider.scala +++ /dev/null @@ -1,10 +0,0 @@ -package app.softnetwork.elastic.persistence.query - -import app.softnetwork.elastic.client.jest.JestProvider -import app.softnetwork.persistence.message.CrudEvent -import app.softnetwork.persistence.model.Timestamped -import app.softnetwork.persistence.query.{JournalProvider, OffsetProvider} - -trait State2ElasticProcessorStreamWithJestProvider[T <: Timestamped, E <: CrudEvent] - extends State2ElasticProcessorStream[T, E] - with JestProvider[T] { _: JournalProvider with OffsetProvider => } diff --git a/elastic/src/main/scala/app/softnetwork/elastic/persistence/typed/Elastic.scala b/elastic/src/main/scala/app/softnetwork/elastic/persistence/typed/Elastic.scala deleted file mode 100644 index 1efe4d59..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/persistence/typed/Elastic.scala +++ /dev/null @@ -1,31 +0,0 @@ -package app.softnetwork.elastic.persistence.typed - -import app.softnetwork.persistence._ - -import app.softnetwork.persistence.model.Timestamped - -import scala.language.implicitConversions - -import app.softnetwork.persistence._ - -/** Created by smanciot on 10/04/2020. - */ -object Elastic { - - def index(_type: String): String = { - s"${_type}s-$environment".toLowerCase - } - - def alias(_type: String): String = { - s"${_type}s-$environment-v$version".toLowerCase - } - - def getAlias[T <: Timestamped](implicit m: Manifest[T]): String = { - alias(getType[T]) - } - - def getIndex[T <: Timestamped](implicit m: Manifest[T]): String = { - index(getType[T]) - } - -} diff --git a/elastic/src/main/scala/app/softnetwork/elastic/sql/ElasticFilters.scala b/elastic/src/main/scala/app/softnetwork/elastic/sql/ElasticFilters.scala deleted file mode 100644 index cf19fa74..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/sql/ElasticFilters.scala +++ /dev/null @@ -1,138 +0,0 @@ -package app.softnetwork.elastic.sql - -import com.sksamuel.elastic4s.ElasticApi._ -import com.sksamuel.elastic4s.searches.ScoreMode -import com.sksamuel.elastic4s.searches.queries.Query -import com.sksamuel.elastic4s.searches.queries.term.{BuildableTermsQuery, TermsQuery} - -import scala.annotation.tailrec - -/** Created by smanciot on 27/06/2018. - */ -object ElasticFilters { - - import SQLImplicits._ - - implicit def BuildableTermsNoOp[T]: BuildableTermsQuery[T] = new BuildableTermsQuery[T] { - override def build(q: TermsQuery[T]): Any = null // not used by the http builders - } - - def filter(query: String): Query = { - val criteria: Option[SQLCriteria] = query - filter(criteria) - } - - def filter(criteria: Option[SQLCriteria]): Query = { - - var _innerHits: Set[String] = Set.empty - - @tailrec - def _innerHit(name: String, inc: Int = 1): String = { - if (_innerHits.contains(name)) { - val incName = s"$name$inc" - if (_innerHits.contains(incName)) { - _innerHit(name, inc + 1) - } else { - _innerHits += incName - incName - } - } else { - _innerHits += name - name - } - } - - def _filter(criteria: SQLCriteria): Query = { - criteria match { - case ElasticGeoDistance(identifier, distance, lat, lon) => - geoDistanceQuery(identifier.identifier) - .point(lat.value, lon.value) distance distance.value - case SQLExpression(identifier, operator, value) => - value match { - case n: SQLNumeric[Any] @unchecked => - operator match { - case _: GE.type => rangeQuery(identifier.identifier) gte n.sql - case _: GT.type => rangeQuery(identifier.identifier) gt n.sql - case _: LE.type => rangeQuery(identifier.identifier) lte n.sql - case _: LT.type => rangeQuery(identifier.identifier) lt n.sql - case _: EQ.type => termQuery(identifier.identifier, n.sql) - case _: NE.type => not(termQuery(identifier.identifier, n.sql)) - case _ => matchAllQuery - } - case l: SQLLiteral => - operator match { - case _: LIKE.type => regexQuery(identifier.identifier, toRegex(l.value)) - case _: GE.type => rangeQuery(identifier.identifier) gte l.value - case _: GT.type => rangeQuery(identifier.identifier) gt l.value - case _: LE.type => rangeQuery(identifier.identifier) lte l.value - case _: LT.type => rangeQuery(identifier.identifier) lt l.value - case _: EQ.type => termQuery(identifier.identifier, l.value) - case _: NE.type => not(termQuery(identifier.identifier, l.value)) - case _ => matchAllQuery - } - case b: SQLBoolean => - operator match { - case _: EQ.type => termQuery(identifier.identifier, b.value) - case _: NE.type => not(termQuery(identifier.identifier, b.value)) - case _ => matchAllQuery - } - case _ => matchAllQuery - } - case SQLIsNull(identifier) => not(existsQuery(identifier.identifier)) - case SQLIsNotNull(identifier) => existsQuery(identifier.identifier) - case SQLPredicate(left, operator, right, _not) => - operator match { - case _: AND.type => - if (_not.isDefined) - bool(Seq(_filter(left)), Seq.empty, Seq(_filter(right))) - else - boolQuery().filter(_filter(left), _filter(right)) - case _: OR.type => should(_filter(left), _filter(right)) - case _ => matchAllQuery - } - case SQLIn(identifier, values, n) => - val _values: Seq[Any] = values.innerValues - val t = - _values.headOption match { - case Some(_: Double) => - termsQuery(identifier.identifier, _values.asInstanceOf[Seq[Double]]) - case Some(_: Integer) => - termsQuery(identifier.identifier, _values.asInstanceOf[Seq[Integer]]) - case Some(_: Long) => - termsQuery(identifier.identifier, _values.asInstanceOf[Seq[Long]]) - case _ => termsQuery(identifier.identifier, _values.map(_.toString)) - } - n match { - case Some(_) => not(t) - case None => t - } - case SQLBetween(identifier, from, to) => - rangeQuery(identifier.identifier) gte from.value lte to.value - case relation: ElasticRelation => - import scala.language.reflectiveCalls - val t = relation.`type` - t match { - case Some(_) => - relation match { - case _: ElasticNested => - nestedQuery(t.get, _filter(relation.criteria)).inner(innerHits(_innerHit(t.get))) - case _: ElasticChild => - hasChildQuery(t.get, _filter(relation.criteria), ScoreMode.None) - case _: ElasticParent => - hasParentQuery(t.get, _filter(relation.criteria), score = false) - case _ => matchAllQuery - } - case _ => matchAllQuery - } - case _ => matchAllQuery - } - } - - criteria match { - case Some(c) => _filter(c) - case _ => matchAllQuery - } - - } - -} diff --git a/elastic/src/main/scala/app/softnetwork/elastic/sql/ElasticQuery.scala b/elastic/src/main/scala/app/softnetwork/elastic/sql/ElasticQuery.scala deleted file mode 100644 index 84b8c404..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/sql/ElasticQuery.scala +++ /dev/null @@ -1,174 +0,0 @@ -package app.softnetwork.elastic.sql - -import com.sksamuel.elastic4s.ElasticApi._ -import com.sksamuel.elastic4s.http.search.SearchBodyBuilderFn -import com.sksamuel.elastic4s.searches.queries.{BoolQuery, Query} - -/** Created by smanciot on 27/06/2018. - */ -object ElasticQuery { - - import ElasticFilters._ - import SQLImplicits._ - - def select(sqlQuery: SQLQuery): Option[ElasticSelect] = select(sqlQuery.query) - - private[this] def select(query: String): Option[ElasticSelect] = { - val select: Option[SQLSelectQuery] = query - select match { - - case Some(s) => - val criteria = s.where match { - case Some(w) => w.criteria - case _ => None - } - - val fields = s.select.fields.map(_.identifier.identifier) - - val sources = s.from.tables.map((table: SQLTable) => table.source.sql) - - val queryFiltered = filter(criteria) match { - case b: BoolQuery => b - case q: Query => boolQuery().filter(q) - } - - var _search = search("") query { - queryFiltered - } sourceInclude fields - - _search = s.limit match { - case Some(l) => _search limit l.limit from 0 - case _ => _search - } - - val q = SearchBodyBuilderFn(_search).string() - - Some(ElasticSelect(s.select.fields, sources, q.replace("\"version\":true,", "") /*FIXME*/ )) - - case _ => None - } - } - - def count(sqlQuery: SQLQuery): Seq[ElasticCount] = { - val select: Option[SQLSelectQuery] = sqlQuery.query - count(select) - } - - private[this] def count(select: Option[SQLSelectQuery]): Seq[ElasticCount] = { - select match { - case Some(s: SQLCountQuery) => - val criteria = s.where match { - case Some(w) => w.criteria - case _ => None - } - val sources = s.from.tables.map((table: SQLTable) => table.source.sql) - s.selectCount.countFields.map((countField: SQLCountField) => { - val sourceField = countField.identifier.identifier - - val field = countField.alias match { - case Some(alias) => alias.alias - case _ => sourceField - } - - val distinct = countField.identifier.distinct.isDefined - - val filtered = countField.filter - - val isFiltered = filtered.isDefined - - val nested = sourceField.contains(".") - - val agg = - if (distinct) - s"agg_distinct_${sourceField.replace(".", "_")}" - else - s"agg_${sourceField.replace(".", "_")}" - - var aggPath = Seq[String]() - - val queryFiltered = filter(criteria) match { - case b: BoolQuery => b - case q: Query => boolQuery().filter(q) - } - - val q = - if (sourceField.equalsIgnoreCase("_id")) { // "native" elastic count - SearchBodyBuilderFn( - search("") query { - queryFiltered - } - ).string() - } else { - val _agg = - if (distinct) - cardinalityAgg(agg, sourceField) - else - valueCountAgg(agg, sourceField) - - def _filtered = { - if (isFiltered) { - val filteredAgg = s"filtered_agg" - aggPath ++= Seq(filteredAgg) - filterAgg(filteredAgg, filter(filtered.get.criteria)) subaggs { - aggPath ++= Seq(agg) - _agg - } - } else { - aggPath ++= Seq(agg) - _agg - } - } - - SearchBodyBuilderFn( - search("") query { - queryFiltered - } - aggregations { - if (nested) { - val path = sourceField.split("\\.").head - val nestedAgg = s"nested_$path" - aggPath ++= Seq(nestedAgg) - nestedAggregation(nestedAgg, path) subaggs { - _filtered - } - } else { - _filtered - } - } - size 0 - ).string() - } - - ElasticCount( - aggPath.mkString("."), - field, - sourceField, - sources, - q.replace("\"version\":true,", ""), /*FIXME*/ - distinct, - nested, - isFiltered - ) - }) - case _ => Seq.empty - } - } - -} - -case class ElasticCount( - agg: String, - field: String, - sourceField: String, - sources: Seq[String], - query: String, - distinct: Boolean = false, - nested: Boolean = false, - filtered: Boolean = false -) - -case class ElasticSelect( - fields: Seq[SQLField], - sources: Seq[String], - query: String -) diff --git a/elastic/src/main/scala/app/softnetwork/elastic/sql/SQLImplicits.scala b/elastic/src/main/scala/app/softnetwork/elastic/sql/SQLImplicits.scala deleted file mode 100644 index 5888c0cd..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/sql/SQLImplicits.scala +++ /dev/null @@ -1,30 +0,0 @@ -package app.softnetwork.elastic.sql - -import scala.util.matching.Regex - -/** Created by smanciot on 27/06/2018. - */ -object SQLImplicits { - import scala.language.implicitConversions - - implicit def queryToSQLCriteria(query: String): Option[SQLCriteria] = { - val sql: Option[SQLSelectQuery] = query - sql match { - case Some(q) => - q.where match { - case Some(w) => w.criteria - case _ => None - } - case _ => None - } - } - implicit def queryToSQLQuery(query: String): Option[SQLSelectQuery] = { - SQLParser(query) match { - case Left(_) => None - case Right(r) => Some(r) - } - } - - implicit def sqllikeToRegex(value: String): Regex = toRegex(value).r - -} diff --git a/elastic/src/main/scala/app/softnetwork/elastic/sql/SQLParser.scala b/elastic/src/main/scala/app/softnetwork/elastic/sql/SQLParser.scala deleted file mode 100644 index 73716a81..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/sql/SQLParser.scala +++ /dev/null @@ -1,303 +0,0 @@ -package app.softnetwork.elastic.sql - -import scala.util.parsing.combinator.RegexParsers - -/** Created by smanciot on 27/06/2018. - */ -object SQLParser extends RegexParsers { - - val regexAlias = """\$?[a-zA-Z0-9_]*""" - - val regexRef = """\$[a-zA-Z0-9_]*""" - - def identifier: Parser[SQLIdentifier] = - "(?i)distinct".r.? ~ (regexRef.r ~ ".").? ~ """[\*a-zA-Z_\-][a-zA-Z0-9_\-\.\[\]]*""".r ^^ { - case d ~ a ~ str => - SQLIdentifier( - str, - a match { - case Some(x) => Some(x._1) - case _ => None - }, - d - ) - } - - def literal: Parser[SQLLiteral] = - """"[^"]*"""".r ^^ (str => SQLLiteral(str.substring(1, str.length - 1))) - - def int: Parser[SQLInt] = """(-)?(0|[1-9]\d*)""".r ^^ (str => SQLInt(str.toInt)) - - def double: Parser[SQLDouble] = """(-)?(\d+\.\d+)""".r ^^ (str => SQLDouble(str.toDouble)) - - def boolean: Parser[SQLBoolean] = """(true|false)""".r ^^ (bool => SQLBoolean(bool.toBoolean)) - - def eq: Parser[SQLExpressionOperator] = "=" ^^ (_ => EQ) - def ge: Parser[SQLExpressionOperator] = ">=" ^^ (_ => GE) - def gt: Parser[SQLExpressionOperator] = ">" ^^ (_ => GT) - def in: Parser[SQLExpressionOperator] = "(?i)in".r ^^ (_ => IN) - def le: Parser[SQLExpressionOperator] = "<=" ^^ (_ => LE) - def like: Parser[SQLExpressionOperator] = "(?i)like".r ^^ (_ => LIKE) - def lt: Parser[SQLExpressionOperator] = "<" ^^ (_ => LT) - def ne: Parser[SQLExpressionOperator] = "<>" ^^ (_ => NE) - - def isNull: Parser[SQLExpressionOperator] = "(?i)(is null)".r ^^ (_ => IS_NULL) - def isNullExpression: Parser[SQLCriteria] = identifier ~ isNull ^^ { case i ~ _ => SQLIsNull(i) } - - def isNotNull: Parser[SQLExpressionOperator] = "(?i)(is not null)".r ^^ (_ => IS_NOT_NULL) - def isNotNullExpression: Parser[SQLCriteria] = identifier ~ isNotNull ^^ { case i ~ _ => - SQLIsNotNull(i) - } - - def equalityExpression: Parser[SQLExpression] = - identifier ~ (eq | ne) ~ (boolean | literal | double | int) ^^ { case i ~ o ~ v => - SQLExpression(i, o, v) - } - def likeExpression: Parser[SQLExpression] = identifier ~ like ~ literal ^^ { case i ~ o ~ v => - SQLExpression(i, o, v) - } - def comparisonExpression: Parser[SQLExpression] = - identifier ~ (ge | gt | le | lt) ~ (double | int | literal) ^^ { case i ~ o ~ v => - SQLExpression(i, o, v) - } - - def inLiteralExpression: Parser[SQLCriteria] = - identifier ~ not.? ~ in ~ start ~ rep1(literal ~ separator.?) ~ end ^^ { - case i ~ n ~ _ ~ _ ~ v ~ _ => SQLIn(i, SQLLiteralValues(v map { _._1 }), n) - } - def inNumericalExpression: Parser[SQLCriteria] = - identifier ~ not.? ~ in ~ start ~ rep1((double | int) ~ separator.?) ~ end ^^ { - case i ~ n ~ _ ~ _ ~ v ~ _ => SQLIn(i, SQLNumericValues(v map { _._1 }), n) - } - - def between: Parser[SQLExpressionOperator] = "(?i)between".r ^^ (_ => BETWEEN) - def betweenExpression: Parser[SQLCriteria] = identifier ~ between ~ literal ~ and ~ literal ^^ { - case i ~ _ ~ from ~ _ ~ to => SQLBetween(i, from, to) - } - - def distance: Parser[SQLFunction] = "(?i)distance".r ^^ (_ => SQLDistance) - def distanceExpression: Parser[SQLCriteria] = - distance ~ start ~ identifier ~ separator ~ start ~ double ~ separator ~ double ~ end ~ end ~ le ~ literal ^^ { - case _ ~ _ ~ i ~ _ ~ _ ~ lat ~ _ ~ lon ~ _ ~ _ ~ _ ~ d => ElasticGeoDistance(i, d, lat, lon) - } - - def start: Parser[SQLDelimiter] = "(" ^^ (_ => StartPredicate) - def end: Parser[SQLDelimiter] = ")" ^^ (_ => EndPredicate) - def separator: Parser[SQLDelimiter] = "," ^^ (_ => Separator) - - def and: Parser[SQLPredicateOperator] = "(?i)and".r ^^ (_ => AND) - def or: Parser[SQLPredicateOperator] = "(?i)or".r ^^ (_ => OR) - def not: Parser[NOT.type] = "(?i)not".r ^^ (_ => NOT) - - def nested: Parser[ElasticOperator] = "(?i)nested".r ^^ (_ => NESTED) - def child: Parser[ElasticOperator] = "(?i)child".r ^^ (_ => CHILD) - def parent: Parser[ElasticOperator] = "(?i)parent".r ^^ (_ => PARENT) - - def criteria: Parser[SQLCriteria] = - start.? ~ (equalityExpression | likeExpression | comparisonExpression | inLiteralExpression | inNumericalExpression | betweenExpression | isNotNullExpression | isNullExpression | distanceExpression) ~ end.? ^^ { - case _ ~ c ~ _ => - c match { - case x: SQLExpression if x.columnName.nested => ElasticNested(x) - case y: SQLIn[_, _] if y.columnName.nested => ElasticNested(y) - case z: SQLBetween if z.columnName.nested => ElasticNested(z) - case n: SQLIsNull if n.columnName.nested => ElasticNested(n) - case nn: SQLIsNotNull if nn.columnName.nested => ElasticNested(nn) - case _ => c - } - } - - @scala.annotation.tailrec - private def unwrappNested(nested: ElasticNested): SQLCriteria = { - val c = nested.criteria - c match { - case x: ElasticNested => unwrappNested(x) - case _ => c - } - } - - private def unwrappCriteria(criteria: SQLCriteria): SQLCriteria = { - criteria match { - case x: ElasticNested => unwrappNested(x) - case _ => criteria - } - } - - private def unwrappPredicate(predicate: SQLPredicate): SQLPredicate = { - var unwrapp = false - val _left = predicate.leftCriteria match { - case x: ElasticNested => - unwrapp = true - unwrappNested(x) - case l => l - } - val _right = predicate.rightCriteria match { - case x: ElasticNested => - unwrapp = true - unwrappNested(x) - case r => r - } - if (unwrapp) - SQLPredicate(_left, predicate.operator, _right) - else - predicate - } - - def predicate: Parser[SQLPredicate] = criteria ~ (and | or) ~ not.? ~ criteria ^^ { - case l ~ o ~ n ~ r => SQLPredicate(l, o, r, n) - } - - def nestedCriteria: Parser[ElasticRelation] = nested ~ start.? ~ criteria ~ end.? ^^ { - case _ ~ _ ~ c ~ _ => ElasticNested(unwrappCriteria(c)) - } - def nestedPredicate: Parser[ElasticRelation] = nested ~ start ~ predicate ~ end ^^ { - case _ ~ _ ~ p ~ _ => ElasticNested(unwrappPredicate(p)) - } - - def childCriteria: Parser[ElasticRelation] = child ~ start.? ~ criteria ~ end.? ^^ { - case _ ~ _ ~ c ~ _ => ElasticChild(unwrappCriteria(c)) - } - def childPredicate: Parser[ElasticRelation] = child ~ start ~ predicate ~ end ^^ { - case _ ~ _ ~ p ~ _ => ElasticChild(unwrappPredicate(p)) - } - - def parentCriteria: Parser[ElasticRelation] = parent ~ start.? ~ criteria ~ end.? ^^ { - case _ ~ _ ~ c ~ _ => ElasticParent(unwrappCriteria(c)) - } - def parentPredicate: Parser[ElasticRelation] = parent ~ start ~ predicate ~ end ^^ { - case _ ~ _ ~ p ~ _ => ElasticParent(unwrappPredicate(p)) - } - - def alias: Parser[SQLAlias] = "(?i)as".r ~ regexAlias.r ^^ { case _ ~ b => SQLAlias(b) } - - def count: Parser[SQLFunction] = "(?i)count".r ^^ (_ => SQLCount) - def min: Parser[SQLFunction] = "(?i)min".r ^^ (_ => SQLMin) - def max: Parser[SQLFunction] = "(?i)max".r ^^ (_ => SQLMax) - def avg: Parser[SQLFunction] = "(?i)avg".r ^^ (_ => SQLAvg) - def sum: Parser[SQLFunction] = "(?i)sum".r ^^ (_ => SQLSum) - - def _select: Parser[SELECT.type] = "(?i)select".r ^^ (_ => SELECT) - - def _filter: Parser[FILTER.type] = "(?i)filter".r ^^ (_ => FILTER) - - def _from: Parser[FROM.type] = "(?i)from".r ^^ (_ => FROM) - - def _where: Parser[WHERE.type] = "(?i)where".r ^^ (_ => WHERE) - - def _limit: Parser[LIMIT.type] = "(?i)limit".r ^^ (_ => LIMIT) - - def countFilter: Parser[SQLFilter] = _filter ~> "[" ~> whereCriteria <~ "]" ^^ { case rawTokens => - SQLFilter( - processTokens(rawTokens, None, None, None) match { - case Some(c) => Some(unwrappCriteria(c)) - case _ => None - } - ) - } - - def countField: Parser[SQLCountField] = - count ~ start ~ identifier ~ end ~ alias.? ~ countFilter.? ^^ { case _ ~ _ ~ i ~ _ ~ a ~ f => - new SQLCountField(i, a, f) - } - - def field: Parser[SQLField] = - (min | max | avg | sum).? ~ start.? ~ identifier ~ end.? ~ alias.? ^^ { - case f ~ _ ~ i ~ _ ~ a => SQLField(f, i, a) - } - - def selectCount: Parser[SQLSelect] = _select ~ rep1sep(countField, separator) ^^ { - case _ ~ fields => new SQLSelectCount(fields) - } - - def select: Parser[SQLSelect] = _select ~ rep1sep(field, separator) ^^ { case _ ~ fields => - SQLSelect(fields) - } - - def table: Parser[SQLTable] = identifier ~ alias.? ^^ { case i ~ a => SQLTable(i, a) } - - def from: Parser[SQLFrom] = _from ~ rep1sep(table, separator) ^^ { case _ ~ tables => - SQLFrom(tables) - } - - def allPredicate: SQLParser.Parser[SQLCriteria] = - nestedPredicate | childPredicate | parentPredicate | predicate - - def allCriteria: SQLParser.Parser[SQLCriteria] = - nestedCriteria | childCriteria | parentCriteria | criteria - - def whereCriteria: SQLParser.Parser[List[SQLToken]] = rep1( - allPredicate | allCriteria | start | or | and | end - ) - - def where: Parser[SQLWhere] = _where ~ whereCriteria ^^ { case _ ~ rawTokens => - SQLWhere(processTokens(rawTokens, None, None, None)) - } - - def limit: SQLParser.Parser[SQLLimit] = _limit ~ int ^^ { case _ ~ i => SQLLimit(i.value) } - - def tokens: Parser[_ <: SQLSelectQuery] = { - phrase((selectCount | select) ~ from ~ where.? ~ limit.?) ^^ { case s ~ f ~ w ~ l => - s match { - case x: SQLSelectCount => new SQLCountQuery(x, f, w, l) - case _ => SQLSelectQuery(s, f, w, l) - } - } - } - - def apply(query: String): Either[SQLParserError, SQLSelectQuery] = { - parse(tokens, query) match { - case NoSuccess(msg, _) => - println(msg) - Left(SQLParserError(msg)) - case Success(result, _) => Right(result) - } - } - - @scala.annotation.tailrec - private def processTokens( - tokens: List[SQLToken], - left: Option[SQLCriteria], - operator: Option[SQLPredicateOperator], - right: Option[SQLCriteria] - ): Option[SQLCriteria] = { - tokens.headOption match { - case Some(c: SQLCriteria) if left.isEmpty => - processTokens(tokens.tail, Some(c), operator, right) - - case Some(c: SQLCriteria) if left.isDefined && operator.isDefined && right.isEmpty => - processTokens(tokens.tail, left, operator, Some(c)) - - case Some(_: StartDelimiter) => processTokens(tokens.tail, left, operator, right) - - case Some(_: EndDelimiter) if left.isDefined && operator.isDefined && right.isDefined => - processTokens( - tokens.tail, - Some(SQLPredicate(left.get, operator.get, right.get)), - None, - None - ) - - case Some(_: EndDelimiter) => processTokens(tokens.tail, left, operator, right) - - case Some(o: SQLPredicateOperator) if operator.isEmpty => - processTokens(tokens.tail, left, Some(o), right) - - case Some(o: SQLPredicateOperator) - if left.isDefined && operator.isDefined && right.isDefined => - processTokens( - tokens.tail, - Some(SQLPredicate(left.get, operator.get, right.get)), - Some(o), - None - ) - - case None if left.isDefined && operator.isDefined && right.isDefined => - Some(SQLPredicate(left.get, operator.get, right.get)) - - case None => left - - } - } -} - -trait SQLCompilationError -case class SQLParserError(msg: String) extends SQLCompilationError diff --git a/elastic/src/main/scala/app/softnetwork/elastic/sql/package.scala b/elastic/src/main/scala/app/softnetwork/elastic/sql/package.scala deleted file mode 100644 index 20fbbf83..00000000 --- a/elastic/src/main/scala/app/softnetwork/elastic/sql/package.scala +++ /dev/null @@ -1,406 +0,0 @@ -package app.softnetwork.elastic - -import java.util.regex.Pattern - -import scala.reflect.runtime.universe._ - -import scala.util.Try - -/** Created by smanciot on 27/06/2018. - */ -package object sql { - - import scala.language.implicitConversions - - implicit def asString(token: Option[_ <: SQLToken]): String = token match { - case Some(t) => t.sql - case _ => "" - } - - sealed trait SQLToken extends Serializable { - def sql: String - override def toString: String = sql - } - - abstract class SQLExpr(override val sql: String) extends SQLToken - - case object SELECT extends SQLExpr("select") - case object FILTER extends SQLExpr("filter") - case object FROM extends SQLExpr("from") - case object WHERE extends SQLExpr("where") - case object LIMIT extends SQLExpr("limit") - - case class SQLLimit(limit: Int) extends SQLExpr(s"limit $limit") - - case class SQLIdentifier( - identifier: String, - alias: Option[String] = None, - distinct: Option[String] = None - ) extends SQLExpr( - if (alias.isDefined) - s"${distinct.getOrElse("")} ${alias.get}.$identifier".trim - else - s"${distinct.getOrElse("")} $identifier".trim - ) - with SQLSource { - lazy val nested: Boolean = identifier.contains('.') && !identifier.endsWith(".raw") - } - - abstract class SQLValue[+T](val value: T)(implicit ev$1: T => Ordered[T]) extends SQLToken { - def choose[R >: T]( - values: Seq[R], - operator: Option[SQLExpressionOperator], - separator: String = "|" - )(implicit ev: R => Ordered[R]): Option[R] = { - if (values.isEmpty) - None - else - operator match { - case Some(_: EQ.type) => values.find(_ == value) - case Some(_: NE.type) => values.find(_ != value) - case Some(_: GE.type) => values.filter(_ >= value).sorted.reverse.headOption - case Some(_: GT.type) => values.filter(_ > value).sorted.reverse.headOption - case Some(_: LE.type) => values.filter(_ <= value).sorted.headOption - case Some(_: LT.type) => values.filter(_ < value).sorted.headOption - case _ => values.headOption - } - } - } - - case class SQLBoolean(value: Boolean) extends SQLToken { - override def sql: String = s"$value" - } - - case class SQLLiteral(override val value: String) extends SQLValue[String](value) { - override def sql: String = s""""$value"""" - import SQLImplicits._ - private lazy val pattern: Pattern = value.pattern - def like: Seq[String] => Boolean = { - _.exists { pattern.matcher(_).matches() } - } - def eq: Seq[String] => Boolean = { - _.exists { _.contentEquals(value) } - } - def ne: Seq[String] => Boolean = { - _.forall { !_.contentEquals(value) } - } - override def choose[R >: String]( - values: Seq[R], - operator: Option[SQLExpressionOperator], - separator: String = "|" - )(implicit ev: R => Ordered[R]): Option[R] = { - operator match { - case Some(_: EQ.type) => values.find(v => v.toString contentEquals value) - case Some(_: NE.type) => values.find(v => !(v.toString contentEquals value)) - case Some(_: LIKE.type) => values.find(v => pattern.matcher(v.toString).matches()) - case None => Some(values.mkString(separator)) - case _ => super.choose(values, operator, separator) - } - } - } - - abstract class SQLNumeric[+T](override val value: T)(implicit ev$1: T => Ordered[T]) - extends SQLValue[T](value) { - override def sql: String = s"$value" - override def choose[R >: T]( - values: Seq[R], - operator: Option[SQLExpressionOperator], - separator: String = "|" - )(implicit ev: R => Ordered[R]): Option[R] = { - operator match { - case None => if (values.isEmpty) None else Some(values.max) - case _ => super.choose(values, operator, separator) - } - } - } - - case class SQLInt(override val value: Int) extends SQLNumeric[Int](value) { - def max: Seq[Int] => Int = x => Try(x.max).getOrElse(0) - def min: Seq[Int] => Int = x => Try(x.min).getOrElse(0) - def eq: Seq[Int] => Boolean = { - _.exists { _ == value } - } - def ne: Seq[Int] => Boolean = { - _.forall { _ != value } - } - } - - case class SQLDouble(override val value: Double) extends SQLNumeric[Double](value) { - def max: Seq[Double] => Double = x => Try(x.max).getOrElse(0) - def min: Seq[Double] => Double = x => Try(x.min).getOrElse(0) - def eq: Seq[Double] => Boolean = { - _.exists { _ == value } - } - def ne: Seq[Double] => Boolean = { - _.forall { _ != value } - } - } - - sealed abstract class SQLValues[+R: TypeTag, +T <: SQLValue[R]](val values: Seq[T]) - extends SQLToken { - override def sql = s"(${values.map(_.sql).mkString(",")})" - lazy val innerValues: Seq[R] = values.map(_.value) - } - - case class SQLLiteralValues(override val values: Seq[SQLLiteral]) - extends SQLValues[String, SQLValue[String]](values) { - def eq: Seq[String] => Boolean = { - _.exists { s => innerValues.exists(_.contentEquals(s)) } - } - def ne: Seq[String] => Boolean = { - _.forall { s => innerValues.forall(!_.contentEquals(s)) } - } - } - - case class SQLNumericValues[R: TypeTag](override val values: Seq[SQLNumeric[R]]) - extends SQLValues[R, SQLNumeric[R]](values) { - def eq: Seq[R] => Boolean = { - _.exists { n => innerValues.contains(n) } - } - def ne: Seq[R] => Boolean = { - _.forall { n => !innerValues.contains(n) } - } - } - - sealed trait SQLOperator extends SQLToken - - sealed trait SQLExpressionOperator extends SQLOperator - - case object EQ extends SQLExpr("=") with SQLExpressionOperator - case object GE extends SQLExpr(">=") with SQLExpressionOperator - case object GT extends SQLExpr(">") with SQLExpressionOperator - case object IN extends SQLExpr("in") with SQLExpressionOperator - case object LE extends SQLExpr("<=") with SQLExpressionOperator - case object LIKE extends SQLExpr("like") with SQLExpressionOperator - case object LT extends SQLExpr("<") with SQLExpressionOperator - case object NE extends SQLExpr("<>") with SQLExpressionOperator - case object BETWEEN extends SQLExpr("between") with SQLExpressionOperator - case object IS_NULL extends SQLExpr("is null") with SQLExpressionOperator - case object IS_NOT_NULL extends SQLExpr("is not null") with SQLExpressionOperator - - sealed trait SQLPredicateOperator extends SQLOperator - - case object AND extends SQLPredicateOperator { override val sql: String = "and" } - case object OR extends SQLPredicateOperator { override val sql: String = "or" } - case object NOT extends SQLPredicateOperator { override val sql: String = "not" } - - sealed trait SQLCriteria extends SQLToken { - def operator: SQLOperator - } - - case class SQLExpression( - columnName: SQLIdentifier, - operator: SQLExpressionOperator, - value: SQLToken - ) extends SQLCriteria { - override def sql = s"$columnName ${operator.sql} $value" - } - - case class SQLIsNull(columnName: SQLIdentifier) extends SQLCriteria { - override val operator: SQLOperator = IS_NULL - override def sql = s"$columnName ${operator.sql}" - } - - case class SQLIsNotNull(columnName: SQLIdentifier) extends SQLCriteria { - override val operator: SQLOperator = IS_NOT_NULL - override def sql = s"$columnName ${operator.sql}" - } - - case class SQLIn[R, +T <: SQLValue[R]]( - columnName: SQLIdentifier, - values: SQLValues[R, T], - not: Option[NOT.type] = None - ) extends SQLCriteria { - override def sql = - s"$columnName ${not.map(_ => "not ").getOrElse("")}${operator.sql} ${values.sql}" - override def operator: SQLOperator = IN - } - - case class SQLBetween(columnName: SQLIdentifier, from: SQLLiteral, to: SQLLiteral) - extends SQLCriteria { - override def sql = s"$columnName ${operator.sql} ${from.sql} and ${to.sql}" - override def operator: SQLOperator = BETWEEN - } - - case class ElasticGeoDistance( - columnName: SQLIdentifier, - distance: SQLLiteral, - lat: SQLDouble, - lon: SQLDouble - ) extends SQLCriteria { - override def sql = s"${operator.sql}($columnName,(${lat.sql},${lon.sql})) <= ${distance.sql}" - override def operator: SQLOperator = SQLDistance - } - - case class SQLPredicate( - leftCriteria: SQLCriteria, - operator: SQLPredicateOperator, - rightCriteria: SQLCriteria, - not: Option[NOT.type] = None - ) extends SQLCriteria { - val leftParentheses: Boolean = leftCriteria match { - case _: ElasticRelation => false - case _ => true - } - val rightParentheses: Boolean = rightCriteria match { - case _: ElasticRelation => false - case _ => true - } - override def sql = s"${if (leftParentheses) s"(${leftCriteria.sql})" - else leftCriteria.sql} ${operator.sql}${not - .map(_ => " not") - .getOrElse("")} ${if (rightParentheses) s"(${rightCriteria.sql})" else rightCriteria.sql}" - } - - sealed trait ElasticOperator extends SQLOperator - case object NESTED extends SQLExpr("nested") with ElasticOperator - case object CHILD extends SQLExpr("child") with ElasticOperator - case object PARENT extends SQLExpr("parent") with ElasticOperator - - sealed abstract class ElasticRelation(val criteria: SQLCriteria, val operator: ElasticOperator) - extends SQLCriteria { - override def sql = s"${operator.sql}(${criteria.sql})" - def _retrieveType(criteria: SQLCriteria): Option[String] = criteria match { - case SQLPredicate(left, _, _, _) => _retrieveType(left) - case SQLBetween(col, _, _) => Some(col.identifier.split("\\.").head) - case SQLExpression(col, _, _) => Some(col.identifier.split("\\.").head) - case SQLIn(col, _, _) => Some(col.identifier.split("\\.").head) - case SQLIsNull(col) => Some(col.identifier.split("\\.").head) - case SQLIsNotNull(col) => Some(col.identifier.split("\\.").head) - case relation: ElasticRelation => relation.`type` - case _ => None - } - lazy val `type`: Option[String] = _retrieveType(criteria) - } - - case class ElasticNested(override val criteria: SQLCriteria) - extends ElasticRelation(criteria, NESTED) - - case class ElasticChild(override val criteria: SQLCriteria) - extends ElasticRelation(criteria, CHILD) - - case class ElasticParent(override val criteria: SQLCriteria) - extends ElasticRelation(criteria, PARENT) - - sealed trait SQLDelimiter extends SQLToken - trait StartDelimiter extends SQLDelimiter - trait EndDelimiter extends SQLDelimiter - case object StartPredicate extends SQLExpr("(") with StartDelimiter - case object EndPredicate extends SQLExpr(")") with EndDelimiter - case object Separator extends SQLExpr(",") with EndDelimiter - - def choose[T]( - values: Seq[T], - criteria: Option[SQLCriteria], - function: Option[SQLFunction] = None - )(implicit ev$1: T => Ordered[T]): Option[T] = { - criteria match { - case Some(SQLExpression(_, operator, value: SQLValue[T] @unchecked)) => - value.choose[T](values, Some(operator)) - case _ => - function match { - case Some(_: SQLMin.type) => Some(values.min) - case Some(_: SQLMax.type) => Some(values.max) - // FIXME case Some(_: SQLSum.type) => Some(values.sum) - // FIXME case Some(_: SQLAvg.type) => Some(values.sum / values.length ) - case _ => values.headOption - } - } - } - - def toRegex(value: String): String = { - val startWith = value.startsWith("%") - val endWith = value.endsWith("%") - val v = - if (startWith && endWith) - value.substring(1, value.length - 1) - else if (startWith) - value.substring(1) - else if (endWith) - value.substring(0, value.length - 1) - else - value - s"""${if (startWith) ".*?"}$v${if (endWith) ".*?"}""" - } - - case class SQLAlias(alias: String) extends SQLExpr(s" as $alias") - - sealed trait SQLFunction extends SQLToken - case object SQLCount extends SQLExpr("count") with SQLFunction - case object SQLMin extends SQLExpr("min") with SQLFunction - case object SQLMax extends SQLExpr("max") with SQLFunction - case object SQLAvg extends SQLExpr("avg") with SQLFunction - case object SQLSum extends SQLExpr("sum") with SQLFunction - case object SQLDistance extends SQLExpr("distance") with SQLFunction with SQLOperator - - case class SQLField( - func: Option[SQLFunction] = None, - identifier: SQLIdentifier, - alias: Option[SQLAlias] = None - ) extends SQLToken { - override def sql: String = - func match { - case Some(f) => s"${f.sql}(${identifier.sql})${asString(alias)}" - case _ => s"${identifier.sql}${asString(alias)}" - } - } - - class SQLCountField( - override val identifier: SQLIdentifier, - override val alias: Option[SQLAlias] = None, - val filter: Option[SQLFilter] = None - ) extends SQLField(Some(SQLCount), identifier, alias) - - case class SQLSelect(fields: Seq[SQLField] = Seq(SQLField(identifier = SQLIdentifier("*")))) - extends SQLToken { - override def sql: String = s"$SELECT ${fields.map(_.sql).mkString(",")}" - } - - class SQLSelectCount( - val countFields: Seq[SQLCountField] = Seq(new SQLCountField(identifier = SQLIdentifier("*"))) - ) extends SQLSelect(countFields) - - sealed trait SQLSource extends SQLToken - - case class SQLTable(source: SQLSource, alias: Option[SQLAlias] = None) extends SQLToken { - override def sql: String = s"$source${asString(alias)}" - } - - case class SQLFrom(tables: Seq[SQLTable]) extends SQLToken { - override def sql: String = s" $FROM ${tables.map(_.sql).mkString(",")}" - } - - case class SQLWhere(criteria: Option[SQLCriteria]) extends SQLToken { - override def sql: String = criteria match { - case Some(c) => s" $WHERE ${c.sql}" - case _ => "" - } - } - - case class SQLFilter(criteria: Option[SQLCriteria]) extends SQLToken { - override def sql: String = criteria match { - case Some(c) => s" $FILTER($c)" - case _ => "" - } - } - - case class SQLSelectQuery( - select: SQLSelect = SQLSelect(), - from: SQLFrom, - where: Option[SQLWhere], - limit: Option[SQLLimit] = None - ) extends SQLToken { - override def sql: String = s"${select.sql}${from.sql}${asString(where)}${asString(limit)}" - } - - class SQLCountQuery( - val selectCount: SQLSelectCount = new SQLSelectCount(), - from: SQLFrom, - where: Option[SQLWhere], - limit: Option[SQLLimit] = None - ) extends SQLSelectQuery(selectCount, from, where) - - case class SQLQuery(query: String) - - case class SQLQueries(queries: List[SQLQuery]) -} diff --git a/elastic/src/test/scala/app/softnetwork/elastic/sql/ElasticFiltersSpec.scala b/elastic/src/test/scala/app/softnetwork/elastic/sql/ElasticFiltersSpec.scala deleted file mode 100644 index 0283aed3..00000000 --- a/elastic/src/test/scala/app/softnetwork/elastic/sql/ElasticFiltersSpec.scala +++ /dev/null @@ -1,756 +0,0 @@ -package app.softnetwork.elastic.sql - -import com.sksamuel.elastic4s.http.search.SearchBodyBuilderFn -import com.sksamuel.elastic4s.searches.SearchRequest -import com.sksamuel.elastic4s.searches.queries.Query -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -/** Created by smanciot on 13/04/17. - */ -class ElasticFiltersSpec extends AnyFlatSpec with Matchers { - - import Queries._ - - import scala.language.implicitConversions - - def query2String(result: Query): String = { - SearchBodyBuilderFn(SearchRequest("*") query result).string() - } - - "ElasticFilters" should "filter numerical eq" in { - val result = ElasticFilters.filter(numericalEq) - query2String(result) shouldBe """{ - - |"query":{ - | "term" : { - | "identifier" : { - | "value" : "1.0" - | } - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter numerical ne" in { - val result = ElasticFilters.filter(numericalNe) - query2String(result) shouldBe """{ - - |"query":{ - | "bool":{ - | "must_not":[ - | { - | "term":{ - | "identifier":{ - | "value":"1" - | } - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter numerical lt" in { - val result = ElasticFilters.filter(numericalLt) - query2String(result) shouldBe """{ - - |"query":{ - | "range" : { - | "identifier" : { - | "lt" : "1" - | } - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter numerical le" in { - val result = ElasticFilters.filter(numericalLe) - query2String(result) shouldBe """{ - - |"query":{ - | "range" : { - | "identifier" : { - | "lte" : "1" - | } - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter numerical gt" in { - val result = ElasticFilters.filter(numericalGt) - query2String(result) shouldBe """{ - - |"query":{ - | "range" : { - | "identifier" : { - | "gt" : "1" - | } - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter numerical ge" in { - val result = ElasticFilters.filter(numericalGe) - query2String(result) shouldBe """{ - - |"query":{ - | "range" : { - | "identifier" : { - | "gte" : "1" - | } - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter literal eq" in { - val result = ElasticFilters.filter(literalEq) - query2String(result) shouldBe """{ - - |"query":{ - | "term" : { - | "identifier" : { - | "value" : "un" - | } - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter literal ne" in { - val result = ElasticFilters.filter(literalNe) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "must_not" : [ - | { - | "term" : { - | "identifier" : { - | "value" : "un" - | } - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter literal like" in { - val result = ElasticFilters.filter(literalLike) - query2String(result) shouldBe """{ - - |"query":{ - | "regexp" : { - | "identifier" : { - | "value" : ".*?un.*?" - | } - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter between" in { - val result = ElasticFilters.filter(betweenExpression) - query2String(result) shouldBe """{ - - |"query":{ - | "range" : { - | "identifier" : { - | "gte" : "1", - | "lte" : "2" - | } - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter and predicate" in { - val result = ElasticFilters.filter(andPredicate) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "filter" : [ - | { - | "term" : { - | "identifier1" : { - | "value" : "1" - | } - | } - | }, - | { - | "range" : { - | "identifier2" : { - | "gt" : "2" - | } - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter or predicate" in { - val result = ElasticFilters.filter(orPredicate) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "should" : [ - | { - | "term" : { - | "identifier1" : { - | "value" : "1" - | } - | } - | }, - | { - | "range" : { - | "identifier2" : { - | "gt" : "2" - | } - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter left predicate with criteria" in { - val result = ElasticFilters.filter(leftPredicate) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "should" : [ - | { - | "bool" : { - | "filter" : [ - | { - | "term" : { - | "identifier1" : { - | "value" : "1" - | } - | } - | }, - | { - | "range" : { - | "identifier2" : { - | "gt" : "2" - | } - | } - | } - | ] - | } - | }, - | { - | "term" : { - | "identifier3" : { - | "value" : "3" - | } - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter right predicate with criteria" in { - val result = ElasticFilters.filter(rightPredicate) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "filter" : [ - | { - | "term" : { - | "identifier1" : { - | "value" : "1" - | } - | } - | }, - | { - | "bool" : { - | "should" : [ - | { - | "range" : { - | "identifier2" : { - | "gt" : "2" - | } - | } - | }, - | { - | "term" : { - | "identifier3" : { - | "value" : "3" - | } - | } - | } - | ] - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter multiple predicates" in { - val result = ElasticFilters.filter(predicates) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "should" : [ - | { - | "bool" : { - | "filter" : [ - | { - | "term" : { - | "identifier1" : { - | "value" : "1" - | } - | } - | }, - | { - | "range" : { - | "identifier2" : { - | "gt" : "2" - | } - | } - | } - | ] - | } - | }, - | { - | "bool" : { - | "filter" : [ - | { - | "term" : { - | "identifier3" : { - | "value" : "3" - | } - | } - | }, - | { - | "term" : { - | "identifier4" : { - | "value" : "4" - | } - | } - | } - | ] - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter in literal expression" in { - val result = ElasticFilters.filter(inLiteralExpression) - query2String(result) shouldBe """{ - - |"query":{ - | "terms" : { - | "identifier" : [ - | "val1", - | "val2", - | "val3" - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter in numerical expression with Int values" in { - val result = ElasticFilters.filter(inNumericalExpressionWithIntValues) - query2String(result) shouldBe """{ - - |"query":{ - | "terms" : { - | "identifier" : [ - | 1, - | 2, - | 3 - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter in numerical expression with Double values" in { - val result = ElasticFilters.filter(inNumericalExpressionWithDoubleValues) - query2String(result) shouldBe """{ - - |"query":{ - | "terms" : { - | "identifier" : [ - | 1.0, - | 2.1, - | 3.4 - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter nested predicate" in { - val result = ElasticFilters.filter(nestedPredicate) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "filter" : [ - | { - | "term" : { - | "identifier1" : { - | "value" : "1" - | } - | } - | }, - | { - | "nested" : { - | "path" : "nested", - | "query" : { - | "bool" : { - | "should" : [ - | { - | "range" : { - | "nested.identifier2" : { - | "gt" : "2" - | } - | } - | }, - | { - | "term" : { - | "nested.identifier3" : { - | "value" : "3" - | } - | } - | } - | ] - | } - | }, - | "inner_hits":{"name":"nested"} - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter nested criteria" in { - val result = ElasticFilters.filter(nestedCriteria) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "filter" : [ - | { - | "term" : { - | "identifier1" : { - | "value" : "1" - | } - | } - | }, - | { - | "nested" : { - | "path" : "nested", - | "query" : { - | "term" : { - | "nested.identifier3" : { - | "value" : "3" - | } - | } - | }, - | "inner_hits":{"name":"nested"} - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter child predicate" in { - val result = ElasticFilters.filter(childPredicate) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "filter" : [ - | { - | "term" : { - | "identifier1" : { - | "value" : "1" - | } - | } - | }, - | { - | "has_child" : { - | "type" : "child", - | "score_mode" : "none", - | "query" : { - | "bool" : { - | "should" : [ - | { - | "range" : { - | "child.identifier2" : { - | "gt" : "2" - | } - | } - | }, - | { - | "term" : { - | "child.identifier3" : { - | "value" : "3" - | } - | } - | } - | ] - | } - | } - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter child criteria" in { - val result = ElasticFilters.filter(childCriteria) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "filter" : [ - | { - | "term" : { - | "identifier1" : { - | "value" : "1" - | } - | } - | }, - | { - | "has_child" : { - | "type" : "child", - | "score_mode" : "none", - | "query" : { - | "term" : { - | "child.identifier3" : { - | "value" : "3" - | } - | } - | } - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter parent predicate" in { - val result = ElasticFilters.filter(parentPredicate) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "filter" : [ - | { - | "term" : { - | "identifier1" : { - | "value" : "1" - | } - | } - | }, - | { - | "has_parent" : { - | "parent_type" : "parent", - | "query" : { - | "bool" : { - | "should" : [ - | { - | "range" : { - | "parent.identifier2" : { - | "gt" : "2" - | } - | } - | }, - | { - | "term" : { - | "parent.identifier3" : { - | "value" : "3" - | } - | } - | } - | ] - | } - | } - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter parent criteria" in { - val result = ElasticFilters.filter(parentCriteria) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "filter" : [ - | { - | "term" : { - | "identifier1" : { - | "value" : "1" - | } - | } - | }, - | { - | "has_parent" : { - | "parent_type" : "parent", - | "query" : { - | "term" : { - | "parent.identifier3" : { - | "value" : "3" - | } - | } - | } - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter nested with between" in { - val result = ElasticFilters.filter(nestedWithBetween) - query2String(result) shouldBe """{ - - |"query":{ - | "nested" : { - | "path" : "ciblage", - | "query" : { - | "bool" : { - | "filter" : [ - | { - | "range" : { - | "ciblage.Archivage_CreationDate" : { - | "gte" : "now-3M/M", - | "lte" : "now" - | } - | } - | }, - | { - | "term" : { - | "ciblage.statutComportement" : { - | "value" : "1" - | } - | } - | } - | ] - | } - | }, - | "inner_hits":{"name":"ciblage"} - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter boolean eq" in { - val result = ElasticFilters.filter(boolEq) - query2String(result) shouldBe """{ - - |"query":{ - | "term" : { - | "identifier" : { - | "value" : true - | } - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter boolean ne" in { - val result = ElasticFilters.filter(boolNe) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "must_not" : [ - | { - | "term" : { - | "identifier" : { - | "value" : false - | } - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter is null" in { - val result = ElasticFilters.filter(isNull) - query2String(result) shouldBe """{ - - |"query":{ - | "bool" : { - | "must_not" : [ - | { - | "exists" : { - | "field" : "identifier" - | } - | } - | ] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter is not null" in { - val result = ElasticFilters.filter(isNotNull) - query2String(result) shouldBe """{ - - |"query":{ - | "exists" : { - | "field" : "identifier" - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - - it should "filter geo distance criteria" in { - val result = ElasticFilters.filter(geoDistanceCriteria) - query2String(result) shouldBe - """{ - - |"query":{ - | "geo_distance" : { - | "distance":"5km", - | "profile.location":[40.0,-70.0] - | } - | } - |}""".stripMargin.replaceAll("\\s", "") - } - -} diff --git a/elastic/src/test/scala/app/softnetwork/elastic/sql/ElasticQuerySpec.scala b/elastic/src/test/scala/app/softnetwork/elastic/sql/ElasticQuerySpec.scala deleted file mode 100644 index 8ca199d1..00000000 --- a/elastic/src/test/scala/app/softnetwork/elastic/sql/ElasticQuerySpec.scala +++ /dev/null @@ -1,467 +0,0 @@ -package app.softnetwork.elastic.sql - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -/** Created by smanciot on 13/04/17. - */ -class ElasticQuerySpec extends AnyFlatSpec with Matchers { - - import scala.language.implicitConversions - - "ElasticQuery" should "perform native count" in { - val results = ElasticQuery.count( - SQLQuery("select count($t.id) as c2 from Table as t where $t.nom = \"Nom\"") - ) - results.size shouldBe 1 - val result = results.head - result.nested shouldBe false - result.distinct shouldBe false - result.agg shouldBe "agg_id" - result.field shouldBe "c2" - result.sources shouldBe Seq[String]("Table") - result.query shouldBe - """|{ - | "query": { - | "bool": { - | "filter": [ - | { - | "term": { - | "nom": { - | "value": "Nom" - | } - | } - | } - | ] - | } - | }, - | "size": 0, - | "aggs": { - | "agg_id": { - | "value_count": { - | "field": "id" - | } - | } - | } - |}""".stripMargin.replaceAll("\\s+", "") - } - - it should "perform count distinct" in { - val results = ElasticQuery.count( - SQLQuery("select count(distinct $t.id) as c2 from Table as t where $t.nom = \"Nom\"") - ) - results.size shouldBe 1 - val result = results.head - result.nested shouldBe false - result.distinct shouldBe true - result.agg shouldBe "agg_distinct_id" - result.field shouldBe "c2" - result.sources shouldBe Seq[String]("Table") - result.query shouldBe - """|{ - | "query": { - | "bool": { - | "filter": [ - | { - | "term": { - | "nom": { - | "value": "Nom" - | } - | } - | } - | ] - | } - | }, - | "size": 0, - | "aggs": { - | "agg_distinct_id": { - | "cardinality": { - | "field": "id" - | } - | } - | } - |}""".stripMargin.replaceAll("\\s+", "") - } - - it should "perform nested count" in { - val results = ElasticQuery.count( - SQLQuery("select count(email.value) as email from index where nom = \"Nom\"") - ) - results.size shouldBe 1 - val result = results.head - result.nested shouldBe true - result.distinct shouldBe false - result.agg shouldBe "nested_email.agg_email_value" - result.field shouldBe "email" - result.sources shouldBe Seq[String]("index") - result.query shouldBe - """{ - | "query": { - | "bool": { - | "filter": [ - | { - | "term": { - | "nom": { - | "value": "Nom" - | } - | } - | } - | ] - | } - | }, - | "size": 0, - | "aggs": { - | "nested_email": { - | "nested": { - | "path": "email" - | }, - | "aggs": { - | "agg_email_value": { - | "value_count": { - | "field": "email.value" - | } - | } - | } - | } - | } - |}""".stripMargin.replaceAll("\\s+", "") - } - - it should "perform nested count with nested criteria" in { - val results = ElasticQuery.count( - SQLQuery( - "select count(email.value) as email from index where nom = \"Nom\" and (profile.postalCode in (\"75001\",\"75002\"))" - ) - ) - results.size shouldBe 1 - val result = results.head - result.nested shouldBe true - result.distinct shouldBe false - result.agg shouldBe "nested_email.agg_email_value" - result.field shouldBe "email" - result.sources shouldBe Seq[String]("index") - result.query shouldBe - """{ - | "query": { - | "bool": { - | "filter": [ - | { - | "term": { - | "nom": { - | "value": "Nom" - | } - | } - | }, - | { - | "nested": { - | "path": "profile", - | "query": { - | "terms": { - | "profile.postalCode": [ - | "75001", - | "75002" - | ] - | } - | }, - | "inner_hits":{"name":"profile"} - | } - | } - | ] - | } - | }, - | "size": 0, - | "aggs": { - | "nested_email": { - | "nested": { - | "path": "email" - | }, - | "aggs": { - | "agg_email_value": { - | "value_count": { - | "field": "email.value" - | } - | } - | } - | } - | } - |}""".stripMargin.replaceAll("\\s+", "") - } - - it should "perform nested count with filter" in { - val results = ElasticQuery.count( - SQLQuery( - "select count(email.value) as email filter[email.context = \"profile\"] from index where nom = \"Nom\" and (profile.postalCode in (\"75001\",\"75002\"))" - ) - ) - results.size shouldBe 1 - val result = results.head - result.nested shouldBe true - result.distinct shouldBe false - result.agg shouldBe "nested_email.filtered_agg.agg_email_value" - result.field shouldBe "email" - result.sources shouldBe Seq[String]("index") - result.query shouldBe - """{ - | "query": { - | "bool": { - | "filter": [ - | { - | "term": { - | "nom": { - | "value": "Nom" - | } - | } - | }, - | { - | "nested": { - | "path": "profile", - | "query": { - | "terms": { - | "profile.postalCode": [ - | "75001", - | "75002" - | ] - | } - | }, - | "inner_hits":{"name":"profile"} - | } - | } - | ] - | } - | }, - | "size": 0, - | "aggs": { - | "nested_email": { - | "nested": { - | "path": "email" - | }, - | "aggs": { - | "filtered_agg": { - | "filter": { - | "term": { - | "email.context": { - | "value": "profile" - | } - | } - | }, - | "aggs": { - | "agg_email_value": { - | "value_count": { - | "field": "email.value" - | } - | } - | } - | } - | } - | } - | } - |}""".stripMargin.replaceAll("\\s+", "") - } - - it should "accept and not operator" in { - val results = ElasticQuery.count( - SQLQuery( - "select count(distinct email.value) as email from index where (profile.postalCode = \"33600\" and not profile.postalCode = \"75001\")" - ) - ) - results.size shouldBe 1 - val result = results.head - result.nested shouldBe true - result.distinct shouldBe true - result.agg shouldBe "nested_email.agg_distinct_email_value" - result.field shouldBe "email" - result.sources shouldBe Seq[String]("index") - result.query shouldBe - """{ - | "query": { - | "bool": { - | "must": [ - | { - | "nested": { - | "path": "profile", - | "query": { - | "term": { - | "profile.postalCode": { - | "value": "33600" - | } - | } - | }, - | "inner_hits":{"name":"profile"} - | } - | } - | ], - | "must_not": [ - | { - | "nested": { - | "path": "profile", - | "query": { - | "term": { - | "profile.postalCode": { - | "value": "75001" - | } - | } - | }, - | "inner_hits":{"name":"profile1"} - | } - | } - | ] - | } - | }, - | "size": 0, - | "aggs": { - | "nested_email": { - | "nested": { - | "path": "email" - | }, - | "aggs": { - | "agg_distinct_email_value": { - | "cardinality": { - | "field": "email.value" - | } - | } - | } - | } - | } - |} - |""".stripMargin.replaceAll("\\s+", "") - } - - it should "accept date filtering" in { - val results = ElasticQuery.count( - SQLQuery( - "select count(distinct email.value) as email from index where profile.postalCode = \"33600\" and profile.createdDate <= \"now-35M/M\"" - ) - ) - results.size shouldBe 1 - val result = results.head - result.nested shouldBe true - result.distinct shouldBe true - result.agg shouldBe "nested_email.agg_distinct_email_value" - result.field shouldBe "email" - result.sources shouldBe Seq[String]("index") - result.query shouldBe - """{ - | "query": { - | "bool": { - | "filter": [ - | { - | "nested": { - | "path": "profile", - | "query": { - | "term": { - | "profile.postalCode": { - | "value": "33600" - | } - | } - | }, - | "inner_hits":{"name":"profile"} - | } - | }, - | { - | "nested": { - | "path": "profile", - | "query": { - | "range": { - | "profile.createdDate": { - | "lte": "now-35M/M" - | } - | } - | }, - | "inner_hits":{"name":"profile1"} - | } - | } - | ] - | } - | }, - | "size": 0, - | "aggs": { - | "nested_email": { - | "nested": { - | "path": "email" - | }, - | "aggs": { - | "agg_distinct_email_value": { - | "cardinality": { - | "field": "email.value" - | } - | } - | } - | } - | } - |} - |""".stripMargin.replaceAll("\\s+", "") - } - - it should "perform select" in { - val select = ElasticQuery.select( - SQLQuery(""" - |SELECT - |profileId, - |profile_ccm.email as email, - |profile_ccm.city as city, - |profile_ccm.firstName as firstName, - |profile_ccm.lastName as lastName, - |profile_ccm.postalCode as postalCode, - |profile_ccm.birthYear as birthYear - |FROM index - |WHERE - |profile_ccm.postalCode BETWEEN "10" AND "99999" - |AND - |profile_ccm.birthYear <= 2000 - |limit 100""".stripMargin) - ) - select.isDefined shouldBe true - val result = select.get - result.query shouldBe - """{ - | "query": { - | "bool": { - | "filter": [ - | { - | "nested": { - | "path": "profile_ccm", - | "query": { - | "range": { - | "profile_ccm.postalCode": { - | "gte": "10", - | "lte": "99999" - | } - | } - | }, - | "inner_hits":{"name":"profile_ccm"} - | } - | }, - | { - | "nested": { - | "path": "profile_ccm", - | "query": { - | "range": { - | "profile_ccm.birthYear": { - | "lte": "2000" - | } - | } - | }, - | "inner_hits":{"name":"profile_ccm1"} - | } - | } - | ] - | } - | }, - | "from":0, - | "size":100, - | "_source": { - | "includes": [ - | "profileId", - | "profile_ccm.email", - | "profile_ccm.city", - | "profile_ccm.firstName", - | "profile_ccm.lastName", - | "profile_ccm.postalCode", - | "profile_ccm.birthYear" - | ] - | } - |} - |""".stripMargin.replaceAll("\\s+", "") - } - -} diff --git a/elastic/src/test/scala/app/softnetwork/elastic/sql/SQLLiteralSpec.scala b/elastic/src/test/scala/app/softnetwork/elastic/sql/SQLLiteralSpec.scala deleted file mode 100644 index 16b10632..00000000 --- a/elastic/src/test/scala/app/softnetwork/elastic/sql/SQLLiteralSpec.scala +++ /dev/null @@ -1,18 +0,0 @@ -package app.softnetwork.elastic.sql - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -/** Created by smanciot on 17/02/17. - */ -class SQLLiteralSpec extends AnyFlatSpec with Matchers { - - "SQLLiteral" should "perform sql like" in { - val l = SQLLiteral("%dummy%") - l.like(Seq("dummy")) should ===(true) - l.like(Seq("aa dummy")) should ===(true) - l.like(Seq("dummy bbb")) should ===(true) - l.like(Seq("aaa dummy bbb")) should ===(true) - l.like(Seq("dummY")) should ===(false) - } -} diff --git a/elastic/src/test/scala/app/softnetwork/elastic/sql/SQLParserSpec.scala b/elastic/src/test/scala/app/softnetwork/elastic/sql/SQLParserSpec.scala deleted file mode 100644 index 49a0c267..00000000 --- a/elastic/src/test/scala/app/softnetwork/elastic/sql/SQLParserSpec.scala +++ /dev/null @@ -1,271 +0,0 @@ -package app.softnetwork.elastic.sql - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -object Queries { - val numericalEq = "select $t.col1,$t.col2 from Table as $t where $t.identifier = 1.0" - val numericalLt = "select * from Table where identifier < 1" - val numericalLe = "select * from Table where identifier <= 1" - val numericalGt = "select * from Table where identifier > 1" - val numericalGe = "select * from Table where identifier >= 1" - val numericalNe = "select * from Table where identifier <> 1" - val literalEq = """select * from Table where identifier = "un"""" - val literalLt = "select * from Table where createdAt < \"now-35M/M\"" - val literalLe = "select * from Table where createdAt <= \"now-35M/M\"" - val literalGt = "select * from Table where createdAt > \"now-35M/M\"" - val literalGe = "select * from Table where createdAt >= \"now-35M/M\"" - val literalNe = """select * from Table where identifier <> "un"""" - val boolEq = """select * from Table where identifier = true""" - val boolNe = """select * from Table where identifier <> false""" - val literalLike = """select * from Table where identifier like "%un%"""" - val betweenExpression = """select * from Table where identifier between "1" and "2"""" - val andPredicate = "select * from Table where (identifier1 = 1) and (identifier2 > 2)" - val orPredicate = "select * from Table where (identifier1 = 1) or (identifier2 > 2)" - val leftPredicate = - "select * from Table where ((identifier1 = 1) and (identifier2 > 2)) or (identifier3 = 3)" - val rightPredicate = - "select * from Table where (identifier1 = 1) and ((identifier2 > 2) or (identifier3 = 3))" - val predicates = - "select * from Table where ((identifier1 = 1) and (identifier2 > 2)) or ((identifier3 = 3) and (identifier4 = 4))" - val nestedPredicate = - "select * from Table where (identifier1 = 1) and nested((nested.identifier2 > 2) or (nested.identifier3 = 3))" - val nestedCriteria = - "select * from Table where (identifier1 = 1) and nested(nested.identifier3 = 3)" - val childPredicate = - "select * from Table where (identifier1 = 1) and child((child.identifier2 > 2) or (child.identifier3 = 3))" - val childCriteria = "select * from Table where (identifier1 = 1) and child(child.identifier3 = 3)" - val parentPredicate = - "select * from Table where (identifier1 = 1) and parent((parent.identifier2 > 2) or (parent.identifier3 = 3))" - val parentCriteria = - "select * from Table where (identifier1 = 1) and parent(parent.identifier3 = 3)" - val inLiteralExpression = "select * from Table where identifier in (\"val1\",\"val2\",\"val3\")" - val inNumericalExpressionWithIntValues = "select * from Table where identifier in (1,2,3)" - val inNumericalExpressionWithDoubleValues = - "select * from Table where identifier in (1.0,2.1,3.4)" - val notInLiteralExpression = - "select * from Table where identifier not in (\"val1\",\"val2\",\"val3\")" - val notInNumericalExpressionWithIntValues = "select * from Table where identifier not in (1,2,3)" - val notInNumericalExpressionWithDoubleValues = - "select * from Table where identifier not in (1.0,2.1,3.4)" - val nestedWithBetween = - "select * from Table where nested((ciblage.Archivage_CreationDate between \"now-3M/M\" and \"now\") and (ciblage.statutComportement = 1))" - val count = "select count($t.id) as c1 from Table as t where $t.nom = \"Nom\"" - val countDistinct = "select count(distinct $t.id) as c2 from Table as t where $t.nom = \"Nom\"" - val countNested = - "select count(email.value) as email from crmgp where profile.postalCode in (\"75001\",\"75002\")" - val isNull = "select * from Table where identifier is null" - val isNotNull = "select * from Table where identifier is not null" - val geoDistanceCriteria = - "select * from Table where distance(profile.location,(-70.0,40.0)) <= \"5km\"" -} - -/** Created by smanciot on 15/02/17. - */ -class SQLParserSpec extends AnyFlatSpec with Matchers { - - import Queries._ - - "SQLParser" should "parse numerical eq" in { - val result = SQLParser(numericalEq) - result.right.get.sql should ===(numericalEq) - } - - it should "parse numerical ne" in { - val result = SQLParser(numericalNe) - result.right.get.sql should ===(numericalNe) - } - - it should "parse numerical lt" in { - val result = SQLParser(numericalLt) - result.right.get.sql should ===(numericalLt) - } - - it should "parse numerical le" in { - val result = SQLParser(numericalLe) - result.right.get.sql should ===(numericalLe) - } - - it should "parse numerical gt" in { - val result = SQLParser(numericalGt) - result.right.get.sql should ===(numericalGt) - } - - it should "parse numerical ge" in { - val result = SQLParser(numericalGe) - result.right.get.sql should ===(numericalGe) - } - - it should "parse literal eq" in { - val result = SQLParser(literalEq) - result.right.get.sql should ===(literalEq) - } - - it should "parse literal like" in { - val result = SQLParser(literalLike) - result.right.get.sql should ===(literalLike) - } - - it should "parse literal ne" in { - val result = SQLParser(literalNe) - result.right.get.sql should ===(literalNe) - } - - it should "parse literal lt" in { - val result = SQLParser(literalLt) - result.right.get.sql should ===(literalLt) - } - - it should "parse literal le" in { - val result = SQLParser(literalLe) - result.right.get.sql should ===(literalLe) - } - - it should "parse literal gt" in { - val result = SQLParser(literalGt) - result.right.get.sql should ===(literalGt) - } - - it should "parse literal ge" in { - val result = SQLParser(literalGe) - result.right.get.sql should ===(literalGe) - } - - it should "parse boolean eq" in { - val result = SQLParser(boolEq) - result.right.get.sql should ===(boolEq) - } - - it should "parse boolean ne" in { - val result = SQLParser(boolNe) - result.right.get.sql should ===(boolNe) - } - - it should "parse between" in { - val result = SQLParser(betweenExpression) - result.right.get.sql should ===(betweenExpression) - } - - it should "parse and predicate" in { - val result = SQLParser(andPredicate) - result.right.get.sql should ===(andPredicate) - } - - it should "parse or predicate" in { - val result = SQLParser(orPredicate) - result.right.get.sql should ===(orPredicate) - } - - it should "parse left predicate with criteria" in { - val result = SQLParser(leftPredicate) - result.right.get.sql should ===(leftPredicate) - } - - it should "parse right predicate with criteria" in { - val result = SQLParser(rightPredicate) - result.right.get.sql should ===(rightPredicate) - } - - it should "parse multiple predicates" in { - val result = SQLParser(predicates) - result.right.get.sql should ===(predicates) - } - - it should "parse nested predicate" in { - val result = SQLParser(nestedPredicate) - result.right.get.sql should ===(nestedPredicate) - } - - it should "parse nested criteria" in { - val result = SQLParser(nestedCriteria) - result.right.get.sql should ===(nestedCriteria) - } - - it should "parse child predicate" in { - val result = SQLParser(childPredicate) - result.right.get.sql should ===(childPredicate) - } - - it should "parse child criteria" in { - val result = SQLParser(childCriteria) - result.right.get.sql should ===(childCriteria) - } - - it should "parse parent predicate" in { - val result = SQLParser(parentPredicate) - result.right.get.sql should ===(parentPredicate) - } - - it should "parse parent criteria" in { - val result = SQLParser(parentCriteria) - result.right.get.sql should ===(parentCriteria) - } - - it should "parse in literal expression" in { - val result = SQLParser(inLiteralExpression) - result.right.get.sql should ===(inLiteralExpression) - } - - it should "parse in numerical expression with Int values" in { - val result = SQLParser(inNumericalExpressionWithIntValues) - result.right.get.sql should ===(inNumericalExpressionWithIntValues) - } - - it should "parse in numerical expression with Double values" in { - val result = SQLParser(inNumericalExpressionWithDoubleValues) - result.right.get.sql should ===(inNumericalExpressionWithDoubleValues) - } - - it should "parse not in literal expression" in { - val result = SQLParser(notInLiteralExpression) - result.right.get.sql should ===(notInLiteralExpression) - } - - it should "parse not in numerical expression with Int values" in { - val result = SQLParser(notInNumericalExpressionWithIntValues) - result.right.get.sql should ===(notInNumericalExpressionWithIntValues) - } - - it should "parse not in numerical expression with Double values" in { - val result = SQLParser(notInNumericalExpressionWithDoubleValues) - result.right.get.sql should ===(notInNumericalExpressionWithDoubleValues) - } - - it should "parse nested with between" in { - val result = SQLParser(nestedWithBetween) - result.right.get.sql should ===(nestedWithBetween) - } - - it should "parse count" in { - val result = SQLParser(count) - result.right.get.sql should ===(count) - } - - it should "parse distinct count" in { - val result = SQLParser(countDistinct) - result.right.get.sql should ===(countDistinct) - } - - it should "parse count with nested criteria" in { - val result = SQLParser(countNested) - result.right.get.sql should ===( - "select count(email.value) as email from crmgp where nested(profile.postalCode in (\"75001\",\"75002\"))" - ) - } - - it should "parse is null" in { - val result = SQLParser(isNull) - result.right.get.sql should ===(isNull) - } - - it should "parse is not null" in { - val result = SQLParser(isNotNull) - result.right.get.sql should ===(isNotNull) - } - - it should "parse geo distance criteria" in { - val result = SQLParser(geoDistanceCriteria) - result.right.get.sql should ===(geoDistanceCriteria) - } - -} diff --git a/elastic/testkit/build.sbt b/elastic/testkit/build.sbt deleted file mode 100644 index 270a5958..00000000 --- a/elastic/testkit/build.sbt +++ /dev/null @@ -1,31 +0,0 @@ -Test / parallelExecution := false - -organization := "app.softnetwork.persistence" - -name := "persistence-elastic-testkit" - -val jacksonExclusions = Seq( - ExclusionRule(organization = "com.fasterxml.jackson.core"), - ExclusionRule(organization = "com.fasterxml.jackson.dataformat"), - ExclusionRule(organization = "com.fasterxml.jackson.datatype"), - ExclusionRule(organization = "com.fasterxml.jackson.module") -) - -val elastic = Seq( - "com.sksamuel.elastic4s" %% "elastic4s-core" % Versions.elastic4s exclude ("org.elasticsearch", "elasticsearch"), - "com.sksamuel.elastic4s" %% "elastic4s-http" % Versions.elastic4s exclude ("org.elasticsearch", "elasticsearch"), - "org.elasticsearch" % "elasticsearch" % Versions.elasticSearch exclude ("org.apache.logging.log4j", "log4j-api"), - "com.sksamuel.elastic4s" %% "elastic4s-testkit" % Versions.elastic4s exclude ("org.elasticsearch", "elasticsearch"), - "com.sksamuel.elastic4s" %% "elastic4s-embedded" % Versions.elastic4s exclude ("org.elasticsearch", "elasticsearch"), - "com.sksamuel.elastic4s" %% "elastic4s-http" % Versions.elastic4s exclude ("org.elasticsearch", "elasticsearch"), - "org.elasticsearch" % "elasticsearch" % Versions.elasticSearch exclude ("org.apache.logging.log4j", "log4j-api"), - "org.apache.logging.log4j" % "log4j-api" % Versions.log4j, - "org.apache.logging.log4j" % "log4j-slf4j-impl" % Versions.log4j, - "org.apache.logging.log4j" % "log4j-core" % Versions.log4j, - "pl.allegro.tech" % "embedded-elasticsearch" % "2.10.0" excludeAll(jacksonExclusions:_*), - "org.testcontainers" % "elasticsearch" % Versions.testContainers excludeAll(jacksonExclusions:_*) -) - -libraryDependencies ++= Seq( - "org.apache.tika" % "tika-core" % "1.18" -) ++ elastic diff --git a/elastic/testkit/src/main/scala/app/softnetwork/elastic/client/MockElasticClientApi.scala b/elastic/testkit/src/main/scala/app/softnetwork/elastic/client/MockElasticClientApi.scala deleted file mode 100644 index 859bcc16..00000000 --- a/elastic/testkit/src/main/scala/app/softnetwork/elastic/client/MockElasticClientApi.scala +++ /dev/null @@ -1,238 +0,0 @@ -package app.softnetwork.elastic.client - -import akka.NotUsed -import akka.actor.ActorSystem -import akka.stream.scaladsl.Flow -import app.softnetwork.elastic.sql.{SQLQueries, SQLQuery} -import app.softnetwork.persistence.message.CountResponse -import org.json4s.Formats -import app.softnetwork.persistence.model.Timestamped -import org.slf4j.{Logger, LoggerFactory} - -import scala.collection.immutable.Seq -import scala.concurrent.{ExecutionContext, Future} -import scala.language.implicitConversions -import scala.reflect.ClassTag - -/** Created by smanciot on 12/04/2020. - */ -trait MockElasticClientApi extends ElasticClientApi { - - protected lazy val log: Logger = LoggerFactory getLogger getClass.getName - - protected val elasticDocuments: ElasticDocuments = new ElasticDocuments() {} - - override def toggleRefresh(index: String, enable: Boolean): Unit = {} - - override def setReplicas(index: String, replicas: Int): Unit = {} - - override def updateSettings(index: String, settings: String) = true - - override def addAlias(index: String, alias: String): Boolean = true - - override def createIndex(index: String, settings: String): Boolean = true - - override def setMapping(index: String, _type: String, mapping: String): Boolean = true - - override def deleteIndex(index: String): Boolean = true - - override def closeIndex(index: String): Boolean = true - - override def openIndex(index: String): Boolean = true - - override def countAsync(jsonQuery: JSONQuery)(implicit - ec: ExecutionContext - ): Future[Option[Double]] = - throw new UnsupportedOperationException - - override def count(jsonQuery: JSONQuery): Option[Double] = - throw new UnsupportedOperationException - - override def get[U <: Timestamped]( - id: String, - index: Option[String] = None, - maybeType: Option[String] = None - )(implicit m: Manifest[U], formats: Formats): Option[U] = - elasticDocuments.get(id).asInstanceOf[Option[U]] - - override def getAsync[U <: Timestamped]( - id: String, - index: Option[String] = None, - maybeType: Option[String] = None - )(implicit m: Manifest[U], ec: ExecutionContext, formats: Formats): Future[Option[U]] = - Future.successful(elasticDocuments.get(id).asInstanceOf[Option[U]]) - - override def search[U](sqlQuery: SQLQuery)(implicit m: Manifest[U], formats: Formats): List[U] = - elasticDocuments.getAll.toList.asInstanceOf[List[U]] - - override def searchAsync[U]( - sqlQuery: SQLQuery - )(implicit m: Manifest[U], ec: ExecutionContext, formats: Formats): Future[List[U]] = - Future.successful(search(sqlQuery)) - - override def multiSearch[U]( - sqlQueries: SQLQueries - )(implicit m: Manifest[U], formats: Formats): List[List[U]] = - throw new UnsupportedOperationException - - override def multiSearch[U]( - jsonQueries: JSONQueries - )(implicit m: Manifest[U], formats: Formats): List[List[U]] = - throw new UnsupportedOperationException - - override def index[U <: Timestamped]( - entity: U, - index: Option[String] = None, - maybeType: Option[String] = None - )(implicit u: ClassTag[U], formats: Formats): Boolean = { - elasticDocuments.createOrUpdate(entity) - true - } - - override def indexAsync[U <: Timestamped]( - entity: U, - index: Option[String] = None, - maybeType: Option[String] = None - )(implicit u: ClassTag[U], ec: ExecutionContext, formats: Formats): Future[Boolean] = { - elasticDocuments.createOrUpdate(entity) - Future.successful(true) - } - - override def index(index: String, _type: String, id: String, source: String): Boolean = - throw new UnsupportedOperationException - - override def indexAsync(index: String, _type: String, id: String, source: String)(implicit - ec: ExecutionContext - ): Future[Boolean] = - throw new UnsupportedOperationException - - override def update[U <: Timestamped]( - entity: U, - index: Option[String] = None, - maybeType: Option[String] = None, - upsert: Boolean = true - )(implicit u: ClassTag[U], formats: Formats): Boolean = { - elasticDocuments.createOrUpdate(entity) - true - } - - override def updateAsync[U <: Timestamped]( - entity: U, - index: Option[String] = None, - maybeType: Option[String] = None, - upsert: Boolean = true - )(implicit u: ClassTag[U], ec: ExecutionContext, formats: Formats): Future[Boolean] = { - elasticDocuments.createOrUpdate(entity) - Future.successful(true) - } - - override def update( - index: String, - _type: String, - id: String, - source: String, - upsert: Boolean - ): Boolean = { - log.warn(s"MockElasticClient - $id not updated for $source") - false - } - - override def updateAsync( - index: String, - _type: String, - id: String, - source: String, - upsert: Boolean - )(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(false) - - override def delete(uuid: String, index: String, _type: String): Boolean = { - if (elasticDocuments.get(uuid).isDefined) { - elasticDocuments.delete(uuid) - true - } else { - false - } - } - - override def deleteAsync(uuid: String, index: String, _type: String)(implicit - ec: ExecutionContext - ): Future[Boolean] = { - Future.successful(delete(uuid, index, _type)) - } - - override def refresh(index: String): Boolean = true - - override def flush(index: String, force: Boolean, wait: Boolean): Boolean = true - - override type A = this.type - - override def bulk(implicit - bulkOptions: BulkOptions, - system: ActorSystem - ): Flow[Seq[A], R, NotUsed] = - throw new UnsupportedOperationException - - override def bulkResult: Flow[R, Set[String], NotUsed] = - throw new UnsupportedOperationException - - override type R = this.type - - override def toBulkAction(bulkItem: BulkItem): A = - throw new UnsupportedOperationException - - override implicit def toBulkElasticAction(a: A): BulkElasticAction = - throw new UnsupportedOperationException - - override implicit def toBulkElasticResult(r: R): BulkElasticResult = - throw new UnsupportedOperationException - - override def countAsync(sqlQuery: SQLQuery)(implicit - ec: ExecutionContext - ): Future[scala.Seq[CountResponse]] = - throw new UnsupportedOperationException - - override def multiSearchWithInnerHits[U, I](jsonQueries: JSONQueries, innerField: String)(implicit - m1: Manifest[U], - m2: Manifest[I], - formats: Formats - ): List[List[(U, List[I])]] = List.empty - - override def multiSearchWithInnerHits[U, I](sqlQueries: SQLQueries, innerField: String)(implicit - m1: Manifest[U], - m2: Manifest[I], - formats: Formats - ): List[List[(U, List[I])]] = List.empty - - override def search[U](jsonQuery: JSONQuery)(implicit m: Manifest[U], formats: Formats): List[U] = - List.empty - - override def searchWithInnerHits[U, I](jsonQuery: JSONQuery, innerField: String)(implicit - m1: Manifest[U], - m2: Manifest[I], - formats: Formats - ): List[(U, List[I])] = List.empty - - override def searchWithInnerHits[U, I](sqlQuery: SQLQuery, innerField: String)(implicit - m1: Manifest[U], - m2: Manifest[I], - formats: Formats - ): List[(U, List[I])] = List.empty -} - -trait ElasticDocuments { - - private[this] var documents: Map[String, Timestamped] = Map() - - def createOrUpdate(entity: Timestamped): Unit = { - documents = documents.updated(entity.uuid, entity) - } - - def delete(uuid: String): Unit = { - documents = documents - uuid - } - - def getAll: Iterable[Timestamped] = documents.values - - def get(uuid: String): Option[Timestamped] = documents.get(uuid) - -} diff --git a/elastic/testkit/src/main/scala/app/softnetwork/elastic/scalatest/ElasticDockerTestKit.scala b/elastic/testkit/src/main/scala/app/softnetwork/elastic/scalatest/ElasticDockerTestKit.scala deleted file mode 100644 index 3e7f36da..00000000 --- a/elastic/testkit/src/main/scala/app/softnetwork/elastic/scalatest/ElasticDockerTestKit.scala +++ /dev/null @@ -1,22 +0,0 @@ -package app.softnetwork.elastic.scalatest - -import org.scalatest.Suite -import org.testcontainers.elasticsearch.ElasticsearchContainer - -import scala.util.{Failure, Success} - -/** Created by smanciot on 28/06/2018. - */ -trait ElasticDockerTestKit extends ElasticTestKit { _: Suite => - - override lazy val elasticURL: String = s"http://${elasticContainer.getHttpHostAddress}" - - lazy val elasticContainer = new ElasticsearchContainer( - s"docker.elastic.co/elasticsearch/elasticsearch:$elasticVersion" - ) - - override def start(): Unit = elasticContainer.start() - - override def stop(): Unit = elasticContainer.stop() - -} diff --git a/elastic/testkit/src/main/scala/app/softnetwork/elastic/scalatest/ElasticTestKit.scala b/elastic/testkit/src/main/scala/app/softnetwork/elastic/scalatest/ElasticTestKit.scala deleted file mode 100644 index 9fd80461..00000000 --- a/elastic/testkit/src/main/scala/app/softnetwork/elastic/scalatest/ElasticTestKit.scala +++ /dev/null @@ -1,313 +0,0 @@ -package app.softnetwork.elastic.scalatest - -import app.softnetwork.concurrent.scalatest.CompletionTestKit -import com.sksamuel.elastic4s.{IndexAndTypes, Indexes} -import com.sksamuel.elastic4s.http.index.admin.RefreshIndexResponse -import com.sksamuel.elastic4s.http.{ElasticClient, ElasticDsl, ElasticProperties} -import com.typesafe.config.{Config, ConfigFactory} -import org.elasticsearch.ResourceAlreadyExistsException -import org.elasticsearch.transport.RemoteTransportException -import org.scalatest.{BeforeAndAfterAll, Suite} -import org.scalatest.matchers.{MatchResult, Matcher} -import org.slf4j.Logger - -import java.util.UUID -import scala.util.{Failure, Success} - -/** Created by smanciot on 18/05/2021. - */ -trait ElasticTestKit extends ElasticDsl with CompletionTestKit with BeforeAndAfterAll { _: Suite => - - def log: Logger - - def elasticVersion: String = "6.7.2" - - def elasticURL: String - - lazy val elasticConfig: Config = ConfigFactory - .parseString(elasticConfigAsString) - .withFallback(ConfigFactory.load("softnetwork-elastic.conf")) - - lazy val elasticConfigAsString: String = - s""" - |elastic { - | credentials { - | url = "$elasticURL" - | } - | multithreaded = false - | discovery-enabled = false - |} - |""".stripMargin - - lazy val clusterName: String = s"test-${UUID.randomUUID()}" - - lazy val client: ElasticClient = ElasticClient(ElasticProperties(elasticURL)) - - def start(): Unit = () - - def stop(): Unit = () - - override def beforeAll(): Unit = { - start() - client.execute { - createIndexTemplate("all_templates", "*").settings( - Map("number_of_shards" -> 1, "number_of_replicas" -> 0) - ) - } complete () match { - case Success(_) => () - case Failure(f) => throw f - } - } - - override def afterAll(): Unit = { - client.close() - stop() - } - - // Rewriting methods from IndexMatchers in elastic4s with the ElasticClient - def haveCount(expectedCount: Int): Matcher[String] = - (left: String) => { - client.execute(search(left).size(0)) complete () match { - case Success(s) => - val count = s.result.totalHits - MatchResult( - count == expectedCount, - s"Index $left had count $count but expected $expectedCount", - s"Index $left had document count $expectedCount" - ) - case Failure(f) => throw f - } - } - - def containDoc(expectedId: String): Matcher[String] = - (left: String) => { - client.execute(get(expectedId).from(left)) complete () match { - case Success(s) => - val exists = s.result.exists - MatchResult( - exists, - s"Index $left did not contain expected document $expectedId", - s"Index $left contained document $expectedId" - ) - case Failure(f) => throw f - } - } - - def beCreated(): Matcher[String] = - (left: String) => { - client.execute(indexExists(left)) complete () match { - case Success(s) => - val exists = s.result.isExists - MatchResult( - exists, - s"Index $left did not exist", - s"Index $left exists" - ) - case Failure(f) => throw f - } - } - - def beEmpty(): Matcher[String] = - (left: String) => { - client.execute(search(left).size(0)) complete () match { - case Success(s) => - val count = s.result.totalHits - MatchResult( - count == 0, - s"Index $left was not empty", - s"Index $left was empty" - ) - case Failure(f) => throw f - } - } - - // Copy/paste methos HttpElasticSugar as it is not available yet - - // refresh all indexes - def refreshAll(): RefreshIndexResponse = refresh(Indexes.All) - - // refreshes all specified indexes - def refresh(indexes: Indexes): RefreshIndexResponse = { - client - .execute { - refreshIndex(indexes) - } complete () match { - case Success(s) => s.result - case Failure(f) => throw f - } - } - - def blockUntilGreen(): Unit = { - blockUntil("Expected cluster to have green status") { () => - client - .execute { - clusterHealth() - } complete () match { - case Success(s) => s.result.status.toUpperCase == "GREEN" - case Failure(f) => throw f - } - } - } - - def blockUntil(explain: String)(predicate: () => Boolean): Unit = { - blockUntil(explain, 16, 200)(predicate) - } - - def ensureIndexExists(index: String): Unit = { - client.execute { - createIndex(index) - } complete () match { - case Success(_) => () - case Failure(f) => - f match { - case _: ResourceAlreadyExistsException => // Ok, ignore. - case _: RemoteTransportException => // Ok, ignore. - case other => throw other - } - } - } - - def doesIndexExists(name: String): Boolean = { - client - .execute { - indexExists(name) - } complete () match { - case Success(s) => s.result.isExists - case _ => false - } - } - - def doesAliasExists(name: String): Boolean = { - client - .execute { - aliasExists(name) - } complete () match { - case Success(s) => s.result.isExists - case _ => false - } - } - - def deleteIndex(name: String): Unit = { - if (doesIndexExists(name)) { - client.execute { - ElasticDsl.deleteIndex(name) - } complete () match { - case Success(_) => () - case Failure(f) => throw f - } - } - } - - def truncateIndex(index: String): Unit = { - deleteIndex(index) - ensureIndexExists(index) - blockUntilEmpty(index) - } - - def blockUntilDocumentExists(id: String, index: String, _type: String): Unit = { - blockUntil(s"Expected to find document $id") { () => - client - .execute { - get(id).from(index / _type) - } complete () match { - case Success(s) => s.result.exists - case _ => false - } - } - } - - def blockUntilCount(expected: Long, index: String): Unit = { - blockUntil(s"Expected count of $expected") { () => - client.execute { - search(index).matchAllQuery().size(0) - } complete () match { - case Success(s) => expected <= s.result.totalHits - case Failure(f) => throw f - } - } - } - - def blockUntilCount(expected: Long, indexAndTypes: IndexAndTypes): Unit = { - blockUntil(s"Expected count of $expected") { () => - client.execute { - searchWithType(indexAndTypes).matchAllQuery().size(0) - } complete () match { - case Success(s) => expected <= s.result.totalHits - case Failure(f) => throw f - } - } - } - - /** Will block until the given index and optional types have at least the given number of - * documents. - */ - def blockUntilCount(expected: Long, index: String, types: String*): Unit = { - blockUntil(s"Expected count of $expected") { () => - client.execute { - searchWithType(index / types).matchAllQuery().size(0) - } complete () match { - case Success(s) => expected <= s.result.totalHits - case Failure(f) => throw f - } - } - } - - def blockUntilExactCount(expected: Long, index: String, types: String*): Unit = { - blockUntil(s"Expected count of $expected") { () => - client - .execute { - searchWithType(index / types).size(0) - } complete () match { - case Success(s) => expected == s.result.totalHits - case Failure(f) => throw f - } - } - } - - def blockUntilEmpty(index: String): Unit = { - blockUntil(s"Expected empty index $index") { () => - client - .execute { - search(Indexes(index)).size(0) - } complete () match { - case Success(s) => s.result.totalHits == 0 - case Failure(f) => throw f - } - } - } - - def blockUntilIndexExists(index: String): Unit = { - blockUntil(s"Expected exists index $index") { () ⇒ - doesIndexExists(index) - } - } - - def blockUntilIndexNotExists(index: String): Unit = { - blockUntil(s"Expected not exists index $index") { () ⇒ - !doesIndexExists(index) - } - } - - def blockUntilAliasExists(alias: String): Unit = { - blockUntil(s"Expected exists alias $alias") { () ⇒ - doesAliasExists(alias) - } - } - - def blockUntilDocumentHasVersion( - index: String, - _type: String, - id: String, - version: Long - ): Unit = { - blockUntil(s"Expected document $id to have version $version") { () => - client - .execute { - get(id).from(index / _type) - } complete () match { - case Success(s) => s.result.version == version - case Failure(f) => throw f - } - } - } -} diff --git a/elastic/testkit/src/main/scala/app/softnetwork/elastic/scalatest/EmbeddedElasticTestKit.scala b/elastic/testkit/src/main/scala/app/softnetwork/elastic/scalatest/EmbeddedElasticTestKit.scala deleted file mode 100644 index d0343344..00000000 --- a/elastic/testkit/src/main/scala/app/softnetwork/elastic/scalatest/EmbeddedElasticTestKit.scala +++ /dev/null @@ -1,34 +0,0 @@ -package app.softnetwork.elastic.scalatest - -import org.scalatest.Suite -import pl.allegro.tech.embeddedelasticsearch.EmbeddedElastic -import pl.allegro.tech.embeddedelasticsearch.PopularProperties._ - -import java.net.ServerSocket -import java.util.concurrent.TimeUnit - -trait EmbeddedElasticTestKit extends ElasticTestKit { _: Suite => - - override lazy val elasticURL: String = s"http://127.0.0.1:${embeddedElastic.getHttpPort}" - - override def stop(): Unit = embeddedElastic.stop() - - private[this] def dynamicPort: Int = { - val socket = new ServerSocket(0) - val port = socket.getLocalPort - socket.close() - port - } - - private[this] val embeddedElastic: EmbeddedElastic = EmbeddedElastic - .builder() - .withElasticVersion(elasticVersion) - .withSetting(HTTP_PORT, dynamicPort) - .withSetting(CLUSTER_NAME, clusterName) - .withCleanInstallationDirectoryOnStop(true) - .withEsJavaOpts("-Xms128m -Xmx512m") - .withStartTimeout(2, TimeUnit.MINUTES) - .build() - .start() - -} diff --git a/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/JdbcPersonToElasticTestKit.scala b/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/JdbcPersonToElasticTestKit.scala deleted file mode 100644 index 1c395ea4..00000000 --- a/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/JdbcPersonToElasticTestKit.scala +++ /dev/null @@ -1,28 +0,0 @@ -package app.softnetwork.persistence.person - -import akka.actor.typed.ActorSystem -import app.softnetwork.persistence.jdbc.query.{JdbcJournalProvider, JdbcOffsetProvider} -import app.softnetwork.persistence.person.query.{ - PersonToElasticProcessorStream, - PersonToExternalProcessorStream -} -import com.typesafe.config.Config -import slick.jdbc.JdbcProfile - -trait JdbcPersonToElasticTestKit - extends PersonToElasticTestKit - with JdbcPersonTestKit - with JdbcProfile { - - override def person2ExternalProcessorStream: ActorSystem[_] => PersonToExternalProcessorStream = - sys => { - new PersonToElasticProcessorStream with JdbcJournalProvider with JdbcOffsetProvider { - override val forTests: Boolean = true - override implicit def system: ActorSystem[_] = sys - - override def config: Config = JdbcPersonToElasticTestKit.this.config.withFallback( - JdbcPersonToElasticTestKit.this.elasticConfig - ) - } - } -} diff --git a/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/MySQLPersonToElasticTestKit.scala b/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/MySQLPersonToElasticTestKit.scala deleted file mode 100644 index d5d8b1a2..00000000 --- a/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/MySQLPersonToElasticTestKit.scala +++ /dev/null @@ -1,5 +0,0 @@ -package app.softnetwork.persistence.person - -import app.softnetwork.persistence.jdbc.scalatest.MySQLTestKit - -trait MySQLPersonToElasticTestKit extends JdbcPersonToElasticTestKit with MySQLTestKit diff --git a/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/PersonToElasticTestKit.scala b/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/PersonToElasticTestKit.scala deleted file mode 100644 index fa4d77e6..00000000 --- a/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/PersonToElasticTestKit.scala +++ /dev/null @@ -1,30 +0,0 @@ -package app.softnetwork.persistence.person - -import app.softnetwork.elastic.client.jest.JestClientApi -import app.softnetwork.elastic.persistence.query.ElasticProvider -import app.softnetwork.elastic.scalatest.ElasticTestKit -import app.softnetwork.persistence.ManifestWrapper -import app.softnetwork.persistence.person.model.Person -import app.softnetwork.persistence.query.ExternalPersistenceProvider -import app.softnetwork.persistence.schema.Schema -import com.typesafe.config.Config - -trait PersonToElasticTestKit extends PersonTestKit with ElasticTestKit { _: Schema => - - override lazy val externalPersistenceProvider: ExternalPersistenceProvider[Person] = - new ElasticProvider[Person] with JestClientApi with ManifestWrapper[Person] { - override def config: Config = PersonToElasticTestKit.this.elasticConfig - override protected val manifestWrapper: ManifestW = ManifestW() - } - - override def start(): Unit = { - super.start() - initAndJoinCluster() - } - - override def stop(): Unit = { - shutdownCluster() - super.stop() - } - -} diff --git a/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/PostgresPersonToElasticTestKit.scala b/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/PostgresPersonToElasticTestKit.scala deleted file mode 100644 index 65074f02..00000000 --- a/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/PostgresPersonToElasticTestKit.scala +++ /dev/null @@ -1,5 +0,0 @@ -package app.softnetwork.persistence.person - -import app.softnetwork.persistence.jdbc.scalatest.PostgresTestKit - -trait PostgresPersonToElasticTestKit extends JdbcPersonToElasticTestKit with PostgresTestKit diff --git a/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/query/PersonToElasticProcessorStream.scala b/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/query/PersonToElasticProcessorStream.scala deleted file mode 100644 index 7a8e013a..00000000 --- a/elastic/testkit/src/main/scala/app/softnetwork/persistence/person/query/PersonToElasticProcessorStream.scala +++ /dev/null @@ -1,15 +0,0 @@ -package app.softnetwork.persistence.person.query - -import app.softnetwork.elastic.client.jest.JestClientApi -import app.softnetwork.elastic.persistence.query.ElasticProvider -import app.softnetwork.persistence.ManifestWrapper -import app.softnetwork.persistence.person.model.Person -import app.softnetwork.persistence.query.{JournalProvider, OffsetProvider} - -trait PersonToElasticProcessorStream - extends PersonToExternalProcessorStream - with ElasticProvider[Person] - with JestClientApi - with ManifestWrapper[Person] { _: JournalProvider with OffsetProvider => - override protected val manifestWrapper: ManifestW = ManifestW() -} diff --git a/elastic/testkit/src/test/resources/application.conf b/elastic/testkit/src/test/resources/application.conf deleted file mode 100644 index ba8abfad..00000000 --- a/elastic/testkit/src/test/resources/application.conf +++ /dev/null @@ -1,3 +0,0 @@ -akka.coordinated-shutdown.exit-jvm = off -elastic.multithreaded = false -clustering.port = 0 diff --git a/elastic/testkit/src/test/resources/avatar.jpg b/elastic/testkit/src/test/resources/avatar.jpg deleted file mode 100644 index 7a214ba84b0f9489c5a16f404392aef6930127bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34169 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 a<8I>MZ%{Q5HAaFhJ+cb>b%Cbt^#1{iArKw_ diff --git a/elastic/testkit/src/test/resources/avatar.pdf b/elastic/testkit/src/test/resources/avatar.pdf deleted file mode 100644 index cf44452f8358b703a5104d6e2ed6f7861bddbbd0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112041 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 z zchxdM!~AfvrrZ4Gn>KIFJhQj@+Tn*u7oMeGeVTfy^I%7rp^BaZ-;mCu*3b6_LXgm8 z1Q$~1kV0T2jcNW>aGQF11Ma<&YrQ6nwCn2^MW562X;&U3oU6FF`||#6bC$2u_6Yo# zs{7M*yX%_uHntgH>BQQ>b8IX42*JlYNH8B)n2#ehoZ-(@HXzX$hu5sYdGH^0FWq`M z9M2btIj|tCFh(XQlwNu3_J78{>Td>BTJtL)54oIVsZ%9`bal42Pp`cF`i;!fl=7Oy zlB&qOlF;mY1mrVG>1X0o_J&3-Jb1!w;|@w*eCOsn$k?jObY1+R&|_@#LFV>Y>)!}x z*4(zT9q>oi{zBnFGyrzG%reByI@Hi=#pb;ykDbYjxKfi4`Zy!<+4aa5rd9_VS&inS z)Q|y$M(_Ve|CxH}S!Vdd^svh43vuVqY~6j-W&Wx`2A175L=9)w3)DFR3)>XctXg_- zfKKoWCI$oM)0fgRX645g zl-AsTc=yqh2Tz_hs#4yue?C%*4^xN_*#V-(>}$;={ZoF1d#6Sj$V2g*irR-%L!}z? z>o;l}W>i$i<`*4^h*^E|ob|Fblf44f^~}0!8WNUF(0g~Wo14~uO_=`wzMIC{#1=~< z?cBccoINRE{>%12R}HXc4_G3R2nO4_DQo zby9R)3Y@=F*ICQ~z z>ALZD?)Wq*5ee-fnG2hX9QI}(bPI3Fe0G+tAN=Rx6~#BocJ2S)w^IS1zu@zlrbAU# zSP+fch69bQHD=FeKd&W6Pw%-FRZw%Qy7vD4$M2Bm&4D3QQ&uLVV4z?=ZEutm%chuN zMNLdz(bdeH!;x`2E?rx8ZqQ z#$@EiCX~d-+)WO9kaD%N@dvpWp@!FY$OH0#Iroz<-$}e$5*v{l9UB*!5f`165SM*9 zEOGOZGnVs~sq30{*EAxlqK}C!6;oTYKTcB=Qe0g@-}!}n<0ayJ$gKcvFyt9&B93&5 zR+m`@-L;LoYZ>=7u=v8kV}ggj{=B8OE7q?#bbR}{OJ`!@uccLp#zAXZdeyD8%A2VbH7VuQi6xa0H;OK1 z)yZiOkHDd!NLXpX-;VBc1Jk-X8Hpt4Uhps8Z0Kx;?LEB+%A~}L} z3TIY3V29xc?65=oF1O2@$EK zNr^X;qaUVR27I2hUW94mCsB!s7<|;;w09Lx_@f`t!}K$-Q2aB+_*ktPV!^c#~W0lXnb?*(gw+(r?9?GJoRko@DQ#dGb z5L~%_<5W`m-td?$7q2Zja%%3L!*(k-7|vTd*1>(4xjpt28`QD^pPHWe05iM6mX17e zV5g6Nx|MXTA|X6KHYO`NJ}xqyC4gszzcFi#H=a1N|H`%Vafy)`*V7A%imGaA?$y;kd@U9v zbe^Zr?md2T^TDHvx(9fB-*9^ik*CE=5+M+H0dNNZIQRw5rDanHZso}fzT5Y*r}?+8 zKI;1Bh=c@Du=N0#Ls1LX6vA@Gy7_+TA=1V_qcEV2Yab(P@~8g$hnc9#M4Y1SeC-*C zwj)A!THHLMg(1huiG79e^w2kFhd$QL2~J+eE`{aXC@L+ftg5(41AG|ah56-43Aunz zWLSD^cv@;qT6SDgbzb(4tr1WgxwOjh>nF}#c)3C3p zE#QNPy@f~L!hZ=@mk^voCI>F~($#mEwF^Op&>k78I>^##go7trda<_!zSOawj+{6bn~;2?04wZfZQY&wf(X?;dU78Y)Lv2g z(bMOTpZ)mg&9)Y&`I}RCT5}m-4uHe9o}kTv=){c|LU9dvZ`otNdJ{!TCwc~quy9Z} zu%J3FQ5ghr^)a-B;S8~I8UmwSY(#ou zWD2WrVO&gg68IDPFzMpsmmNmpwUuN4VZ zCz^gn;YZa;PRzZYQFtS#q`XwXj(k8Iutww;loa#^!2ItB(s1)9LKRm#UQ}V@>s$0UcNp#LgK5UtBW(3EN3Y?wZfq<_ zI1pnPLGoh~TD6Ubnpl71=wUTJTt3fP>Efa|gnSF|;Yq(oM^VTFT=+1E&Tp60oD>p}%bWwEVuT z?%vnLmQ8iL%rsEfF$tq-7yodOCPd$e zjf)RY#<3W2Ee*8K&MeBmQQ8o5RLCPqX?#M~k&|J%-XUG7RDeRLZ8FHpS!|BeF63e4 zP{!w5-?<1rQ)Vw9U<1vE;+Dd%F;$CoYM7lH;3H2iAjpewpkN|`NK(cx-_OJbEgFv) z+h;S>slq|r(O_e%Z=5}SH*VW>>TGmo76^(7R`>X|$WJ?ftKDDuzeSanf1SWTk{Sqd zI1-z(Gc;=H(KGD(V6k)!F34|qcII~MU@NwsnV}1SsOg&%go9`5GortgG%M^TnhH#? z(?>dZs+-tiUxQJ63)zPOfTYYLp5Q-ke0x=8iD`J!i9PGCYx?1Ioi8k1EM_m=fAUId z+Ku9(3I?-kRaJox2fSa9R|fe>P0mMlj*7^Pj>w3QOiztU%!!IFijAri_SaP)=r#F| z^lc&akOc84zMbV&I~HV`J)07Oh&|Yzy3?u?3Z9d4@t)v0Va2hLH)3MaqY{&&lH;Q@ z<72bonW#EgyQRgIjUI~RqPPIEGK%72vo2mrT)y*&^}-dy&Fx{)y%Bt@UD}kx-f*dc z_5eN;{6i)N3g&}hg|~~L2=k#FeNTOhex`N|A*m*@Rx%ou0UQJi8tvpY!p?1|rBi>Q z1(_pSiZMS+pUU%JJ9=pN&aq#*+GoSob;nL22SsIMr56@g-o1w+jS1FnWa#WKwmkRW z{QnAiQ(8?(Cx#Unb_AKT(agL9$6eNKo8&cnsF@wyhKL9nwI)~s@P4A&A>*))ObnPe zbsuGd)7Dt)7n+sS zbLgc9$(QPqFIOZ)=EcP)gr`J>r6ZBoQB^V{7s+)YBS)_Zra0$0#ER>PYGH$HE1DK=1>Zk@2feU6{M?sOh3rT0slGbMyPc&aI!m z1&SY=ZzBFfS14YA7Dw3}RJ-w{Id}(s3qd7t)MpN{>nhk`oUl$I;=y)$=$JxQ`T778 zTS~aI^!%Va)Aj`_tDuSV<9O|BVuLeloSmC_$g<_zkDNLaef4TeOk`$iQr`9SLWw_( z7Fjv+l`&skTv!44WTp$_3*{N`i3}6qlNyn5Ju(JgOlf>fO=5(Qpxf^M!lgyUKkC4f zLQYM>w@_qO6dqBv3}cY1Z9S;UPa=!mOS*hBF|0fxGCwvtJ1QnEG9f848A*reskD?l z{E0VmN})PUEi9gb0{)`^Q=JmbR3#pcq%Cq%$c+Iu+hR5le}kBQkzIWVvI$RNyS9M?Fi>YG$uged;gG0 zf%7q(VLOuN2uY`bXA0OE<4WmuFL*L*3gvNNJ3Q#G-25l_%w^qyAYpx@?6rRIH2kD; zem`K10omKYvZtnzdC1aTr!Swo8Xp;v8IF2$Ej>CS6JY@Y&#-4zRTkXet+tGO)?e_4 z|0*h|$i7}gc!^kW!!yu-&?O=Xdj^7HQuE{E%MzmRCSreGdYF6(2*Oj0A;xn5%Bjk9 z26?DrK}M%Yqj<|aTmlbr%%RVfZm)-ZJ_+ZHjm}C+xKUDk^YO#yKmGK}^PhfBx{-hIYWPgw zAd2Fuo7gax;Lm_h`^XRD6AbwwU>?k8+MGpz&lm?!Rzt`SF<}7A5em`6e@F%Dr)Q=gG~a#Ah8^cGT}(*Muc#`& zbq68(R^5HvVFU%W9wj>Clbww%&7Pl%qf$4HFRsL%I+2)8RUAwzlZ7j11OT!QgM|O^w6u`5a4{&`W5<**B+O#3&$Ya>fP_iUUZ19wgvU0RsxP0fCi#dg* zMa5N+T?TMOSO(w|aVIFN=>TjE%~PiOr0POAb#+jDX~%CBy<9IY{@p*~NLeg6AlJ|8EDDifRJ&^fn(u z{6PYut7Btwva`x=-mH84`1xD-Rwsn7csT&;qk-poQZw zJNNc!J~F(J5sRyfwU2dkg3lZnII_=2({;oyV1zH4+5v8yzY(}G!tu|7KMDoH6&s90 z78M%94VMZh!3w|~#ub8NKoG&$Bg||kxOi#!&9z>%0v5FW?8OW5N!N=?Kq2I45s~=X zL5c0drDl<#^WIbe4$(M}9Mt3eQ3)H*hq`avVK8r5Up+In(vt!{W)Y2NL9h)v@MuR* zj3c-W%fz&~i+G&`Uky-X;g{r+^VE2J#7lJ1F!*4avKg_K@p$)uAN+#(I&@dC9q_8v z)mIuJk32tYN2tovw7+-rS#juCY-aYOM^7F-c!I!}m027Ul?C#Ig$nWmhXG8W^k|!M z{e2n!Fe8wLm@?Vdi|fOF!UcjM{sahkkJ}_N=T<^!d0co- zY+PD&VtgbFhi*xa3eO-!0d0+bhUd`93doB<9f>xoSEvsEi{^tB4AsHHiVV+;jv}fM zoVxw<&%gZRAOHN{|NY;8{p(-<{O3P^{`u$o4~;_AXIYBVHT z|0e`SM)*vdvl#sO&O7J}CoiO9T&|d1`0+tD$*h{5lMzcHAe2c4Ff&5<%RsQ8g-8bo zL0Dmc7qSd>}8IS*gQ3X>2-EF2~n3w>%tTxMh}%0^*)4DMqbI%Fi> z7v|XSh!8}gHsZkTOWP=RTJPa%g^l)Rz=vyt@KBk#X=WLBQI~Xy2*8^_O?)IN4>{4X z*CS)oBI1%G65<<>93&b9WPqI>qi^9k&Gs#wtWce*{Jipvw0vL+RV_XiqpTn=x8lz2 z2Tz~;2-T68^V@H~{rbzVJkV#)o|jhNjLFCvYvYWf(o@g8pP8T~EewyA|4X{g3He|= z6VU}#{J2TnXF0Ll+2vMZ^dIpG($RrK=LeR1TUExvUCP5c**qOB?8z9 zL{~y|T6nTx0#M%e*EYu%qJ(SG$|nu0RiT7?P?mhfq+TgXl`~X6GW0973MU1 zzzizPugJ+N!T7=`fID##&Ze|9&PhWKBIi0YHW{s>JU#;OAtFe!AZ!ynKFC3}5r=P6 z-INNY*Y*-y_-0V0HNOJ#+>btfFYLgBxKmuGLffQ+FM8BVFwT z5ed!b_uj5%Pm2kL{e}K8dA2wegPbN$IL_4f{j{v#ry6)UWLHdO1s)!{hP^n_G^JQ$U@^5l8f_1iT9=i{ZKL`ffA zGoGM`iJGAc$y7JCCfInKn;)^nfHu65Re-LWK6eR@atte2%GW*^M?o#iGN1w0F>yOY zUwq1LddjvD!>pX7wu4_%k(5OedYfvwH~YBxEmpmOmX6(ZOt7i#mafVwE`9X$DGMeP zopH-r`TW`Q2lpS}y>lPgCo{d6agNgp#9*|8MYvs9c5h^tNVj`aqd)~j(lo&Tg$ag_ z1{H`eGUIR-b1HF8z$Y{Y)o!!99PcuCx#_(FLBorW3MH}gpu`- z2KFW=vAw$a9pg@dsW{oft6gGv`fd7|Sz?zN_BzHt* z_K}w{$jI^w8|Miw-Zo2C2JhT^E-o=Sub`&xzO=u{OKod=`1E;e@TVzIyyiQC;V2P+ zvd}0law#Y_2$W5vj-Yk!9Zx}y-3Hvj+&ksnEaW8@Ks6h2eic!psWH@f)?>m zkb*I36~pQkTR-rCScwR&>85RhJ&uvpTi+Z&1I>h*he7Qhg1E)rs-K@{+PM_ycuGjLSjZ zlm-H8v7|uFF5GgxW^(Z{JT>4CUNBT3W+IbF@r7kZhT#qqT2MITDl|0`APP@SicCq5 zPRfai%MHJlclBKUrQ?Mck5omTB~0iZE$K>a(iJMz$cA2ot4Q&k3D)uYjW9@gLSFAD zU8eCv{34BC=NfLsU#*V2S{#1i#+9?V5n+hu>CuTPQ7N$zq*kTDa8Pduumoe#aN=XK zkY%u{q;&j3#%VrK6Y!xY7168;%#omtw<|72^cW8!IpIe3_0oIyST25o?FbX=U;mPn zRRuc&d4Bxq(c>qH*}3~JT^(v{4e}86H_X=E^@mXeYfeK0)IplqNq!T3d(?I;ihp~8ln)|1-`I;#@1Bl;QZ*5isM+5 zr4toM!+-9Q{YUm(4vo5=omN~daBmE z1MR_UPZk(hI?Xd++nEc{9w|=3*uft`e#8R$>$9gnB0Hn}loVAbC*~1yA}uCR2++Xo z4u-)037`dhm@LhLk6g#)Y--4Yn1<*Z8EJ)S$$9W7rWGd#e`G{r)uagk-Q;pK7HFmv!$zfMyOrLfZ2 za9T-RWKn!Cl@*!NdKRLLFDERilpFG#wGl+zKP8HA9Hcpx7|Tzwr@Drd)7w z-Hyd=YURW*CS{d`Ac9~&Z*{UfC;ZY=@q#HuHL({M{6fl}BLpSzhZ%=)ftwg*X`q=s zo>^uk8P+{C4Tcz7kG69B!QEFoXkN&kgImsDjx8ujDXq+^x}8%);XbN{-lYmw(?)H> z;=6UX9zMRW(4<$=p4VhJ^6Mm3k>)_=_@&G|Fo&pPyyVX7wrT{<>#b`_zAZN92UE4U zJJe%*Rlp9IfUgS4h2D@Pr=IG}Sq#tNeUyJ(-ile&D$L;tG8pioqyEv8-N>iVGlS}I zD$t&1O8-NgcqpwL53TnS3o3$uE zsmTS5e`bNyY5)q>bTm>Wu)m^&iVgqfKQEnJFIbT9(Y#Vqd6N5GIO2m z+34it$mGPx)P%^im@pwviGU?RY$UhIq#t6UGAY#s6mu_h2%stfrvbxxX`iTgjznh3 zbLd#OC1+M(CmRP=L4H+b#hvHRe){#7-{3jYI+m~_Ev$yGKmPdB-MahhkDb;I3}N*o zH;_;Y7M<3QkXL?^aSr%Q#O9I;L(oDQtHT&3W2>8%F)T~Ey4v)+jCPh zkRQdM5Nk0JES`aQjr*}!>zEQ0%?xFp!h*VM8sNn@ll94Y5dppo*gWa?CeL zgqXRwm%2+nZjlHN!G2i_uiV6*q$=uLP|i3p~u$jvGt4?=>l zgkKz3Dam;N&Ye5LfyF`q9py>=`s=U1{32AHMpQ=yK2lqrvu*b%3rCC~5fN_u( zUzKe2n1j=5t_W#>oJe1};5lv7VT2Fe$-9rC6%RzFw}*Y_9uZqH>>Yqc`+R@j$SA~!b=4jusWlB z%A5;~i}F&mQ&~Xfr3z}J3CN#d5kSNo#DY`e5(~xBFI0{!j1EbJBnm0Nj=xMcp*}Tg zl9&k{yQS9nTQ5$coE8C!7M{T`(!iXA*c{9sQq{m5#T#DCbt+KPl2F|AHh~)1=pxB; zKrQ@+=_w@P5~U;jfCAE^v(i%wZse9ddi3n4AOFrmprSg2>xh4fnB43u$aC|~J+F0J zPz*pGYTXpg25W1D&!GQ7Fb7hCx!GYjxR z0)kutKAazav7CmyG69*PEZJ1&z-nOTzBgHeiHt9dYF8B5BU5|c{$rajhFwU{Vars~ zDsfnY1CqT!gxJC14ImFmotrLP3*33|JNA+`v&U`yA>rRZ&2$}#aUo$5ii4akH5Pyc zj>nyHgDT`f%%@h85`;Wg$}jEnHvu~oz990GN;~{CvNbsNE{_*#pfBEtGPmdoxxqYh+mu_Zy z5oo~FmR@qHKQmha3L<$BItmL!{9)6n_p)~yOi37#h^gc2G^o*wCU6ORB=1Jqjoi|l z>=J$j{T3)_)Zj~)Z!I^bIaCdBYxrH%mE4?C<~xmFAvOSPfx~bCp@ z$10|%;zCC5v4k{gR;)d9*=g+-z2GH7&F$5c60+1AA#9+pfhEcqF?&e6xCk3Ujl4T< z6Ig(X5Xm6L9C%>+>>!>##xLHbzQ)!)bWBKEA7bGk-L&eQ~mU=>Rr3`UaRYwsokt254Xxq zZTIJDE7BoI$93{g@}{&G*jcLw23eF8ncf)J=99WJ$Lc&45)S_`{)SH0lGxM@kJdJf z`!G|rcj}9lb3U;S2{Gn${F|3hAitdp0XK^Js?Ow?kG$;(l~`ES#CD8k!OHpvvt-jA zC%d~1{@;oi{PSWtKys=DDR`1di}?Vh_NP?oz!~k%*lZrd=*t7qfZMBC(3e{_MVLlu1x55*2*@G& z>RXpNMV4RJ^M2S2B)+N@e!zp|1^2Rk#bR2H6JpnZT?twpqqC)EMG4{FbN96jPS97` zzR6)f-&qjYM?mv3im`e>Xg>?cNcqHOM>FA-&m)l)<}2`#?KUOC6do`EWyXkCLe$mnY^M^yT2VDyva8eDWyI@9sK$1^HT|Ormnn z!iYYCh`67q)-QKN3ZmYil`go;Z)X$sl~TJ^p+{~#8hMB%0EI;?P7tC1sJ0EscdrleU9W_{B$(vuvxx zt>d>P146!$(p@D!S79PPghwTxC|`|dg2zv+$vze2MmTeOg+P&*a0k)6U5usK4PR|% zXrBeDECRlmT^t)o0ZwvH9oy)Q6h_#NOi9c?O3?P>N_!Mg;vn=VN1B^#mi|h|1vlpG zQCT#a-SwX8nV%@1I1`}WmwqI!gK@G9BDp8u>>vzxrWaPF(d*{4nzWhS4Z{DlM;RTr zS{izfAB#K=f3f`WXN6`3qmMf}%O44}^N+H9zakO*OcqoaPb$79nC_(L_5ntI<*+zjVK@EUQ-l)2M?E0^z;X4Y zoGHkgJijJ^rbE8^tN7}z-_^R6rz3dztwwS-bRTHAZW1RIB6G{ji={|%Di0ldMQY^J zgvsMpeXmOw$fr}N7?Am-tjsp(D?4B{&1FZhhi^O1|0p++nqpf&7S7NB+v zBe7%>_uv9Y2oYULzn-oieUtvD4~jKb^r=368eL7Ht*-5}AdTvK8N^OBbSP6jtn8b1 z(z@jRhxFEa+CkS5=i1Mr#W+c%=)pLUuVbzS-gt{eoTO4}eT;opTg19c4D`AB+av+I zZ0m{FYX#e-n&~kdZW>RxTM$6LNB8D)?xgXaN88pWvwBMf@%66Qoe5Bxf^~@j8NN6v zH-ALYZ^209mHNhg=&333HB1kjCTf;_%Vg0W$UNUG+FJb>o zOzZC9M$pHJqDkN0A*{9amr?TL?SGH$@Oe`zBpquIT{7hU1&uE{<#Akr_yY_>j$52b zfB1ZiZzYuUSR9n8Zl7{aMZ?F-g>~qTdSdH^A;G|qOo}hbR#rmgQEDl%ee~cM?7)V} z+)r?Z+Cx^_nW?PUN}IEWLyp_$5vQqIlIb!eugzOh+wokou^dcSGMr_X*DA)cuGsF_jZ8shOf1z^_)%`{qJdo~eKWtd+6MkHvvnMkZzo*Pezs^+t zHLli^o}Gp<;AHO__~u(-k`mnf&~*CkNV?7bZZtp5$%-0xptUfof1%w8I;z1;{3UiU z-HE%6{Jw6i(N+>!?A_2FhSI~>aF1JOz5A#uNSbrAV$a;2tpQh#Y z{%UCX-Y#PqE1>e1CV+she3gWh+`zN|1x-t{?yC$}0JWrdC>DlnDJ_+sibiSIs9di4 zsAJ*Tt0EadK13&2AitEajGMEr9Quulv63zP3gd$!?0fi%Nyg@8avJecv-_@?X$S&t zdFHlUeKnEs=kl(f-Y&aN`>i>@nVNs&XUNGRFQP;&?P8}01d{TZh`fex4`pLDhB zD-MBEt~-mnFq`brivweS6ky?maTuL6e>bHs_MS`5YB^Mg(GJ;*Xt`84(l+jclod&w zIJRgtCN)eZBA0`QV{SW9FXzG&mk4)LX6p%0-TmBX;8HkyjZAI?VTdfbv_t_OkpiaV zSIpnBw=u>R)qK|_{F9eT%1&06cwFf}D3gu2f5FnKDHWshr3>|<5llM6WsF3$$W9Qb z{ICX|VB>V)bA2>Se1bHbkXlp-MPHCxOl*DDQ{k;Z~yc+lI>hZxG86_N|%79a8-#yhQkUG?;R z-o_Y!*X0M3-xhp_OST-oP&C5R&33#(6BRtHH)tF18U6OFkhAFuTcCl)#Gz|SysIil z-1|goLrxggi{c#)sf2cLzG<}IAaSUW2azhHRFm;+#r{?By2#9DShMB|?WUxpkOodA z6?4E{%~4CGnEva%tml}E5#}Y-;#QisGdHeWo$@{_uHmdQFkgm}qzRVVuiTw>hoY6$ ztp>VoZ&LSkuU(c9k&QxBMZ@b7_E!owWJ zt3%QLC%rV zjIi{#$?0fq1K&6XhL|?d>L@_hfgh=vDe$^eu_INb<<@>o*Hux7f}+inwRH?YFI8Ki zFhbs1pCRGFeKK2YPH=fogrV7PHVu-L%xXzEa#WtE=Mb#;z-4%Ro6Z!sw4Q+|-+Z|V z5h3H(pf`8PyEb}G$zQOolaAQ?-YNC`<6)z>ZOAxVoOLfgGFeQw>qN#MHoI&D1dK&s zy(^BTc$%kykDkpkT3NHfN_ zQw^@(T8E6tPl8Um6`vK!Z79gAiR``4d79>GzrsLuWo-C_@fEmjBri2L5NCSW`)#1aS8rvm18eTc{=qrJ@WHnBg9MOS|H!afmS@&xuH@63=_!b6WApg=DfcWrlZ{ zoI2C2qfCaKl(HQIO3Ix5!)&ZvrwxVA-%%t`SDMWVZ&DhAX$i-Gyw2PHknsLSILufUs;z3uMDblX zcANkPTT3rsvyrR?j)6~{lC58rykR5Eeu<|9cBk7-R-zc=>oPITd^;AWSx5vM?yW`) z1wQR&XLAjvbBdHp#@*_4R|(7b(0=Sosiw~1OKnu7M1PGfF5inHIfoE~qG-g}pZ}9Z zK|k0SwCgtzJAV>-t5n{rt+&*p0oY6M{QfGYT0KA~3Db*U??_sGYW3R=dlc(v@T4aoVtix%nwD)TcIcr&JXTA_jw`p@tS)MnV4S4&A-);)S_1pQTsuS_Pm zcf;wQYlNTx?D8)|rMO8LC{|xDi!WpR)X=Dm@K%RLq|e?huZWM#ST&);Q`+L9%~IBC z0yMf9VPh*)!^m^}!IW{}v+h15QlLanJ>mQPYFPmwW45@`_WGWX zRX#O z=7Sypj2+R@CgHh`E_%&t%Cpd`jex`To@10sLVdo#tSfzvG)_e&cuG(~mWc1s_18 zZ)B>y^7SfKxSfXTCSDuqn<9nEaHX${g9`BQp^Av_mtaUp`bV(Q5%VZ4q|jv{`pQ=< zQ}Wj`i;)B9(06A`djJ&Lh8lJ<03GWCV>0E7C?W&3PRe)d=ri!Y;569g9odj!r#DaZ z6am%rsK%*pNcKP?^PPlob7!YHH@N0WF(JrZgZy`!>iYtsH3_3XZA85L^Ue*?Riq`< z0txZX*3{Hy>3<)O!+(C8T=bVPq$B)Xa7(i2ONK8*F?f)@$3q6-rK-Wp03`2qSju&S z77J`HGBg|g>Nb{yKPrs`%_0hOn}ohMoM0iWY#~0SeGorpS6%zJ@dL?03_B-A?(9?8UBK@ogFAHfrMdlke4fu+vg@3&wfL z+lJzHR0H4IVxf!zrIdaretzzMv!1~+;&(}#VK#VH{_R;$_ynNp_TlHV)1~5F?zjgu z)C8eVb+NDZ%$`FPmeFn{Lu!BTG#c4Y2Yqs~M9V^Qm6243Ldo6$3!p$+v*1Dc&h!LE z*eW7oM1}-p1J5h?0kK`|uGZ?8e&1Zsmr9R9+E+ghw*tg|Z22z!kX0wv$An?lM?Rc? zA26Dr`6bWi@baX@B=GCgZf1szJT>?aI1e#1M~kO=zmSR}{TW2ZKy8;xG;~(FahOFB z@9KfvBs7uu2rV>Rx}NVs1;rp|34>(pKc}stu^%S&`98d(o5g~17TOf?{}|1-)o9XL zi>~-=q&UpM@jKJFvU7_IzOS-tH;0b(jG*#|61(hi|4Q&SK|ZY0?v|@BD3^W-c#G#9 zRY7niBzY5)`7ER{5Uftup`^^sd=+r#&+z_}OC_DBqh<=DEHeX5Y^MZj7b75Wby{?3 zlj}uWsvSVx+2v&F#DE6mH~1PAjk;iTzU7%jv95kda@;cZJt>TfsgOO*ImASKXm@N0 z+KcsTV{Y>2J!uu9^e6yyQy_m6{=3tQT5Xt}R|00no zMpJj0Egz;P9SeUqS(94UQYL7}Qjm6^fd_#uMTVb})Iol-w9cg$bPm#&F>Mo^F7J>) zpjj3nfKd-v6#DU>5H4c9E43#aaAYZ!mqCd7*dIwU?DN4lb?9q_P{T*+I_Byortwua zmkMfQLT-M@lV83eExaD7dXCj$eL*`kWJ#oc2s&zata=hhl=tB)=9Obg_4b!HXwSU& z1}9$BQm9{27Kxd(O#VLQKm3U$N_A?;iGHN;jKPA5RXt9H7D6GcSF;5B@TN4v3iAhI z{5Kw_JD;E7gk@JXM0Xn|13$4T zznn5brw>x<76iy7{x(De{Pf)l96Y1zZgHfFkD>6=v0}4a<{WesyUyUGx0u0GEMFus zE|)*a*tLnc?tnIui?k(x!#ukxBIoRA4&YhT56vDrmWj5axNv)e?%cNy%1%l5o{9n8 zA&WxA$+1^16Yx?!o-?>Rd~-n70eD7jFb&#m`;EGx;v@7^spv{rP){&M#FEG* zK;ForgyMV`*BtI-0VPlx$C^y=>Ao4qjPFrQa!^=)3E8%Lk_D}GecqsMJK)Y&XODDR zR;eNg;cMYw*bTc*C`e$i-Tx^r6wtcWyZ_A8?_6fOE)Z7^YwNn|vgVb1^T3W8r{`F^ zq*OKx58bUG{tCT`|7S6S?gZUFgPuXBFB9)aLT*V0=_ZeWD5skfo+%uz?l)IBgOM=Q zRyhUB`{*l2pFm^_z& zurzkOT6O$~w?+vq>TYYg$t?nbXR?=_kkUdkv`58IRX(>x4XHg|>0f$2e0j2G{_|Tsiy?Ufx2LNsSx)l9 zc!bY3z$8|wUdO(9ap8_}c+l>OuostRB&fK*)aQh(S%Bo_!ZPs;yL?_gjPMJ@H8)O5 z4~rH-D`50k!}W~JqPh(+0WAY9Al*h+46ab#Q3LL^HHV&rYJ!PzyPNs? z`wq9l&Vkte2p-ivBz=Z)m2*~pfhpR$uP9d>#J!`+_=&1_u_HlolQ}PKUw$k{uF2Kf z;zHoDe;rWlhwTe+0pINPe}d;KcgMbfYKkV;AP#lRcQBmp$8xGj`;P5O_8tef zTA7aN$jE5vrD516~Q02?L!d`D54(PhU_~=1Z(EMj<(q#0l(5 z`x*yONeYa_!;GNL&_Q+^l@l-n3nc!i-~-zc-MJTS{k)=0j4J3E3(~=$Q+IBm(&=D< zod=iB{$6GL`iumMEpf5q?=I_eKHKh4Dg9Wx6R8vaYz_ww8^?;dSDSmLsNQCH=)^oHPW_Tjgw1Ky3^&_q| z=a(0+MRn~@+~)=xS%YsGu2)iwry;X+XJPR)o4nJ%&UtCLl8JISt4mRt89SuUv5PA; zt9x?HE8k0H*ZUg$$g}>_I}~3xKSa{R%uR1j3l}l}Xkuo8T*sd8yCf?4&wsPaMg@mT zWSj({&~$KAXnrw|Wgh&oLcma#dtrrv;j8e}V4I?k0{y4&*|vturgT=famh|)zsMK} zUrtmgEqtMHoxKtjgZ!MFJ5n!XJj>Fn|+-2d*O zPFJoz=K-<_YgW%GSeO-O^3k z+0n(>$ZTpR zh{&keWZWkISNT69{D1a@+YSVJOzZz4*8Yf~$6yl>_*bOy{11`F`)8!70^xxp`RMsh zkKyrG+-S;6{qF%$mFBIlH9*p_mCC@t@IeBl5W0x+OUExZA4uf@Wk^srdyY=$C8KS^ zS*l+fQW?$CL?S^#T$d(WcPaZyJK3|&M0#TgXUKU;{4ic{Nzgm!axlYvI6K4t&=5iU zeG`EqNja*-M~FNH6-9^Yb0sUSTqG15L<)7JX2aa)t8fw#(S+OygPN1ZS=v$GX@=mV z+{Oc8*(rdB8bN_*LUrT2&ul_GLU79o!x$xkf~dL^53@W{2KIZDOiePNg;wY|+HB79 zge24IL~^ejnp6%ELh`m@H6IlMF&&o|7l>+3gEia%v>DT#U;zAm1T_>}cTVWDwEhuV zX@@b(bI$zimpC{rOW5q~cNLnI&d(i*Bd!SwOarUKI`g;UpxGC0=^$Bfh+eZg1qqIU z3Y~LC!&i)A>2{!M4kgA5`^>%(r;a>K*F0m-+GC3xVzvc%xEAUDe7eb9QQ_Eq1p&V*w`qUdQB1I=TK8T{GdVbs zVnVtEvQES6Fw}9^WNJ2|SDB@CLvGbm$)&p`t^!j)*bM8KRAnNWF!E|4)UiU^ZpwGK zR+*e(g-0>In6i{oG`+#H>*(fPz^=L(Y-&CHEP-|G5ROEcBtEk-9uYSAFcv!=Y%OB7 z?;C>#piKH$Itzg7F*Y{X*>YrK2f=`pXvX>>22xZsNdL!(uT$l;` zDSCqq!qD;1LlK_#o2mdBBvA1hFZnJyS~_#_IslppxF{W73OpiolB4ty(_{%x?orK3 z>7SC5=5G@R8?roz(NR&_V~r8DeU8S@SQa`0`JB6o{PK9B;ZL$+q*zGX}i+gLE;^ed+Tbv{S%<{I!;H7 zXKmCFcO%D6O}5OuG)VjVYx#V$s#PAd@P4AFZ#K=it0xj20C5(!KBAF#@fR=z8oTfZ z144rks@`GqlG+%3tSR)F8O2JBmQX5YpZ?b{GsiLcR#|pkF2Z3Yz5?F8cq~S!nwh8N zMdQW7#Zp9NFr}ms?tDDZCDo!@JDF?H^mo^%#^51*cp~#Us(q?ugJhpeLSckfYu3eI z=(}U3mCN`Di;Bp|$ei!%(XR%nd@&HJthKE)un^Wz5J*#x*T?21{UM_X%cQczVywa) z22FWYJ)ysO~>Ew+z(zSa>M^6aX_n<;IvM4LfoS+TpqRaeskMrg6dqpYgFGu zI5Tco?sQbjgSZ)^O6u}G&Nk~Aj~~{)jPd(GvW{dhi4}C#>_8t#62)k4n@(f^BR1dA zC)SPR?@DP8@iY^iAnh-cb!ksV9eH{*dnMIMv=n;S^Yn=K3g;GMG3EgXR`6qZ zn#3F>t;A6Db~7PbVWV)N@O@#!(d;j}K!&#i9QA-%95H{zb?g%nH&)-LpL6Gx4uInH zml`ZYRM9v+SplAGs+j?6uwzJ3%Ta!%QdKOVRioKbddKIP2^h*EQU0FK`OdLma&&l%dTf!Mo&71h zKZ}|M2`flo-f-t}_Dj#KqYSw&byZ6XeN4H)Amd7{SjSL7*hiiFDk}MPZda~8>ey#p2>T+9K2Y_ zt;;lT>pkfui5KO&Xs0f>t|F}xRC7*SByJN6M7~oGjJmhF8@~}n!wx$~WyWsC z$%qh(pzAu1V8$`7Yn-{?S$WLO2ioIUSJrq zXj9BmtSNiwv9tOnf!M9W}F|YYuoL@=9=bU4j*Sd-q6RMk}*^3@gKx-E=(;6 zOn;oZh@$$=^|iXAdh}q(XZ(0zZ8}a|{64ZRCNMRy@eUacCo&2bfIEbHNLfYY8%G=` zm_w#Ys~VG?nY}3d6M~if>DiK}Y4dZp7YAdTCkvB9Iy*17=Ju_2Qdd7t?#Hzh`zQOC z{RTWqk`L~qizkT(4ffJ{Ai7B`@ER4m?P$pN0gT% zmeVfRZn(s@R1(A#6!2vs2y|O@%X@qI`wK?xlLS;soGYSj@+M#{KROmB4jZoBlPUB# ztVM!bNH>H*v{0Z>aDvgp$i|+Gl|#hAafT{REV8d`j&7KxB!}|7_=yb)8j1i>+8B-V2sX6DSrz&dY0M zJlHoAJTqDMWR+uW-PQ59{CIrTepQ?ig)y1YgDFnUFu_MiQBW~?BKf&uoZ4c}ydb~N zPDI(Oal>L_l@`$jGrz^N7+w!+iy?@ZmM4?<<_eLflGL~MWaoJWC*n__l()_NjcKI) zKl}ZMF4t))G`t?{CUvo z3F>zmQtGwpYyGgf&Uu&p+Rb`oY3qT%UNMXjCF7ua#q4pM3Bf7KULs`&J`(|t+? zy%irgAE@fT6JZV-UJ*GLaa~OLJms!89GFOx^sp-uK<#YwGvNKT@}~HU>%GRSZ*6$x;!lnZ0$4ars^q9UOgA zYK}K%Z$Hwy8N9>@;)_s z?PZ@{UVaXX2-t<^!NRzruyITKrp(%r_a}9>sz~7JU^4H%b=MCg2&vn_neW3XGswr- z=JU4i@eJdoa%|cwkf5RKXScH_Y2tAoTRsXJdbZ4Y46VI%?}Nq>P6yWHHRF4%&8rL# zegDL?w9`c8{re!t)?4`Si%*{ zFx5HkcJ1;<%3}9o^{25<$cIWxyRA#7o!8O^#@b73@v}6)#fe`}UAJDw{7ySHCTwrt z&O41gaUA!qI2;oH`dWmtpPDSerO2@71wxn}A16+*xBs#bEe66SzfVxQ@Rf7%hy>~b zGcCPz3f@Bu+QSkI*CztOvz9h)C8P?05;_?G{(y5BMekEdBaxFW$CivIo4UDPiU8#b zmw)>M8Ts%WF`p0l=#?<{$QMVGWq*E=-6iCF&x>Vo+IK3qVFw;e+rsP>Nj9&!9uVUf z+|d4$GXOuXPafAXykI_|zbONuzc2&%nqFF3%GBM`0tkLw+-L${{Vnr%yGK^xPZ~iT z&KbA^K`NFOwx&|fK0t#<;(=d?pNmJ3UjPW<19L$jLXcOF{DCK&3IGE^%C;8nKm*|4 zJ;PamznBa#-(MR1t9NNr4^szcYar;)+x)ex(zSGRw{>;`g1}sSk1WF9?fL&|&tmEY zZ|BD4;$+SGUl#@bzNlhq4ph?u>ceva_=H`}sEL5Y);K#AS0ckid;oB-Va{yQcp@Xv98 zxdjDy|1mcBI3Pmsnfq^T;UM3CVsP+3}q>7vmB951WE{ zg#JxG4>wa=2TQjqYXaYicVI}@UZ-|E&~rt5k5$_!#<&SNIXD~9>MivVB!N$ko&4>&ge`Oc)tu)#Nb$ EAF9JN%m4rY diff --git a/elastic/testkit/src/test/resources/avatar.png b/elastic/testkit/src/test/resources/avatar.png deleted file mode 100644 index a11b4dcd25e6213a2efd288aca42a124cc1c4f95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 106122 zcmZ^~1ymhD(;$ij2*HB81PFxS?iSnw1b25X?(VL^U4py2y9O=}7k78RmnGl-@4kKe z-p)C5&P;W8*;IE`ch!Xdl$SvHMDPg;3JOI^Qd9{F3fkxW_WcO|4v`PZ*MNdTrm+wa z`6(qLLiW=EU}|A)0tF=*o}`ALt~`p9t)(Po?*CB|u`_(OeC{v#M@ek!Aj-I(KV(0C zclq^8pT0!BCcm6Kq6#iOAPQGbB?PNK4Up#&6ko8PCA9>4KKDHtO|a_HZ^4}yCjIdhb~ z^uWHwY4Dp$!y^nn8Wbh`Hd|Lz3hb2ul$~p~XgUH^9^vt1@tzbJ)sJK$iBE_>tn+&C zzn_-&SbQfcZ3vGw5y|`nl{T~|%?kBX?b2}Xh9#HuPAQCd&?_EqTRaTx_Je_Rd-`+i z$mnRWdOy5jM#IIc@oT@?N}K`%r_sl92gOg^>XeDbj%Kepdyg9liS(pV2B?H`-%w&z zb}LG>RBeM`fMf!U@{tiC>V(@T}eqS!wPUB!DR~P zpuqJ#QerFIN_Tx@{$%JJb}?SJO%89Zi}i6?F_1&ay&{11b&@%b0VP^xSs#bHuD62; zkQau@?niMGbhTAhyJSqFHY$gvCoBiMU00U2^oT;@q{BIJ?=)fP*FgL zt2&)jiy47Y_%a2JPDbc6o>VX(GK0~JS%MD6%0g&0?Jqifks6-Gn?n}~>m9H&{5FHb z*#ovC<--tw)ps1?x33n~-8e;5K(q;x2qFw}>C2+Y|8ayip`lM_9!eyFP$%;uz`ORz z!?{1uPVbQPc_^&~+O9{(pipRybjT%e<)U>glm((X4y}hJ@kttA95Hw4bwmgx53(7F z%vai2{4q#tQ=)mAmrZ)3@2e&#;3e^D9!}qjYB0G(a|*K%HV=*C0#iP$S!48xkBu|? z+rf~2gZkGTxh&>p=^DRY{S|b$FHn~`N#$&w4cuxU~nWR z2)P6HP7go0Dh{q!uV}8bBzf}mbv(+cJwick>_Q-rAwZjC%RZ_gg5L^Q&P)dd3d{v{ zfP1w&>y?D6#`p1(R1CHX%Ud7$fq^hJp4ARPfD9_gkC}{MAkdfze?jBpK`E9jY<-W{ z&kxc)pK9Q(p|pBjY6wqZ5rm1&KNR^Th4 zFnoz(DvWwNG>9x-k}e^tkQ_e@FP@@44o(tLDa2Z+>5Iw;U_@~j%K_R#w&&s|00Hb@C@Me7r|2)A(N%RLLWoqMU?nxwzYCZ*+GF3E}PFj z16$Q;Y)EBKRr|Xoyd}piVM*Le_VjDKzivFKT8 zr!;NuY0i0$;8^3>`&f&eot>4PvC-a=ywTnAWDd0?OYP+6izKjUd1k30oQ>NC-v;-L z@(lHiK1;qiAK|a^q5G}L?e}MkXOU+F=a6Sf=NKM6?<$XAkIHA2J2e6<6q#tD=urYV zf<1yzHU=vvDML#iz= zrjaHy_k{Jn!@lso+Y2(zNdwvbb2l8rkPjrtB4j)vH)WDg?aiyn8&QAU@-k3a@d{J3ZnNWeF zHLtcUTS8<%?!W)}Fz({#*?L2m@4<;O|?7Gf>xFR~IohV&gugtHjY_=`mH4*mPRIC~{Q#CW5lbkC( z5IiuTA;tcRjV5X4bLKPRli{ECMDGab*!N)f@V<3pg11F~YOC6v7wF z>!-`7iM{G}>UQ>aR$(rZKSo00Li0@X@bl*L_zi3gSo`HeF#9O_)Iv74@$B6)zspbK z#WL3qXNKpB4TxEXxnWu0(nMS1MaB@by38-VC?+dzm$aFGo($K;29pi(m|9L;?1b$Y z4yiEPmJKs8v$L_|Y5B4B9FZxhN$&3pIA-C~y95XF(>94qG7*$#%ALhkX)6@b zap%9k)JfGn^AWp^*v7Ap;EwWMgRg0?1!ZM2UCi4SoUU$@@m*Mxr`%LOYhIqndQkyP zfpkEP8#Ct#)XYvBR2FI*d7$=9E7F0>6jDaBr+~B1!mHpXVx;j;Z@O^}xdwSNeeOu4 zjGwfA=@jZx(zi7(nO!A+eZ-haSop&rv&ab6oM7xy-&R-By>9)j=5d8vthWH3?WXhA zIB|H`%TW8Q7N)kQebnvXnx?8Dyu71gq&8n=TFIaben_XWV))&j4A79#q19XMVsOv_ zdqm#RF8HpKt=Scjml(c@_eaZcc5Mu7I5|7^?5ERJQ&(73D^vk^dG^hf z0~Pw?HzXikXigx)Ag}cTNrBTYU8AWU$UVg~!9qZ0iLv}Qm zCq($F4d~eP-Z~VUj&x30W1MelX(W2;KZ~WPJg;m_;b&OXyK0;L%h7iB zsPa}`Tm`4c>zd^Vo{cUcwBf|GUGHMQQhl+3oE^Bgxcf16>45G1-{uX5O zQh#Dx^*sZ)0t6S(*JiH2ozHX*cMLP;jAVPAYd`*mG`yyb)|VYjO%t~NXs_x>z0AM) zvumQ~I+LU6YwKllb9|`>?b~=8b#dfH?+tn@x~+t)>iD)pKy!q^mrI?g_O6FP(p$k( z$l+7%lk#k_nKw`%QK0(8@a4~z)xhElm>UensQO*b_esAd@8}4sV;OE5aTJQFs^_Nv z!#qfqChS+<-a}G7vld_FBC4kriy;QL;ym6^z{~;C)sw8s!C*fy}w#fQ1 zT$FY7kEG8p8wAiomQb6K4c*-UW#6|H;Q z0bu|XBm2I0K(v$8aD;-w`TFmMmQtd;go1+Mu=u6!q%J4RV+62aFfaxfnlQN8*uD3L zg5q=Id4IGqaWWutv$3{yA@GUz2C)wcN(f3yRQQ)0^l9f);#Vn$4P21(hJ@P2!P@Zo)D#1bpXBEcVxRIq z9x|m#&7nQ(6m^Tf`5ChTH^?I3M@9XR0?2A9YwCwjPfw?&rsjeC-Ww7iv*s?+*xQK_ zuP5EkG7lA<`MdlV6>)JlH=?7h_Y2*Aw^ci?os*Z;Rxk}k0Vk#Zd5AT@a_vUHM>^pv z=0v>R?nl9%GX=ReFt(`1Fv3g!z^2gQ%=k>ll4|>BTKw_M`6azm4O|>dG0|H7-RVnP zK0yqQY<{FzoRq%V6>+y@6X2-o(FH#NyoYDsfRG;ET;he}FXxks6C`kFXNIZR(>bV< z(B~eQ6Ef}dvU=HzKpMo`3gtdNDj{=xKCeP|9?!0~fA=6KoB$#ZXWm4UJH(39rv&deS3 zR1_!@h<{!nezUi>v%0dPsjj7kMyT<0qF-jf|Ew$0*O}Nzt#fj;ye6+7EnMuKA+*ob#6Q#{Sy>w%v`4CWhu}3qtn2GJE^} z`rMD!Kd=+?-%DM+YCn0&}WOrOVwd80NAu0OF|^#~y>8sdN3Q6+v#P@8&0_p2K!FsdZ5Mfq+S znO`udEh*uV3TaCfgvHz2nD7sEsIIPNZy@Hn+XyPN^ik>9<>%z24&f(_`8LU#v=jCL zwQKbq69YYCaP&MjB4%fvTHq_XcWmo$Y#-=kv9HoS2&(p&4w zrdYD4BFw?w%keaZTqm%7DSWxwnL5c9>#{(gw8!-jS zZW{XPud=h8rub=$OkA5y+g|!B(ggaWp)5?BsSS`0{bv*HJ7;4*O-?CNi*9i57`&oU zRJ0onu-^JopQ9fChV<`)I|mn*_}8n46T&;|#H}GO&c2l)k5n!fx`|94?ES$*!KXvq zt$xxsofi8&D=y8{wCovcfFx^x+1Ak-kcmwE4RQhY;d&dhG7wdIlTY($1+@dR%%O2F zJwZUkLLH))b~yb8$7PVbYi5<|)@W_p z;srpvLH5TsvJQZQm*kpcn7^Y-!)wV$1>!~DYAIg`Ic{Jcv3b|Z%uLa}wrVtHLbv?_D3pdojhaPm;3+CCOxkfoHnS{G6yS?` zbD0#{e8YTpFZ5<~Wrv$+9m}QfoXy?dTBNhVPU$Xq6VFmSFlQVas30p#InFi2yCY4w zowTkfi@0WCzLbzB!592aHLF6rVJrs&<-}ag9te?u-D)y76}u4898Sy5WFZia{y+r# zp_~bvpTfEBPTldc{<4~F^J{$8dm{_ykOcy=Sj3IN*8Q~Omv__7PztHS0r)z396L`f*jjeT@W-)Mb6yG3 zoYJd)61uN?;_sYBII!@FNCWmqGJ0dvHp-k{$J61a9;Sm;n89YqMEQwb>poh1&95xZ zq=9mo_U2o9P7RoyKJ|6Z{k)4XIzt<+O>1VUHBpH{Q_bS9F;hr)IbF zkDd%Dfy&B7Bt0w$^HZ09Ynb8jJ}|Ikag?;TG_f0PsWOkNj0@Ab$4vcQs8spBj-bZP@}o-%tl6`LRwHpZ{2c~KI<#8 zbcmJ7WVV-5;cb@>%IV>o)e!oMfr-r^jw>!MuI$%g!@EM$0HZ`8)luTY5zHN_8?8_H2p;>cwiE6j5J6ujh=7AV8Hj1>)Py<>Qr+ zmY*nsHBv*u`jEu1O&(4Wpv{08{$g(O69F#K;LxDDel{d<^z%OQM!)-c6YG#(|H$$6 z-F45ddRU`F^Eaabo#cd4YO3N*rn^y>a>=xM2QAOuiosJ*W@FXsWM72GAkUM2h&$EZ zS^sx<3&By=K{1a7-4Q~;zyw6tk~mS))R7|D4IoMU-1PQMo9RbApXZDkx*fj_aG)St zn?vfkqi>^7%GE{nMQy<>nPS1PPW-^#*0e84*W>HW<;5FVFSNSrb#{*5X|49k{$!%W z{(10G;%nNUtX6EG>kv#qLr!Y8R1)9dH)}FKB-<23+xq(w*}qB6-#sG;NECDyOt!^6 z70*>oVxk0hrhbTe99yaKm9;m^qz^B=qI0_GFZ@Qd$ZkWz!-p{#J&+g&}D>)c2uzYfMcq2;h71vK3}t zu0f!%>fM$4Z@%CH&)XAdC$=MStVq{dP4?6De7!X?>dq~W={>EN4eLikVxlytUfAO< z4W3_*wT-EQMFCMdQIsq`c;0MUqJ6{AZ-$vI6mNrm+G6u%L{B|QlRsBznpyP$n8P#j zE2<_}y<@8!Q{;y2P0}et?rD#ZrRbO3tu0eqnbihnu7r@bJCd`;X!nmF56_ji$R-H| zo%J{OZWt{TMks>8&;2=}n8njUHz~T|JidS)%kA?QBny9r9p5jL>#ed`S2g%n8p>yA zKCe5Mql~`j0*~$8td4S&*TT%;qf(1ii^w3fe$Ei1*E(_~a(2^g#)*$qe!F-%ib?%H zY<8~{$4<3vnnDq?6IhF+oy+hns={xckXdC0U@Ey!zD= zG9|qzQa7vR?9zU%HT;fKub+8fJEb>(g4Fu>Z@NWg7vn6X=JZ2t5)5f|=+&W2tv&!+`)X1_L=O4yOi<|h!-uSjZKHZXIdFNOVfzogJ(IcGm36j@FRECPkvCffId5#Qq1}%+c3KA|6Vp?{if5Tb#Kb_2 z?>h#Pgf;KZbkj5o&$J)Sf=hb9Uifc6uhy|n501O9@_;I*DPv|*P@$edoE+x&K1!20 z%I61dSOgxo*+9qQm}P8auW~MMJCj>{@?FHrwK0ZZR0nbBScsel-EkdbZq}qxOp42o zkz82?Q?q_fBs~ItU9Yn{JIK7+4>Yqgb?#5)Wba0QZiCc;6_q2G*Ur-Dj5b%HvfrxTeKv8y3z73^)s8_PVXFDyTGi9zvD_q2qT+#YqD!5%{g}N4u=U z7lH%OdrRb4LM%?aW#m7=0*85q=#rbrk^sqQJl^aLNu{8B_!#D4ysl&5aoEQ(n~Z^X zJ30QPU>bNt#WV{TF=Q@X)yuZN7jcvU)2ng^$RsKZ4u z!OV%b)Upa%)LZ>*f0t8N0^o$f`VnU=*`RWb17^ZRtb!O;e5oO(6?!i7!Bl(?Hx!ag zqLosYA1lS$cJ{PpuS^-qs7#y7%vSBeD&4A6zxZt=Uzyw~9geN)iM}&R=sG*i(J9Q< z3H0m_sI2qgWsnUJDW@eLR!|eg(YlZp?=|Zc5MNP|;J5%gV+03u1azj<_n`!tGiI zAy1;iBHvfQ;uW}bnia*cvP;W&)9~Q4R|Z27O)B$51`lvLN~+-65|i|@f);wZZDt#| zeA(;^R@*ANPvY~kW6b3C?XB>sVL55*wDAv|d=v}ncnLY{tjLB0t^XeG`&4p_3F1!6Yu_V?g{=L^^G_$lNA%;IzN_ z9W9%I2IJr=i)Jfw00Uo}s6YSt1C6&xvFr*|^JMSjDh3g9a+Z_rm-%x}sF(``k^SX} zwLBGCPwV8Sh%;w6u21mHjB;>v{E=%kvb2RmkFf(gg5Mq@<^g(nQWiGzW>l?rFGC0D1X0v)_g9qgW^1EPrxKw;REGCsa?k3GOLv{Ati{`XR zUg6kg*o@KK>|yb-L=l#ftwBXptcvizS(^}!@;(;oObAoG-BqoaRAz3v4N`Q56r`J+ zHrC5mwM~v{1|(v%!X3@@BeY5Gm*7EK=(i2Sg+X>xSo)VGJ%4xlxgHy5L_Kvni4P+W1S)I26kcbc6{0uFYk zFFK5?>;5&9%ejrio3o~#{t|;3GQ~0B(+3Y*tjs`9AJX0o2(A`*u~DC~Z>5WTU7c>P^7h_m~NN>*;1Kn}L2c87YKyl%72 z_CfRA@wiso@P?$xkA7Vwg**cK54s^qpr8S0t3;)%TwXb12~H--9w;zfR!Z8Dvg=A{ zu*k87iMqwA^&%PscfTa@I#!7bHXRL)mo}c+W6QfJ(thc~Nc&{EW@FyAEF9&^Hk-V1 zqiq2mlP{(3RCIt3KpQef=bZ~O2(~x72)oaBn!eo--)TxK9DiK3F2O|NF)^#-nQtS{-Z0uD3+$g}@v38Q?Cb+WMZDINueb zMW|>pr=v%^dzBOQ%%f`45-~iT5V$Ife0gFVm)xqNo3GLVYr1ycE~Dzp3KzswVmH~Y zx%^W?GtkmA6W-#qbZYkPPJ%S#iCUq*=4^zx=7=RKCnoFEgXBT zehc+w2>0c>J@n)J_2ZX+4jSIC!p_+y#XONuvnH=Eul@bEX)yIN52fc5puaW>A_=!3 z>%?s!qD@&$;YQJJ({wqAUUwEI-r_=bHiiv@^HdCRgWe?}YUS28g;0%&kgz?&;Zf=5DW8b8`3 z87!LGY@0M8neT*0z=X;DUFA>ZhUv2pPF`zY0!5Y}kR%Dal;dr;`w7rLF_3DmYbiHr zO|Lap6|!V`uVvZJ;b-vp>X90uZlLQFzVgip=3NO=MyH&NQKS|*tZw_+0*zu_1>O>W zVE4izS|UTHu$-TH6F)v^f!g<$-I4GDu~3+eA7Sh@j@K}1%f-&vt6(qz{Du$DN`~-$;7$E!iJX22TJ{MG z#QSyfu!89|*5^6VPRHyt7A?~!yCU*7aCpOBn(6_(){M~CN zX0n2xnl81C&%e1gFXrz?dW1Wd%t`Qv8znusqgG?MT%G+tHH^MjXn%v>16UFrd)itibty%gSbbt zKdX9w2fq__m_GkRC$N`Yt#m98yl7Cq*fBlCvB{v{JpN){pnMy@V<~dn#0MwAy?upd z?<<5HYtn}X9B$3)LzkLKO93Ox++9h|Tc_j5GoGPoqf**>gGgm2$w{Sz&!rBHZ!r}T(*`beOL6RD3q2>z`Ygz1G~s6^8^9?>v`909~g zq(}o*B&8rAdr_=hu+eZ#u&&gx#GiIin|cNvYFOxjWmJDnGev3x`luJ#8@6N7U1g`D z`*Be4X;Y7jCRB9P@_L4tyyFns`N*yFbtRqinCu1>iv4~Z+K0VaGVh;@^)_tCe(2kfO9SbQX0K=WP2lAN=E_ab{e#rm_~Uwb3W9fKdwqXJT3enSXISfXhzZd$y0;n*DNOLG|1HLK zi?VYMVdq@MO-@NJyw*TeW7I=MBV9*J>3;lSrV7Hj>83HT`qFW|pyiUZlPmvl3p#}x zs;aJnYm4fxQd#-96Lsxrc>ng${hG2hbZ50<35gDKoUg#8Yvl=(+MNxq`^H;1Sn9g* z=ELQArR4U)d$9~I5b2+Aro91C0zzV)3sikM2t$G^=% zYhOLzlzGN;kk){pQ^pptd++=Ab$w9b!wiGPvAYIv3Q(mzPgY~Bgi@9vfKGF;C*%j7 zRep|dar$8T(bXvg;!NDoR5AE{nKG!k8=PhLI#+u6%zF`Grx3KrWeVAtfXM7#*qN&HW!4Hu$-P^D{_70WJYTGSwBJ-*_-@rUeWJPtZ z*yokh7faxE6d~_IvV*!^0bK4{LtVkAg;wI)GGO9lZLQpBp<_SG5_e!=|Bq`%6nnCy z9DYyvgH5y}PIIj-eB|Whls-%;O4p0EdFJ{>XY9h;>sMuhR8Hlk0*OWw9SjWI@`*2< z6?M>7pkKm^)m}D1vhgtvr-h|rjskB0T`e?JWA#%Acc zZ|ik)FH=b6ffXbAy$yUl_PgyZWEgYu)0^RT4=b5aki^{%F^pH0yy(Gpb~}m< zVA#sCplqhzY|Yz6OJ6}t2#W@0k2DH+Up3+}oOUE1fno2r&f}*u`vcEPTk@7_US?@K zxu)RQ*KAtv;{l@%i*Guk_b9QIWvnqG+k=y;;1lT@CylRI(Y*Swfw}$mt;LOJdjI7q z3k22MBrGXS$oCC#`FuJEAmjU%)1cl?#k-50md`&UY}HBF`SqyK!xKl$J|Vg=2b+OV z&oI<33>CX0vS<}X%N*MLZ)(>plSCtL4Cl;IbL4suDf7ae!BNEKGtvB^xo& zRyUR7a_ZFP@~^)C9%0HNpU17x!>{p=BJkL*PVj#@H8YH;`4&@I+-kn@QSj9Q8ntEv z(cYCw{2KB(b{anJbi?DoX0_E*b%9q;`ugqohOq(ua)1&)wac0OkCe_6ee}_DJN@Oh z7E~wS+mcjOql)rHYOgT4384A+rZ7EbnjMioTU%;iZMC@|k_0yquRD3>T(6D#fV~v& zW#(DnC@Zgfwbxav>2j>>Y)(qyG))ezW}p_8F0cPGrX6IKb}Li1oU!XUokNqPe)+*= z-1{@o8UKyJn?3BRFF0~5DiyN3E1H^_j*QkY`0CH$d1q;W3u6`+CQ5>|r6i*huhi1^ zXsg}~cb^ZZKg>nvwj{TTh>i9rn}92Dfg7Yk%)9Xt8Y=r^n7~%u)96qn)2y+i{W}T2 zo4)GeP}@H}Ois7D8!Yek_TVF!>3cWN#Lf=NWhmPR*LAwS zv*oK;ly|_WT?2#*bs9Lmdr!70_+BU4J(Z6e$l{h2HVvotJ;C6E#;c}`3?~DseLlM* zd??qcvd0A*m_t+gOl?_wt!P+96>#ghwP^D~CrRSDs|0jYV@^48 zoV5IzOeB3fEp|ybhz4}c#G^XaC3|8lP|lNPq}JWB-S<@J6IA-|{eIpqbu{a5-|*Jk z2LeJBvv%F_7@Y>V6+ha zp|73WZaj9*{)h6fwP8|UB2}?4Nv7!+<0`6pc1n*_fuL#fYG$8c^r=nzLh5I!4S5a! zlQvtVQ)wj0=NizC{EpUyD5cH23kX~O<1H9bnB0>@4I9WKiq}k`Al~=B{n6Cl1GkLM`sVY`DPhP?)!tk27M_iz zuFW$&`~WUwch`&QOJI&uKI}=5v})o*=taSh`7)Q3xz@_j{<=quU5|I|Ng~ z{C-rO4Qyj|JIv^U@n*%WzV;qhtJlAuw9{WLczLMM(~0jL2*9x&c3MMxeld`Gyk&JQ zH2SGA@5ju+M{Fve`H^}m6P4Q75>#D4f?Cg86O|L&^d+Fz_~gkk3-rZC{cD(E?m%-?Ar`b4&uc z@#Qb><+y;9=}2%aYL2F?v|Ws_mSHg%v*@>b1jXYub)NRKsAh z*-g$xJR%ME{Z`M6)5G<*5)B61ahTg+ySjddnopWOou+MXQEA09m5lf-}&F^Zdc* z;AN|_dE-+zWwVo^`GDXTNE?I({3gj+!6L*IULK5;u=^l1O#-G8QO zD9+&rdUCffGrL2``FDqG8%YCt&Y7?S6%V(gz;y0BZmdbrmeoJ}4be719tlz_4D=!mahUbxWbq zCKJ>y)0Sr?hfrrxMb{QOvNMg(_rWSlSOIO-7Tk8!Mqi_+)$@`KbmK{7`t}Q-RTiQ# z%?jLMtv8)%N}G#e3au0XKIYBL*vWc;Sr4YIs8E5i3yv?1I`?(jp~+VlrWCv)f{Xzf z3$-X_4da*Ki*8b%$AdR2?Wg0qQoqmcT=jZb=~B;Y4Sy(Xm=7kt=7Q)bY+ox z4iyK54yqjfP%Z!x8X3?C@rdHCvKD&(zM1tY>5QUOzZd8`5%nMLy0v`E0ITWxQe@6j zNuPIBWysBKaU&bNhjx*A_@z0eUy+pgHHEs|w!GBmQAJoE=0JJV>Ut$p)^gi3g*}7$ zI&^xt&hI28k;0m8;I@ONr8%L{~e>kVB0M(y<&nNf7Uh2c`+gNOHw zz*Sil6k^q+xDK_J zJx`ndkl8Rj1-gKk!=YqrFXop2MEP{|`T@#m7eMsCUdL(Rrk~5-zFC~fdfTgeI~aP5 zxg5pBL~G@&z?6wVue?WXuMNhp0s!~~=S-v%w|S{E<~hii#mAHz3V z6c*fmSwBx;h*w(yw#$ZU=ZkvY zCb$(w1jnj;RJGr-STB=KPf1*E9JpT!h7}qk<04gCqB>g=7wfN4w-)$G*54zZZ4F$*{B0mm&u4bLV^gxufrcA7FiBl5Am#?{xji z|H{Z?2-zQY@v@$_Rw6}gHTgCwJfV@*i;~Ft@rw(!+xA0X!Ooo}*~vVn?--;sB5j!l zt27cdl?T2Kd*WBA)>nq$lV>znq*T1$cSskQK73N3m~~65w3@HHV0PjeI|Z2K?bm=i z5e92+=X>pxD>Z_O@kd&e*<)up2^z=xX<)HJG&-tG!lwQ=L7cYVs>rnp2RF#aOD2js zzkjoq4sDUyiARiF?&Q}#4GPse>mh*Vslk)HB+kO#d$YJ%wm)5wVv$jT6m7gofA1-( z64GMY!CFVnxYd*iqrzXreDU+Yfb4J^K?an~NTCc7ip!o;G~82jf*8~4?+fJ@n%38m z)5Wn=wVuL8y2S1cnfC;#bVX-t@8?J2Hd;GDEugkCJ^{)K?!#>^5(_+`Dc1-&V!Ba|V*g#PEX5 z9OS;;c;MVA_C-ha46_DY=n%n@%4_h^xde7n*UD;nLGk>bg?5m_l2IdX)yb}17Y5X(U z4SR(vywVL!f*@9_D+%YAL7queaz7l!JQYikRQ zhCzl)@C@ptv_YS^%k!68w5(Jp(YM@d`tL$oOqhBp=@JN8OIH&T!37FzYt;MG7TLA2 zGtJp){Jv@j)dH)hMo#^NT7JQxjraUii~U)f6ovY3!$n+Qt^u>}NmQCB1$nfRLll5? zW9kcqUbw7MO_fy%qz?BVrw*f2{C zmpi&OJLhTe5lH#7(3;Qrac&mjZf_#DM%RA0JsLBtxnFXvxj$0!slJ}V99R9;`U7D( z&uZ?QV%jzGJcrgh8Z3McpV?yKVWAWktbleVn~;qTwh!)f7nb-CxeV+=&JC(PFw)%U zgAb}eC#I-g6Ky0neS0iDkIR~mC$k(DLrHh!!7$d9eLJpvS(sY9;QPsClLEf&X|B%v zK2#BkqRM5oOE96oe~Z2HM_u16JuHWRZ)0mhy5uEP|En%?`TfV)WJrdJ~6m{ zHlQnFb(f+GpddmZ>9`t&`ux==)|IdQuEC>xf+>AR8PkwOWNSNZ$3PnyQJj^B&s>%? z2^HB=76+rmZI&l_EP0+5W!!X5Bd%GlqPuf+e3@3VR3{u(#gX*)j^hc!^_DJvX3 z_E#iU`)`ot{uu%?avZ*x%v4;`hr8JXQ|!WWD?sXLi77V|$c0GBG6xUIhMT2ur!g9p zDralBzH2~T&wW$%VfS^xcl9*tNxkj$EX?Ba&n)g6@FSN>#j^)1P+h> zzajEo^HGU_VLA(yl-r7uX!B~d>8xM;YECoL0V{6o6H~)3o$d;_A1r#j`FH{Ku%;;N zIW<|wu2@55#Q}EOqt7>Pq|T=@@mI}bQ7#wC?X$Qx@Kakhhr}wJ?S4Ao``%7Z7laG% zI}qpklWNQD5Fz~Pr;BltMcy)+10Uy;XE#ZoD$Mg=+|lZy-xO0ixmEGlW#76OWV#&> zD`{NO!NEYc;(4oDK zLa-A9cwcJIE@#4q*sjV9Qt{V1ZbnR11fO=t?Jl-}c2Cd5C{2pQ#@-WfpG9hheM>Ml zbOrHz+r;POrQ+7PuQ<-spM$KF1hSYD?<*-x z2SBPJ#P9(}6wA}HF~MrnImpU86iRy&VHU_Tv!tv?bMq+p081U1`9D+hp~>!eXUUwN z*%}=I(}GU14CGuzb@>=+*-<>crtl1Tbum43269Ra7`}b?(t~3+iX{TU@DV05YHD7T zK6o4x9WmRpZHlnRGpn>Z9a676mKmAYAq_Fcu>%SjF&6it>WUUngt|uh_neco(PV4s zvLt{ubqVktRImE;DO%s;)scNht0Fue#1o*~usk2#j@B$eg| zmZN7CQN+7aEskXN&S%aUEHOEqkAg4F1c6Bri_VAO^D6IpToA2pQ4w2s}$kdMt#TZ({&{GCTx%@LY z{|xUlIof@%KQvs2@eh3Isbf8qU(d+jVy~bSPA|CqBnFDb%zPiakZ&uPGr2en0;&5D zEgKcO+DNKEKKkElG&&0~3^}kBrV2acSiYGx?{Geo_qz^f{!B}kD0b3)dp@yKPN{iM zV{q`Xw3#^{tLwNRDqeI@mV)q4rtA)2hyM+_i;ux^a{OUraCo&dytUrhz6IQP zrQ-8>94T7zR*GdFyp0i-=qt(*?(5@_0L`TTiti26f0V42bd`L$=ys+dN|X+O)9MRn z#8brn;iYw49DaL@N3I&#ZdBlfc?((Exz)-lvo*-HcEm1pC*UZs(;wetjBtScK0 z1*In%XV0vRtR_lX7h!-p8peP*LPVO1k=3L31nnE)Dv?Jx!-Jd9S)w+QWG6Nw`g+8k zKVN5N<00K=0%2n3<40qzQjH!fr8iK=gXD=S=b!sl3fjD2yWP@5k1titWPgd+BxrA? z2l3_NLo1fKdQc8v2+r)(bpXO)FMkmAi){O_5!?73&aIyej)`jS|CLz!++-*?dEUJL z-BXV@QWp^@(!kw>o?9=r#KQ404Q^0M?(A*-1rQur92?5Xn2k!^*uzCPj)?>3x~si5 z5w|NVcp-S@xmRstLS>G@^@W4d_WDklO9tS(vgK)$^quw>Jnb63hk~}d+%_9o@mBjF$a{&&K+U%wDuJ$uruVj%V%yOOv`ZV3sySHYXI9*4~eRC%p0W~%|A zK1D3vriE#ic(cM)>YssI=w=m4lgeL!rxC5nRitpJIP1#hUTO4p+D%AdmLuBn`qrhh z;ncB7(b9R-{eSSQFT!V6Zkh@0L16ygKIzkD@v`jU4(zevH!j?ntF>SS%c`2i}>j-U+L1wa9L9-{X z5D|yIA?dU0_MEJC(B;1&^$r_*wzq&=EqcmLVqCsSD~W>0qbqzKQVplvF)6SKk~~3% zqk^jPD(DnW{)Pd{5vFh+w%JqVsw49`-iK<1((-=@W<19&Dm*$xo6CrWa zVx^dAi*0Zj)DqAQM@c|KBN8ZBD|+x`);4~z%bHDaHUlWNcbfVB^i$joEbp|_7olx0 z9Q>kB{j)balEU&#$!9MLQIkiP@h6RfM1Zva=;4F#SAY3$!{?uW9_DA~MEsnmQtZ+- zRgf$mY(clZZkXiKIWI)T&d@1!(toIpSm^c79G|icS{-Hj#+(vrh4b^81huYJ0a);& zBCMP?3)iC&qA?_<^)$`miyVsli?EVU%po#X;i^N|!0bHl5*AAu@sz{MZ=@Ae_K0zw zXt@4fR%_~GCFGF-lPCvKqA069+`M%-VO` z9e?%(Z9+r-~x{7FMW5ZLSgR{H(f*{8N>%XaXQAxZpFslXo~n4EaD@b#h`CYvMb zh~Jfc{vZC%yW!H=<8~$tI{htoh-Q^&u;NJoof)=8)CI5!Thp-n?&T1vxX`UM}sYGNCK$ch$49Hp4<%EcNDQ zXU)22Gx(P|zRUQtuM1JIP>LTe zNsbpJ>2vN*el!38J9V{;En`L%h8FeOJIxTJg)6@i-WU>titxhccp5`;&R&Pl!U~34 zs+Uml-E)@?Hl=Vu7#MlWqlXPP@}OdfuC82JRGSaS;285fV`d2!hwv0h)E2!68d&*Y z>N%zLr27wN?S$0_x@&=*zI&nb=Nm-9D+*k84@UYen$=0oV6zgAZkB!@=FiUF!|b|@ zUtb@BLbw0)>67rIAN`GvXZX39X~Oi;?Yo2M7#!?89`92gxra{{bJ>*jU;Tscg*Pso zGIh_6ZS?o5{B7|d0p(1rS%PNaEh@4ZlkoVhd|Qgq3O_$ddt8eG$e z2FO+cpoaN#INYB|=l_Sl^G^7`{_@8<^i4b4FJHHN7IstZK~bo&o<`X)y-a0K(9&h6&Bec!MJOq4jH)KN%q zdrKDR@L9}_XXQ_t-99HZc1*oftxm>BO1w>Bwfv=;whGmds2q?>*%n-mGt|zQEBQ1) zHi64=q?IADzvqZNpvcJD*Mluhe6dBlJRR^UP?lct)U74WIF2kiRS?N-*^$Ii)fHvS z2$KY~f#H^g3UtW+5LAp@oN`M$e-Vf{BD$0M^VsL&Iap>)L8Fu^nG~>zmv(nN3>wcQ z`SX65-Jxgv5C8;q^X3iPtmWpL<0E5s+$#&Hz(6OqpsSmsjIdu2-JH-7zt~oPOSbwK zw9tnA{;o0otYA$}u2KLM)FoBaf@VDV0MxL=1@~@i;4jTVYkovy)e7j`gG; zeCxHquHW1DA8Y6Dk#JYXK69g5QKp^bLd$T{rWTM5=vB=ih;W+^QwuT&P^>ODcXQ!nZ( z{r6IBjxH@%>aV736$fk@pS433#Wvot`R8s2?i;uunxuc&aoMu?G3k68I2)3v=+^f% zkRo_tW0BMQHmc%=8I<)fyY`i2FKH_w&9l(+#pj=eAN|eWn965BmOKLk16n|h*R)Y} zh|dVH)QR@4QIM`URr8WgQvCbh<2Z)X9cKDX>Qd9WYJ!?1ZV{Xz*|ZZo0Pv-Z^->o` zpr^6yONq(_a=7ZVoKZ|;FbS^23qU?=@f;kdzx%V2E(X^Isv6bs>U);jgR9S$d3xmb zpOg=4pA$Oo|4;wucfx=7kAGLUf1R+~+ZxNh{d_p`oPi{D4egyqHu2Nf*QDp?9$pS$ zJk0J;OZ>+4MF2}$*nKF8|MsohVMEhObeHJOebbhydXV^a0mzHs``*D$XLsPTOP9;g znE`avr9x1KIt?PP7+jYxVO8iRrOT4%SaMGV9>$l%S!Yk)bLa*Jna9XePQY7wE|9$g z-hd;IAvUR+!YxX5t$AaLJFA{=Ssf^T9n+_ex@$J*0JADJ)Z*eo*Q&=G>o7hc zgjw12K6r3H+>(y}#*OP?dit2`<3~)*<81;>ZAiC|+1iV6n9{MB`zQbKcf#ARoRwa` zQ^zs1u1)MpDW^#Nb!m(E!so0k4XI0;ggFz=lGJr{gLb$iY4j#3@@(-m)(H3<#1dz3 z#z>++riyBK(Rq4eJg+KL3R8`1m97qU>tP5; zT&a0dy8b^EjlQS7C7kQSqVHZhX5`NF{h8*E3HH+`k9B2gpB>be#vL~Qp;q-UePlDo zy}Nf!#RK7ZFHBg6Xd<)ZkzO$G(0fEFQKIV8=9!W3%GnbpS#@-iR$KLe;WT49DS|Rw z`7MR(kiVTGbttQXfrD@}M!QL-TS&g6D(?CRDC%iv{OCqE&iLVQw>NZV6g$e}(IExg zPJIlL=;^H=CXK_pt7DtxVa<`j?R7tHR;CvfeyCMF5Xu7`!uE|0WJB_ISJy8$Fag`+ z1tFxCJ}us|po_$hw9lbg%m8-BQS+oXStfc{aBLIes;yIgj{+{sB8~dz2y)T$MT}ICBhe996CYX<>$_5)sQzJGdk7;w>*1 zVO}hYuEHrg<(RHm`V7E9(>s=WDsAIqD+Df&E#;H1jou zY>229K6|fepgd{rspMWoWzo%miBtlqlJcxft>kaD$SN6b@0CFxwai5_ZX3d z$43duK^?;2D{x6z3WmRS7Ue*CmhvJzTSp*Uk0W?D9qrsNl@Ag>rp6zCe%1QVqo?!I z`BxiYyCo4GKBhh5Q7bvqSN;rn&1Z|-&GY3-w_BX#K(rD}9ZCP`Q#PHfSeesBAsB;g z+Culg$1bXQj@nge}OF#h8({-(@shlR>^ltwHS?qsi(u)6FFgkI;c{>*?&uca3y zsk}Q54<_C_bOKvq;0|jgD81yRwa~is8Sb3<=PfIqshnn$2IGn=C!Y-%w(K*pkraN}6Rv$8CBG^K)TM zm#!aXcc>XZ(gN2k&OV(DE85sW--hXz8Gk0U?Q!Q}gBC|K!2I#|Y3*TuxE392s|9qi zIZGEY>#t{###>kh;qjhd{w@!G`&N0Nh2_B+a!|uG%o(V^WMRoHsDO1W{uF9U3$DzQ z3kVsE1AsQqFw3LnQDCl#%o`>xYwHvR7B62BvfDmR@;o8aPOfU?B9|4NwKFF{Yh#DH zepBg4`?~Qw-6s&4zA^v2ost^rJJ9?v{`t8s6&n=Y@clbX+T!uB@&`9Y06C zI^!qarju=S*iM(4D-}7Gfg6ulEZbFAaJDQP^$=%d*#cp~jhmDNUR2^ONkA#QN%}lE zNtcHeo`R2NNb^UgN+EgbYVs~{6oAb3V$uu&>AVd9qa7^DA_xG07<1(}4^5s0XcFk& zBy~$Thu`eE84~i_8;5#wOt-{+>$UT;m^%`#-+5qq`JLoWzvLHZz(rm8hV_n^7zg9W zG<&rx@z*!iVF-m};udaQ%%Ewbsd;3JoHBkVvb}5%It#mVFP%AI694X=wV!pItk2k6 z-k0j^ND}xQE=ipBy~H2ANkGz|^`Qws=53CH81LHV%@e2Uv17ET;@X3E+8>n< z8@d)2SIp9fYa8iTJ9Tw95-KRrGf?QCmVZs9OJ6_i7qJ{_s~&o>{o|PsVwre|+dHcs zI?5;)sB5P#Ti{tB+g;oJq<<9N%-1f**m);z$hsk*&9dZRc=DHuzX5qeK*97qD&i^z z3&)*3{k4>*r`1#OWx7h@ntF4j5n?ONoWiF5rg`m71nq^&=bUW%F;bWq9g@Y5)~j|W zyQWxIw1zitGkY$G-@E?Yv_LO7VwZIYFSvFGFniXfMzm>bRC;}O>9WYnOk{6e+AULm zr4&RpFqN&sCpZhPszAr%Pl6J^CClEc!3J6hY)ESe>i-zmSH&C!@43hyixC9{0D#kz zcCc6XfI5paFMj>ca06BAz)^B(KuxNepi=VM7ZRm-Yk@ivh#&D1-5x1(IJ)M^p937(fJc&|Ci%hYzeQWw-rmvkl%RuJg+ zQ&q*TWLSr^9i9O1(67W#EnODTfHyFyEZrL8cvoX*TUFG_{Ed&iEHTf*O9_>^OOYAT#MtD{dw$2{s#KD&&6AANSZDQcb5 z$EPzDk1knB7|@;;HRBoW8h8SK_kgVj^{Gh%lkwOP9~3(w0Y)W*yefC`}O(!B5&@wqP#&O-8tGNx_4 z<+!(%y1hhc9{BgjxpmLGubvB6Zr;n(m#I1C&I-aeMJ3wOj(=xayi}66 zRXq^N$jE4zl-`f47IACYA`r}fFC7vQrx&)wh4QYf#IzPv7PXCQTP zoeJ9|52BlbG>tIcZww+&Tt-xSGc=`;O66L`RY3jKaQaK~tn&2gdncK7iaYfu#n}xn z;~VrGZ|j-}ub)3o9@9XXBGn;;-5PqHPYwgek zQ`7;OxeX2VYY^(QL#_5!dTG{c>Plb0;;ZE!9#MeSA!@?FnhLs@v0b#9HUFs#M8l@aoSzx8nG$4RnqQ0MOWHMh9T>PIQq?WL74;{DeRTmuKXDFS zgruP!o9B^d3zA$}V_1qeFyJCEI6RpJXMYOTUy02bP8OabaL_a?McTr@jU;(V3$zpf zOnc{`>v8Sm#jgM9!eaR5*8R?9UZEWjvsr6tIlEll+q*@Mv_|ZYS}y>uN%KH^^jT&s z_G^K2KSsS2jGylr`&S!ZHvI}Bl%C!`Q}gsl%|d5~;crzsNKtOrj~ORh+tKYK8ND2? zbv+tl^LqPtY=9^?ODVeal6P1a+AFcblZGfi^3IubIh*PmoxT_tNGk|0auvAleJk*F zu*$1GJAiTZu`%w&|HWv56FX7+bX3(-Q#FJ*E?lw;TGTxBGiLYP7FOrzd;EID&|3*# zkPBruvJoPiv$RP+b@}df*nwvJe4urmoVmERX3urb;TLw019t_9iZDr(lH%J!r_BgZ^^nR|0df&w8bPa`%`Dm zg*V^+R+yAYXVp=b5e-HI>VF$z;+)gPug-bPt?9tIwT<<#yp~ufd1PI}eG+z{89!~v zXW0<*hv~0sWS6&X$BjTe==euQLZ74%TrMGiXOBK00}Kma?ARUCZr#1>4BM^XcGFoC z*ZgS*Upp|(P%CZDV5)eCi|LM7C$AjVmZS7^A`HAu=_3-LgXGD#;@MH+tz`6KU|GZB zZr)+hyIa%kv?PU)=HID|(LR@Gfc$#2lejKlQhI6xN`ycLfcUY?_{i@lvkCp+J!{T; z96=d;-@P4p#?SP8U5!t}GwL=Qq3c?HMIUTvWGqY{KM@9ph7E*E9oE)3=R0=bx^x{6 z6R}{r$@x4pljAL?m>tjig(Ga%Q(|}#Yh5=lE#IeYso%M@f6@YJjxY6iw1cN{js6H- zsD2_uducOlphy!m^C#eVQ?M+mjYpaazt^^nq#Xi5;y-foh%N9&b%_Sso%T0X17==T z9Z|zrik4~XgaO4pBzN}dd1+lY1!$QJftozH`wfA1vzotOK-U@$_d zE3HRVpLMX^uuil9vs+Y~gOdEw^K&!TPVM=Tz+vX`Z&6APWMdW&GskkSU;TY}cvx0* zvA-9P^!?j9gLnkNl(vAh4ZIp8l;xp%R43Y-OHsl=DKc0iWpwR($xxQ9lVGv zBIz$OJtT^L>TF*9=V|@Me)LVP$|%uP?O9WgUdiiZJ!J zO4Io`XdWTw+{t71+;K^Sz-EB{%!; zz&1tl;vx>b#J`iKgAn@r2f~mf03?1Jxg{ZRQG<{3z!*X8B%>B-=>S+cAo*a&k0iAJ zT}t&k?6QVRf7A~40brR8C>sLQL|%rvxTFOg%u>Xch7^2wi;sV^*CGpJ>%cfNJw@4thRs$Hsml@>8I zl0GT|9J}=Uv{@4?zQgQxSCrl7Eeq2f(!o&^QX%R-2spVAd)^T=84v)mzzj+Dh$$QYmIjiB=p=E9 zvjMJ2K%P8${ME}|3`9jp6HEnYQ-HmTpid#~L;G0y?4*yu+?&-N{r#F8nzjucqhs-WZtmiQkWAf^_h>N*QC9NWv9E~4k7*lH z{|moRy;<+6t!)J6uQQ|OU_jI(k1=&|&C`%ik$dJhPM(zpJxU2>#{Bbi5qFUTzSQwv z4s#uTIkX*`NK>T#BtPSNimOKF@3|*W6hqSR)SrVDo=u_)z`gQmwk?Ja-E)M)PB_0Y zerv)8N!!N(+1zU`!go z5^6EHN&zkpGbdS+%7LbQm-<8CIitrrN(;7B~ zdW4smI_8C;5Dt)fvoMR!OOuzc(Uah4jBFB8lO}7ECJA;m*s|8QGN!#L9kfxDW_8hb zfG*gfKd3Xg`efD9qZ_#B_lMaX?%M0iLoY1)3~GUQY+}NsfBGh;bn?UPrhX7+KdMbz zoL|z*BG+Czt2`AS{KgSP}2$M7CEFV+fD4k971ry%^7eM~Yl4ofM z$AmXelfCElF}NH!;>_|l7>}|rUNT31gyr!b(d1cz^A~?6X=;M8+@*5@$Pz@mrXF>Tm7aq6^5{awbdHuHu$%ufi1 z-Mc5xoHdmXgtEa_Z~ct0__Iq6#?Li$Fn>%t_tsIby1KztnmJFCrAt7MR3%#C^7y)Z zvOIE_6uy$bF|#zx6IGunObdJ!u*O%%>oL{$&B8r?lV>a1sx(YUGqlb>v(rxyFnA-lu|~&}y*TXDojG|*JN3tu7j-|}?r>Z6 zWZ#~}TO*7UCqsXB1><_QxC;^OCZTYslu7(U+D#~Z|K7TUnI+zIF!11}U4;mM^o+2c z!fWDkSQ?&Tb7tOVN6fNu#6`x+`FVDXvVAK7=D9H#+5Z?)e9IgHVZY+CfQIl0W-|!9 zmH5hQ1*fO22A5OixB)9iSwfnsRNfrMKSYu&X=Xcl6j;LAY;VhOm;O);gr0t>I(cSA z?O1L|{HS=?j7g;j2DFAJRc^lqnf*}zj#YGqomf$*PpjrOe zqb%MQiOR*#feoNw?jD|{BT*%&z=)5?_={lfSLrQn42?K4h%Bg#2Q(=xSepQrg7uQ- z(}=VgmIg=*NO31j(dtN?{ZgQMG`kY2Z08r3!_9jS!|cLR-dx+siy3>h|M0yEb**Of zqhsN?R6Xp_FEzz>GJFZr9D2sjH-uBIZS&jXfr5}_h(Ws_qK>~kjNE_x7^oO+8nxxr(Gm9ZADUo zYoZ!5GZ-as*ml!3AnJSo1L3Or*y8d^Sk+nfP20Pjtm#brG6!z{q2Ff!i2Yjz6f)-= zK$V-odYZ%b?5cY7#UTvN>_IQc4qopl+|%iYws~iRW?Y!t39+F1z;3@^=Yo@eLAE== z+A3|vY{mcz%l3T(ub!Q`1m&>U$NZJ}9N$*&4-@m>&n z$^RH=mec}MBF?`>nzj~sj;|vgnnG=r|F(-%MYaBu7FoEtP>D;{5#FGI5$X4joj749 z@dAI?U6=8T0w4sobB}FkXCFtl%DQGfBMzhnOu~#KS4QS#L+!css6)GXX_w8+rWIfy;|B}-p@q|vG0 z;i@=2oF{SBfdU$i9w_Vwk!=8siQ>s--!`GX^3(3(DW=z=v&%#Kfwhm*%9 zCG}&*SrBegq@8PSWl?1|4xi-iL5(Op2H+^MC&@BzOy!Uzfx8h{#f^|At*5{Q zJHs-N2p1PUldNK}1vsv}d6T%9q9Gtd#3PBT_a?9v_$($#7DK!N^Y2n_n>5P;c>@+j zIidop!MCMk;8OxilO^=FZa}<$0}#}(-^Ze7VR6Cs=bXK8Q77;o4@dXhYHt6wQ{FXgNJNvNENK@mrQ@U^Qhq&Jfl9X@tu|taz4DZOXbpXnaFQkz)me-WhHECBBK$r0+l)#C7Cr+OZXCGV)SHJu+EH5tF;_mv!nyF)+9~E^%VLKpF z&yG$4)8|ZbTQqjcbpW!-n4D!-{E69TWymGkB%OJ)c<+>y+ibbblkpz`HXY1r%2Ul^ zYwr{&L7H=P1*d~W1M?@9(p8dMK&%`HvZ6$w2GA_F@!^o3-XZi}4JrjBk@-O6Y2&lu zyEvRbGw=jc8G+uswV2V=6sQ5dNYNN&jGJKPZ+YsE`Rp0O^2%yhSX!~?qMpQ&W_Erl zJY86n+C-+2Vx**HwmPhRCzv4n;euG=vk!$mDgC1BX&plX9HW8PqfBeM&|Em@WYM#{ zy8Xpnu$#r5EPHSmV=U{0-Dto6TnoBm;pFKvwoj*%#u=8`0sgb^7=KPAFQ8G4pk?dJ zLQrpZ1>*Be0-`t&8UM1bv*QxFj=$U?B3e3`+aYgka1U^l$cTk^un@hjA15U=x)?}X zB9*yh#?w0q&mMIa2A;b78*z3ngwpwU-VxSXM3q1q%QEvEiBDbiJ&kLR-Ih}%Z#ioB z_%ne?y_B_s&KY}@;$gJ1QiHBUH+H1KT72@pys{?M$g-I5T3B3K7G1@%Je^-O<`3gX z`p3azhsEHX@kb+t9y@YmbXXUwh^CO{V|cF(2jyAr=Ok^Sk-2FfDjh5KYSsc&e)!XB3e0-8E@EX-*2zpsx|{Fv&- zQ#zyc(zybW7soM=siPP(uAp9$1a|M?ldz-?8Kt7tFu$O?0{Ev+^+X-=!IQaSTArsb zx{ym|bi~QgVe62iI{ujsN#4V{Zu0!8nS95vvqn&vx`dB_D^MmJJB5y5@oz0~3Sj=6 zAM@tYKlzc|8DN&b53~+u2rM12*_)TuUOhz?=P=nT4@#c~SKsq8c$#W;y^IwkdCng0 zaHZTFY4*%v%Hg1h**daveDRjX>&uxmpppnLOn+fJ)N_#+xH)Z zIWhZ(Pqkk~%ooNE!?xK$RwkITN&0&CV$#&;%X^NB*|Pyr^mR&WRcu7$*bxjCxI~=` zhR&ZlW*4n6GZ>KibZi6$9BZ{Irq7IGMT1SN<|*n~;{%odrinzu$P>Rw~$G*xr*GmG`5A8886g3dUfsyY`2v;Ee9yU6`9Udwz&@ zPCIzff1W$@JR^D$zIrW0wi5@Tj*p5c$A(R6@=Agg>5L9b9(1@#j*}G=W;%{^1mia; zetgtAE;?BT3gYO*Y>M%Y%oLbrBtQF6X!Fyqbn+W zm9%z2@1#pz$%-b8*omKt^CK&M-n_JPC4a>>caEbuMDv(DFY**v&Yel(129YL*JC6AV%f?bj7WkpAs~!PN5B8~V^qHSH9G@rBm%UlrF+kW)gF{MtRa!Cx z9sI)#eQAkV#EM}Zn-~vg&tK32@01Nx9n9)Lqw4M5$D<(qVza){$<~J`2P7dQ^>ZT^ z`azQ(?A(tvF=1WVMwtY+Q^pS>ro??>?6&1Rs|fg+0SrniAD4Ot$%T$Nx;kJg8W{W< zx=~4Wiy9!7H7h{6is4!;)706*HAo=+ubG9zGwE&P;J_?`osvr$99(C+CWgUU25JCi z5ceO?hI?WdH^e-?xpm(b@&_gsT_=Ju=otj@L3A| zsX%}w!rEVd%U*bp&_LudBuQL>IR%UWp1J3kzZ2bnD9HzJCEi~faHwDwC%Q?~IJ}i4 zdntv*BF4W-guJ{VknpWhmn`Vpp|U|IfAj9c@JMRF>v}$&TeKg71P&=3nj6qej)hph zq zaLF@`mcLq?9FoeW&R%0!B_@L+G>b{RWx|i~@ta2#2~3XJ-5q_eFTS}IZr^_#zPffN z+>+iN!N3-AMYkK0=t+mg4d06Ooj8EU^_^*50QBai(`JQZdU}0x7HK~0@ew`6S2P(YxB#k?8`FAu?EFIfm>QV^X0!e0?%N0o#MXh?Z2Xdsx7+6d{8 z&0~v8tRcvfLjwb*e88b5V*dinVo9?D!s(#sVtGE&I>Q4EDlS97`dGNbgwGbK zZ4gr_sFD|pa+#k%oJZt73uQ1Yhw!Q}PX<($l(3@xJy4(}^$ZCrF2_}*sX!!%9k>kJ z@a6$YXYwF$gQa*ce8kbtILpbr*^p8Ki_9B_j+Bp7|5)Dvrhogv6PuMi&A3<`@q;{A(-^~W}nMu*ZaJZlae&~cjfMb&V7!fa6EL$wlbK$h_yjjT#ng^y>MNQMq4DXPP(@pnkOh&Z z&vZ6ZA%U*q$fu69a!@M*WynHe0wdZP;bk^}SsQxX*Dst_r--I*bT|e9I^ESp;m6kL`12c0nHb}*RxS`hr~$}Kb5Lp6XJ>bq}V3};VHhj(8)XUv2P zHIOJ7u;^qcHgH8N#d5~K3bpbB;pN9)bxHjSL=kD0EKLE^ivvg+x?O*oVZ_l8k_Hqb zQ7OX$M85n*p1l4Jo<;g2|9){6_jq>R82x8gZiY`T-w1cb=o#KM67H-AtVT^I$R_kSSP5JiaXyrVk1hvuFyM$1Svi6spAtSWnl#Zd{Mq=2Oi6=P`#R8 zUfML5RP)MNgtdQcp8}J;s~)+dK{KwJgci~6Bhp|2Pau^%d2{F%&t}kd;R>^0BzI6o z81pCZF*An=#?Y~?pO=d6ju<=(wZ86h<>ozoqeY8jfaaSs@G`UG4rg@qC&lbBAUJn& z##A{M&m0e<((8|D(1z*fbX;lHW|bxnZ!&MfX0V%uQ*G$b_ZVyjUl&h#_~C55V13p4 zDfJrGjQzAWW6q$mmHG1W^Fh8fM;;%&PoYJ{s%q;3SB^zvivteiJu#FzbIK%n^mO`%%oifS(y*D*G$W%H= z_g8M*HM?(~SGB;)BH}a66d-U^LP(jv{QRm(J;Q@vnBMsPw=ac@XJ*15eg7R>7>rly z9W`b_XL5Ue)CJM`6ILkZtW5FUbYlo zVZP_!dG2wB9swIzg>%#-V^nfC?mP(J+`b=v{HrgezkjUr>mNrQzODCwGvY%v!QhKz z|DI-I=T1$B@4j`(EVW?r%mVBuIA@mD=EmllRjx6-RkJlh?{0|K2}b`M2CP{*0vctU z)cV|8Z@eygJQa>f)rAn!j-xdhkQP}Cmtf*;RhO2*W-(*@$!Dtr-FIPK#$WSEVEofF zGhu%AsjPg4?3-dKu-?C6-2`GVV%x^orh|@18arkeu+aR@Icf^BS8jxGkpmlgDR4p0P$yPQjA* z3@!<2=JizVr)Y+$GvSd2W!)xvgsw-lmUZ^r*)TCNVHp%{-#R~N5XQf1LXa%#0QZko zNlF{oj71)Aj|#}XeeE*-noq;#tr_X}mt_yk;t*~{3rGF>;o@Y_yj%W*T89pc1qG^- z+P>HZ*2o?O2vN3$q+m9U=rgjFbV}V&2Vhae?n~42OWJ1<_}cAe}6K`N-Yy$ye8HFTnTTxhN~3sqp=`UomNPRGVnby2KZ=RcFLn9Izzv})X3Qpo#UGiE0tm~)vDAtOl`)V$ z)ZogR%3uEYV_9N7w%Hb&v0z3FhHR|D00C3r)5j;nYv)de@4j&{Ts*Bcsbk|d&>{&l zGsRl0B(Yj!mm7p)ltZ@G96L?FwcP5VX>|roEP7Y~KO*U#`*knqq+K>|QAXOY9T#!M zOJdgSs1!n8KG$XZHQ)582#1XylOmcy-Nnm5fUUdjMAX!7XZFDAW@2ci4p~!%&ILuX zZ6Mx;j!+?R6_Y_gJcJc5F%dTLOqg}ZKz~no_3ZI*Pu4xG1uz(}7QhZs27t=<%u1a8deW2EyyQFBLV!N1t7_eFIl+JqS<4yw)^LW)A@~02tBdS8i)l&`9{b z@4OZ+Xcj@iM`T_&Ivg*(VKLnnxUDynKNX9Te^DNo8E7YRb`xAQSLXL=6FoRBxff6dn)Y2?Is~*H6+vgB@mU z1KPcLR;mU}JIBPpXXlqqCyk}aCtu$*Y4!f2IqQ&5*Eo82E&TM8%i;3%d*QcNZ`lk0 zH3iI!&0#jbP}|r)Jy`%|VbnUU{KN-LXz|-S<&BQmg4tz(u|j&WSOBt!%A-keGjLK@ zlUS1~kloJ~iwP}(`q@({(s$GmceG~0Y>77E+@+iMp4bjt)=^kIhv83-58FBk>m{$s zB4>JH#P(lcs>>P?-+-Bu&p>T9logJL*W7%WG*7XL<`aYg#za;R(j5fJUqz>WwCjq_ zB;xWFwX)r`b@uGpFrz-pS{?nk>l$02%lI4HBnuJqmjs2ba6-1gw{PAsB4bPVv!ffK z$Mo|ey(pM9#ZrzCPW3uA)3E4?Dd+ZF?5L6STVZgx490tQB}oqD9(y{|iokTT=oG2{ zyrX(xBhc~5QQO?*NBFKvT19n%nu1MYsD5DlUu&WA`khB%_?v!P@P)x4`OYXk8^F+A zV@n=qRC#V0qoPp8+M>Fm;9ik=MevM~`Ag@Y)-(xPe|H8i?r(Gw2LS=vdtA6>PX&7@ z*f7TeIXe6w{o*r|@Cn<{_n}=m5@AGU#LTeXzIe*kTHbr}f(@`7g=}r4cJd}5Zw5?j zq2fbXIS7v?Mnwc<5Yt+Uq&5f3i%0^J$?3E`0~@=SVw}ukksu`cI&215Htw}FFOf24#A|6qCYlC(JZ)CLT5*n zv?R%+VC=-}PZs9Ps|Y6|!qLHUSObXCI7V`Inxh6Wj(*09w*j?QYZ-cu}elRrv= zq>ohRop6&BMKtBIyzp$r*@1xHYD=+}F%pSP{^NcyOL zPzT+Rbh{`Ce@-f)S#3z-K)AWZ44+Ch^zGNr*)CzEViu3Fc;bi*vjWNn0c&fs zRG|o}Q`!uve=AM1U081qWAu8-m_0OPRyr_#`OVBwQa(FuvBAe4pG{aCf`{oQ(mQG$ zI|xn89fr>!_y_O5p&h5}gyt-#tdRb+_UDB{B+uk~5YAxj%rRnc1fGu2)E}V=oc`YG zN%|MzGNC|pK%c}AfZB0E%t=Z7Q#x%{5$dz6Ryb=1>#wAXLdAG=?LSw@F7h57!B2aAZm!;{-@- z$$#+9rSR@6XTtC4$bfs&`(M=}@B`_!k@ndEJS)5ZPcGjy#&}F>pA(Y$-@JG_oY7%$ z=OqDSn@+EOE~)nDksdu!nUS~tI9JT`Cc=tkPvZvVkQiR~HzrLIr$BiIiif50h|K*t zl0HI%wMpEK%6a#gyMF%7op4zeI@hG)LYEK2U(OB@LyG^wJFl38e@;85IsW*B)H`NQ zt3foL;S}?-qAAm2wnGVE?(XEvfZ|0gYMP_}KYQ;0Y(SI0-Cb95BzPu(2M>@0Ku;D; zIjKq|Kka6>&6(Z5_cQJXSa;6s?&+B`GpA4Qw3pN^wOUe1Doe8HJqd62_lvxF-+c~v z06~@klYF@MW=2N($jHdZNQSq5HipiJd5rHIzk$F&p)y>4`%3xl?Fj4$;G>3txD?zx^_FYRgaYm;xLEy-lYg1kS7H|#s0>`=l*Y)dPeGW^j318y zdTD1d2)`OVcd#t!DnYTwUEaO^&!_tYx7qmAX-i0i%Egs!dnwE$;t*}i#Dpa0R=J$M zALhOybmh}$_{YoOKOrV5!CiQ2?_5`gMjd&?ht;8bKbx!w)x~!|!^=)2^s2OehnoPvG*hApUrq=rXuofVwH<;-P`E+IjrzJu_G zcY*SQnEn?=B;mzNM68f^Jp|L`=_RL=eHwpviQj-a)Yr_tXO#9^4_9hL(HxFbxcSkI zG_mq9t=@bHQ|F0u>}<`<2f>drQGc!3<7sSqKG)utP=-UTkOLq)nuK5CwfZcjXAjfP zXaAaZ?tYG8l$A6>2ML=VbcC&kH&Y9+Q=4#^h%07VBWamItM$qqxlSrOL_G(n^aDjP z&%?TYpy8K^mS}=o4I(Z*AW&lBgFh0q;4FjlN3_Mh#ugmaHj~~?rDo_@8PiNPBCxRu z>>0)CCouCT6A5ABP1smor*sne{q*Gd*x>v=;vG4DuaW$Mvj8{vNi3hR2k`UXUJFBH z%D0bkJ>~mvT?pGV-+uE#HV45bUN&y0$jpSyainG@x?H~s=>myQ;lwc*zJ$0UQsh@w zM-REY=Pnc0=h(nkjrAsnuhVWkxjnFQa7 zA#<~x`N(aP#YkJM7;KMH&t~0xjjF|BdyHdN8C;5)B?b2hC2G{!#N7~K6DV}0e;=V|Qv?;!f~ z>DYUJoaU~6Cym&L8P7+}9X-PD2y(!#EkpPpWG?^F%@E(T=mf}XnHmEeF10=Wx;IP8v8I{J&?b&$km735z%aW@_FeX3bZVVShoQ<42V8BkYp zOgm~#@IDGm&5@4el~3BN%vH-u27RcAi(;4{G*zyNn^h1og8@M=Cw@J^G&h|sdwKy) z3+`CmcFjVsq3z7CO7BbHuzVgu3+i#qP!=Dc|R_~w9r)x9ejW+fV z^^_-H(q|Gb-OR~Gu~O_a(J##%prKI}L|)Mazm}#&*f*fL14fag=Oj!k+JmQlsyn(W zD_TN66`zsil4MWWzX~4a&rEt6*FLPda1Vn|v2Qlb%hWdNu#8j7I5x^kk#cUVBJx>V zL9+WiZ88Z=ox7SgpT7~De`0J$p}z`+dqsITdW0Y1rCfjK&Sz=o^M6gF5dSeY(T<%u zmu44E2iIiXZz4C8t6F{bByD4p{{jb>em5exMk83B%>fUSDom_!%QvT5Z04unJBYgfC$gQ zg>Nw-XcTL0h1C=BC33+zN@y5tl=&$wHc$Ce4{kQ~;Tv|ie-l%eS^o;&R z{;g+CFt4)j=KC!7T3q%_h?YptUrHwz7t%O~w_;UFUvxZgtYB;E?#;CN{7G8A_S>{g z{NwNaQ5s!5OL31D*Z;LA{C2Y7FRR|RXQyKz?68@ep50FgagLnAIAVjD6H93h;_sl* zZP6(t{0_2J4h}6s@E$>cT2CO1^mI9Axs6Qh@QQ&MQD5F4I{aSFLD=T(Jsp}kB25E$ zn)b$>duY}yqep%}t6W03!j1{~chk{BML`%K6mHHchLB@YF8%y8@@uHgHk*b!W8-Oy z!Q@I{o7Iy;QGyqym?ieqjmvE71-^^WocH+KD2Eg01o*TR>Sm|TdVLm`#H@$t#0}wF zOaRX?UUru~0dX$#?#b0l*r-vxc5914?u5fHoU0F<=`{&c$2S4>cVw*bZLDU*c+V@M z4P^K$tWqAbD!F~{ar&6eTURBTwRTcetn&tD6f{nE>Ffd%TCsm$<*mwD^cm8%>*W5E zYbhod>s7*U9mBdC9_Ik)cOxxCITP5A!hZ$z*c(hZ?kRYC+9Qa+P8HExOq3oL^OBWm z&IM?H$vpt;NbMIdbvaJIUgDIirkXWLVqLlt$j^MPB8Y8NMpa3XLf0P)p=-2l_Bi~% zB|q-9h5kNCs{!RoYH8s_nqOc-TRNGhxPfbg)u4NEPM{mh&(hj+j0^JIcziF!baPj} zlf{?SYAA{jS+1BE%3XD(!!O8}i1BN216S!&{p!h1Am^mpVKD4!jLE@+>4mNP*I4c$ z=3x>zcJ^YLhu}}n9S`|vNkh4{JO9TuEOu-u{qVaVz(IVTu07Nqjd$LzRs@p4WF)zy zuZUZRbN`p${GJ0s_qmquI`Z@T>1V(C9fWh0i6Bo{`Kbm96Ja%GB`)!^vs16}qjb=U z7(9x2BnVCd-n?}gz5BCp+CRWZ*x988T%j#(f`&4IW$M@d6%$~~S^xbzfu9xrHHQpen z{hv}@VGAhQb`bQ7`&)n3<%TDA`I-6>9R81f@cs1dH!r1ya~HWiZziu^=zW&I#l$c= zdMq6~eI64SQ|Z~gTWRgi^|bNmUYbD5XAF&>5hgrpC;F>mqfPo6$<$?g@tl8kFtk`H zex+R*B<0!6Y{`22CL9TW6QcW_<>5B+YBYI9;rbOpNQ}l&^>PC!6Qe4Q1BF*%Cg>=q zPc5ZIbg{=>0YLa;V-I<&9Ba_70Parj)qG2=<>s~Dp0!Dy2I0RA$M@+Mx7cKS5;thc z{ZBFgT1;xw5UJqK;oRNO9| zNNi+k##R#Vh1_%M;UglN8qH@RBr?`*aI$h~a9^&hltb5bZoB!b>e0!Dz>0_~zjtzX z5c9;}de@DvA-eM$T$$U?{$;ebyF*6Y{*LGcEYj`{s{<>P0-3I2DGB@xHgqKZPEhVm zcnL}_M8=(?)sd?NGm(rO>b}7QpRK=oG{SH_spEJCh%y8hZNyr+sPD`;VWn zfA9>B9eMR8N(t1wXHIMj>qj+3>(%EN?K?Y<7>qu=4u|h*%D%e|1EBd4?~_(jZ3P+l zy`3j9iI|slRyA5=J-f6RcP=j+N3_P_DDQD_QtNhIrR)wqv~P+PW#cbnJ&!|?PqTtK zNnRIDP*#~Xn1QCVUhE?h{fTJ~2=eY~b!+u`{6?p->0_J<|D+Q(cQ4u&{=R9hwZc;H ztLmzuo+jwfHGyqp)=GF9Yc^z5lKdWa5cSfh#^z606`(qI4dUon|{Ex3ILZt1jVPMwTXjpxsuMK(SgwoJThU}6@Fn|v?A zF-09yu3OhWO27Kq|HqQ}3E~_!!Ks5xR5MvyW8(AHA?6V9ucqJM_#%CL^G^EffBALP z-S^(Ul+G=lNZ;pqjJlmdo~`{C#b?T`w^x^gAWdjMi+T^4f7G~>gCi|hnKfG8Xq;#Z z+dDUsZNr7ytUMUSA(AT-OdYL&7BuDC=WebEU%{#AY5UI2Ot3+FDhQ_p*X;On9#Uc- z`L8oz2*^TBlxO+0&FFt#Y@;~Qa*Wvrc?zSEI}g5T^zJ9W`8X!+oA(}&#twS}93*7( z^2`FF4fZ*9cBZ5L;>NX*{Uf4-uqoRfYf<;8F~0lVKT5}!&ZIkc?**}6r(Za^Ke%%X z8&t@>pFBy=nOGh_<;fn;Km6;5nBM+<$o;RJKZ9Yo^XX5%|6cll6~IXdt1G^`uC1Tk zD89V{m-}K&U`&|m=v%Gx^x?NIT}T(sp3WQnRZen$$_?tfV|~get0^iEkwa|YCYy{Ng1V1+WDLr|y-dv=LnJfJM&4vPeUd#4Hu27%JrfoSHDWc%q&Fdq)(jyc zYVSOMirU-5iF9HX6*vURTWE+l5DIa&+^geoix4O4sYQKF%*?_qO(d>u51I0lHSR9J zRKra+JeSdUS-{xXlsfow{;UEAfA4JSm z-57fTlT$OCWL--0*sjrPrJTOv5edH+?Pz@PDI&RN^r453F&xKLnrZxA*uPPn^^kVF zcIz&tG>*q=QL&ODp*gg&91CBHqrR-;sCz2a)8ZOFoTq=!AV##W+E1)sMUGCcu|jvX z<0_VZ=Opfa#}=ACTPP=l)|zpGpwP~~#D6`$d9L-CO{%Y|lfX{!3+)n#Mm2c4^BY9C zc-nY_9AWG1=&#+IuX)R>fu z)eE7w{{6Qvhe#q#Fzl%74Y*D9%|pz_B;sbUE~m%eD6oULyyC&T;iFPZ2J*-#d1MlZ z$$${UD(HCY`6EE7$PC>3-4ls4fpv2=UU(0=cJ{>2JmASMP!{~+2@k^zMjMRPj@)Wi z<3!X5cfN_d^)YSvkj>$refVkE<$na{y^bhnWAh23q>Xgt41E!FE!xD%j%v#j9faR+ zEy-TI@^(6V@$Gct)cGLzPJ}8UJfM#+Ke?OU`R<=&PPg*kaKjTqu69f@ZM;g~e!`iA z2lu%7>+YR&$AmNRFjnjEt~jZW*wz}XDl-sP+|hwO8lfoR9E zgTzs9RT3Fg+DwtFQ1wkUy@wdrlK27|zhN^ZeNcB^Z42KI;*2H{a8TcW^f>+gvm3F2 ztNP=yg{Abym8Yw8yO8 zoy`pL_xVeUNV4zK2QZW1$fg z3B2k*(Jq-8F#oopD-gAl|o-ZlAJ%r+4Qmzg?Sq7>D527C%X>=ZU`Nz>)Ugrew2FqT<9gKDk*FYO9m=s`D z5E?%$p(prB_}@m3DB;%Z@G3iSiYlrx+@FqgmBj((Cah|cP8>lAx?uMDwC}g%-XWsu z;j6hb4{`s|cP^zr`w#zfdiOiuO~+7WQ+Hc4wN6@v!#7T7a#V!hjorJquB96fZ?N*Y zmbRYVNn@MOke%B#VwCaBluRz)!4Xde*fCC>9!CV={=f--MUC`_Kl+38`0;~u4wXht z=8htsxD5yU*Z=Xa(p3zQ{n2}GGg!GJ7B$DjVz0a^YYgw1rwNyXtyD1)IWwzd>IA$ zlF~dHgx`Tkhu&hDU6yoelqgyV8FMvE?M;(xhE1~RsQ$hb-A>)B& z?Z&Ji%a%iu7T6(;A4~p$jR^^0uqh|i^h2Bg9tgX8fa%G#pFO#l-belJK6$#2jMhBN zBFjAn7o-MHXz^59J8JP{j$Zap?J&@9>c-j5Q;zBq8i;ViXAARfoz z{uf-HqkV$#pX}#4Y^NS{FJ}eZd3E5x2SN(l<(l!jg5uNg9IH2OKls6Z#^*<#!o|}m zsMAUGg~|O4?wPh<=aAKHG{hbuKOaMlcwv4iotk0)h`kbTe%oAoMpdk^>cSu(ra~Zf z4MNXKiuzw=qF6<{%6HS8Kpb?IJ(V+T)ECinnN1mGE!+^GFbW2cIII&1a7}Y_u9}8XLH%a>lG5 zK6#+bEO7W^;S^dHm=%Cqat~k~?tF@6^z>wl%dOAFGFP=Wb*q($YCbpe@Y%aQ!p{R00%mse(*=kXb*WkqV50wS0AM>IC%BLKmN0{z!ib#ue?S1$I_Drx6*^#==86matt@B z_(IWFGf&S@y}bWa)m=1mp0YPZGEVS|=Win3yPg(KU!*PCR}PMmwr&29y)C(A_ot#S zQ0F=ceL21DD#m{4N^OeO#`WL-Cf&LIX_z~hr+vTk_LcPG@4OrG^oab5|G3gyr!STp zw>)Hc^}@Myiq+>fa{SGTp&4iYZt57PciG%_1!+sj;FPu+^GpeKw)X4`!esD}u{HYB zD=yY6Pp)%>zw+c?QGzl4_(P|0!0KYqLfEq(#7@0kID|N4gt$R%6+2nT{OL@a41zJ> zJ}ro$ux@SAUHcX8wYyxXD2s5ay*Iw){p^j#PRpMe95FHbKi28kDqf=6l}?O<23U0V_Ff zvigiw#OHUOrR#T*?>@=5XNl+0GdTX`&Gc9Q?Z2c?;SB%m&;KG_zH%j03RfU9_iuij z>j&a9>)8P|47~*V?1hWz(Y<@=)8GC&eg5(9($9YOZ|Qqj-Td*lF2`XgS5qacCC{Zs zZ39;V@uPk6JJ3epCtRK!%wa!QF!u{9sq_mo4G2XD@qId(@-)SgNkO_$Ks&>p&Zih} zyRAJRHn~Sp82Hhj{)cqo>N`;%?!!!@cKe(a>1XUYtt~$aO}3pK#W18}UnH#}j=A$_ zE&cklM~FNSV^sZlWMm`#-OqlReoa4EK+LznA;mY|{8n)DkFZbk1QP|1AKXTzb1tnQ z_E7C}!hRl1L9Vw_lwfF>pq(s!|ItV3AAb6?^zVQ0e)D_X>-R9*CX?zq@g3*1RxW~d z$j_$Rt#`cY^1jUUjtJqWQ6b)&s?tHIZDe^IxuF=e3|u$eRO7UB1Q&##oxp*iQ?+x{ zWiXr)+U*$LnhSMGjg)PfgQcMqK=H6Vt%gu3b;J zZ(M`>#&!u>9@BL4iazw@F)#5c7XvDr6hOU_awv9D#NcFx7#VIAIVqgJs)vQxNG?1v zeCzfdY7>t0!(Xu6Mve@J{|tHZ69|pO(mFE(=GB#y_3mmVL=FtfZOXC9N#Ngnek-{C zli0e^pqb*ILc~kmM%&mjdVsrp=VxhymotRTOZ|`ZcTAs+SQ*C5#HTuGFTMhTrn%cn-fHr*dk zUQL6nxgk#5v>o<~RLv&MYoC40nF+*@&mRWiU*-&e#9o!%Ae=BgKC6u8>{f{EC+hm& zx^W}@=9fQDPaoXHlIQ~Bl(Rwf)t+gKHruJcinnNE>fIB&R7se(USh?&?R^@fbHTi2 z)!;l?nx|k4m;_wSOYFCU;bUJ;XT3(tQ4{um>UJk43>)#A?T!ZFw=+Y0wh%pRv&_|n zj30g8%o)h>W1;YvH}lBJcQ(8o&PhSdfK>?_W*@vA%8kxD7{+oC6*=rs-G%%#hwM;s z1^}5;m%QKq)|+THoJ{}g|Mj0Cpf}RBPdJevZT;$*Od%sNzO-+PSRXP9In zbtCiixpV2#l`E`L{x<#Or~ioU8_xrvp#dRu1KkNa9N8-aKM&@L;|6c3`v|9I+d`Hc zwf(o0we8b>DT>AL$r24p*DCfDqY0tJg6#5?e!c=FC_Au-;W1 z_>5K5#uF}o`ScfAB!hpfbjNMeI*U0{&tLT8k?()p6^P^ zes15f=9K^_o(zLuN5E3&(IET|Lq>~@Uhm%18(cywk9Olt!as@pGcL@jG3bR~9xPYH!wzk3fq`1V_fRc1JN^GSM) zIq7wX&ztXTq@^=w+30oi)O= ztiDd3!TdL>w{EEB`pDUcI>FC+kGdj*9yFSrUkCyftFbW@0!YjDn4!L{UrjZfS@5!& z$?SBBv@HltWZ1UIe~~tJ2jr>)t-&@*K~6F_wL+Izx&lM(`Uc`5RudK zbmh|d^u2dwDo_zsbL&eY`Zlli<%H^Cvk8dyd99yfhIyKCD+8zYk;WA*W2xIHAMsqV ztF5=H<{CG&g#6z+VEjY%!;FY0coo@>(67Jg?uZb6ni0;$8NDo{OGlDWIrF>wKgniT zrU>n3m%DHoxQ1PRo$`Ne-CVwKI#d$>3A-lBUd1<<(`@5&uzJ0qPr0)^?Azf7eN!wl zIz5_x^xiwM0rwyO>hIHUKKw0eo#^v3VCT^5cZm)yhygD_aN_fz&lCefn=Tu0?c2}- zRa#GQAWK7St8DrngV3KlInTx}2aM2UI=eK(Nm5|2oGJJI@EP?=J068l}dzMgex`=tvmIt-yqoZi@EJ&n*ea;sMywO{lF z4@G{0nS+1*=z4ksJ@_kE-{N%h4>(wL4!OFQ{bUu&oK}?%_&rT-U8z>ZO7f~P0r=8o!po*SO>_T{`dn<^>T+T8+-r#uOuX6>GavN z>CA;UvKU9IfO9}g3{IY9naum6JD;ZsRsf5r1S(=!g^N0_d0J?k#@UX;NiNJ!U;}7^ zl>yupgn5Gf5yc=gtU3yDaA3_~n|>C;HOFC~8J5}BujS7B3J!eh%qJV5Xicp!xq5iW zJ8Qo{gyxA-56vyISqb8dn?W0#K6#60ltT)~FN)NtSHJ5aD};S)4&IIG~bl20*z@C@C0G;rcH(rJ!4$ZAGc$1fihC@k`z9;&$1{s_>AP<;F>}(_yNbQ!#l3=)3u8>a+i}TD zt{Ycf5_M!mAXq$ekqP~5-mhZ9F3vW%j%kK}`}xl>*Wm%aRYY!Q(vRQ06b9N3ljxhK zW9B(|U!$F=g_4auoWX)rpvCWjUMI3VN@K4&RZRsU6O{9bD77ihcgzgKuMP@mcy$fBVz)@r~c7tCudt z4Ma0=*4xM66fKLZ1mfs-`rPF>r1JzjJrZq~&uaTvH?s^TO*mL@i^sGE>7^@*mmvJC zCXRVj9-`&M6XgdkY#~9k<`=n%3Ng<#BAp<;QLk>2dk`!ifRiXbee?iTR4&J01F{7! zFIs;vqOJLSqS|(HLW`w2#5d&L5MVrFVAljRQi0l)F&v3j_3L}MJc&40go`oHpYmOK z_wk7SV%p4wfRhgS^5B`o-0488rOOaBSAnidz%}H(L85^wQlrQ!#IA8<^Kdzp> z3G-i-;yLS`&EI)Ae^&?NaH?^b3c|GxR|*&t;S3Y+I7Zbt@e2W+$6%T#v-3f%Y&P0# z$*@sEEOGX1mMHg^@(&38DZ9_I)GSzRf+ zzL@M@P7$ZqZMx#6I}GJ=6Vbb>{$pQrWA_yHd9=B;wZ_4=<+i$}Nb@BuIY&+caU&d0 zC&idV7_&oXV5nSveSkWt-Pb4IFP>9-9RFJk7%Y+HP2CD>bS{XUn?MpMxd=Chyck0* zALaAv&#OCsmg@r7S@(xNe~2SgfgG|oliof+hbprJVjQcD5(A0ypZ)l|ajC{V4(~j} zVAV~uHI6MVr4N4mhnQJd3gSHrH-7n@??I5p)Af&k1&8`5ef;a6g&w{dA#T*V8aO`B zO=Vnhr;`@Mja&GK=z?#{tt{FBJv}v-&c5+B#O5;gd^o@bwyTTTDP|%*F}9KZhY#LPliKsg#{C)UH4W-ICn&d0VFA5GGv8vnmqp)k=t7r&!;W&oyK<3 zNB{DV={Gr9*Ub{1ULN z-sMQM)3{WWLw4ZV(Y(q|;=05I6;Ci_up!3}QD~_VAaS6B?&ahPv%MEchLI1+iOUOP zR1^FbYMxi%?EdrL{sj9uj}TAY#gN;{bdiHYa+JyfTh6SbjCYrNh?koQ{QAV@{u>=+Y>-kS*g3a${Tgrc+J)8 zL}3BiCD@5eW=iGb->2=`N#wTLf#tVOs6FbCxL65w^)xx+rSans(Ag0Gc|+C%R$eza z1a%(~&;%>88TM4hO%w6NaSZ9X&k@8+3J*G00!wh`YjF6_JZ;V@Xob4T7D2Ok$c@`s z_H^3L@>CT-{uQU$w&%%Cfc4O^!EwcL;ZeN==Kvgt1W?b zP@LmEtnM;tI>sY?tL})oBTe}2Fm?ieUI|#79cBgYSaSwm6F8}uf{ql#f&rEL39?iO z;ajLsUcdi@drz_H&A*YrL#OIPJPLlFZtmXY$o`!ZkAtg~I}d`?1bTsY36*4dlmRSf zth%S-6U{AbLtw7`>SyWt?|zwfxUAy@#OE9bzNWF`{s({Yzd(>bOuzlb-yj0eVkd`U zm>4t)7f1gYMBsNIE2Pv+TOgk;PfR>-dpUgUG7~t z3EG#o*4Tf!v66mz^EzdtOw_SNNwI~5e_{s1cW1ApOKiA5L0$3QjgKHy^he82nu?pE zh0SZ_%!5BgI1>I*?D0=Q^xyx{pQn@Oud>?vA|{{>t{?UE?gr}3a~N&=^B=yCRuD`K zD_F&Zp(qdqDVPuWNoAJt(XQ9QX_Rr{De#$z(RAh1v2^t`CRAMIRO2jQs23TuP|~1Y zR$*k+)|11}{YVB5!td%Gab|AA!RdaJLyj=vXTZ=oV$(;Ci-8x8jwIKhVKz;+&dr{U zj#P6EhR13EV^;LL^NaInx^Rj4y(bX8_8{D=)7bG{?moscqg_EAe zS>iu|uVRyU8p2QI!iA}FIG4_$DtHV{s^bv) zJP51$s~;87m&h|IIJkogwVtD{`_y+Gm!vu>va%coB0HX2>8nt*>$E{ewpj)8}aaY@q!#0wJ<5&d>%kn7z32-uF>; zU4-+$LVw#yw?6+}Fd{oGCKV<{6QnjML(ZuENoK-!$ad-w{1^ntf3*lGfIL`js8TOgZu)kIqiD0oItjjEYiL$HzV&%p=MK~>$6D$BeGOl6Mu27h$mCbIBiYeajZ}yH z$M3%zOKrEL3y^&fWLc*8Y8>E4Yg0ZUPiqPfv}eMfQT8-~K#3*j!F$-u+(mVf%{&eF81D z)h*1XKSe`{$uzfT)o<0B61F@goRw2a65>E zKRO3-w&2v97~~|>8AeSe9Y>#d23=}V7{7Ws)NWK|?}c$n@RXVRQ&w+&B|56rg|$$g zo2&-L(0E#8Lv{)RHO&UuI`{6btt^KqM=_YIlNR-|f`K);RW(_}eT;)i(-8A%Cd?^i z_6c=N>hLt&y`qjh0CN=yAH?2gR9<`$Sbggmzf91=U+y-5`&C6C=@Zc3zh34`koWBh zPV*E0=}N|TdilYxjPNv-`es(B6~3Q(;)s3G}`5uZHLHcfz3`2?Txut!&!@Y4m;($ z8S>#V#cRF5|5bJ}Ep&7g+p0S{LN;Rtin=+TW&QO`pQ#X-p6J~e}e52|&VY+i@!E5vk|&LAhq ze_S3mdk>jmuk|}Ov01@Qr{Iq@EiDAB@}s+ut=c*7Kcwb|GX({J>kKBCjxwPwkmEj zdH{6DI-8pGQuWDn0h875UcFfL5he!e-}aDT2hrl&z)}QfAM48%g^CDjX$h;d%_iAv zuzj2)bfquE#|!+4yjSsa$*MXTfKxqw#f>YN^NzkV08Vcw67y+}?Sqc6#aYgFa3?f} zKjL8JliSy0FGF+c*|rvSz}VHqy7*Tf-D4FnhhH{X5mg_RmQ#pE$xTC_#;8h6ukdE<3U*Kw{d19;k#>2n zBADk=2ORE-8^efYdE`8E$tC%~Sy=(xiPniyx~zKU3?MNsPO`%G9&S%gYXEQ!E27Hww^`}L z>YTEVVr}m%1O`9e5B?r2q(<6%pkI^Y=W?SJ_KgPinJPDihxm+f zdH%#qI(MAIYEF((7#K3nDm>-Tp>;aLK|x)X-!pbckH?8{?QBW(cbG{-B*v<$-&lj* z$Aq-GgT@mp*_Y+UDN76bGw(@vCNisQk19L#tQ<|kPp1iPNaD}ZJhW5b;N>7fUW#8W z+XpsugE8Q=0jID0-z8@~nrM<)me6OoLzcbIG>W|xSK#eIHlWQIjV^Wl$z{B}ZZ;tO zZttq+RpcZ>^CVz^gsLL7%^pgoOF5g~ZPdlr((zt>20;p->@o1WL~kQ3C^Ei=e3#9agW+yWl|cLyj5mh0=7~5fn+Qg_wR|WMRPeH`fc2-$ookMwR&H^vNS&l98uN^o=rEMkh zmyp;lD32uJxAMm(IN6G+0ttDDaM09o^VTaCT_P&~FJsM4^Wgf&X&uf%`0n1ifZdw+ z&v-n4GyU83awtT8c<^9wY_4GNY48K-bj z5!4KisCi=PMt_j+oRfXVR4$L;YM^T(?Ix;3%3cC>1=a`&YvKrhJOGcRjaj0q4WujI zA>Ga%rP?B>4kxU}UB4LNl{xcO@?Y|}TjCOO33hCN?*$xo_lGIH*mk%1?gH+i8$cA#+N0qof)eg~amBBS(?PuoOnw%~wy!=pUP#000Q^f9 zIwDz5v2Fme3IMK2B%AMnu8!}ENV7-4@!5AmuHw{DMbwV!*GqB1k@pl}_oQMRnYUpa z9ZQ0Jn(HZ-Fc3H~rV?`JM6JkY9nCZE%GS(EX!XeTv&pT!DU;enW2)Uid9&EDXMgk@ z#Rw)@h&W7+zV;_zh{q8V_7FrKT%fLq= zD0Pv_OL^v$>uI3&_N5@JJ*#Z;9bVo32KV!;qBTQ%z#1Nn*LFl*Nl zS3#Sf5xe%_r3+ouPaWQw&;aiWs57*tv3l2O@Z!6E;ok`CoSiD1hx6!UgoeXu9Q(d% zos9zYM9$^R8==a?-+BK#>GaHadcrB@O;n7e*rsPWw?~YyxXkaS}vp zXyW-(&)B0j*fV0=c-O^WN$>0aEQ|23dpY*ZzcM`ux`bU)TZny! z{0}TK`Q~39M+YgGJK3-F$3p7tOsLBsfu6fq(&!U>2ihWgm(!q&n9d?zh?O0_k+i5;Os`y zQ?>@|kgvHty6bG0$mXFt9|7Livjg zQ=*S159davvE4Mwfk4OEgrZo-GLu1ZH6sUeme1m7@)?>Aj2e2rySEUh?wxePb`)dJ z5Mf;9Ga>kX6x`9^_!(g~Y71T7(Mm60dojxFZTG@CCcO(UsJm_^IA!nDO|6yDYqr$mpK&h<|nd^PZ2Yt)A>& zM7(qIgm)8g5*&_ycIgabk9(aT+zAnEJ55BhaQ>@UmsMmNrW%5%=PlH@B6Uk`KF+6T zs|Zz38@q^k;`Xf#R}t-%L>u=v>+NU|emazdf2@dl6ys|xe>@t?1@*cP_B`N9czc9;+}GKGSH@tgkJ3g zlg&$CMg&H^az-Kpeg6b}gtVs$(|d7e+PyAh$+dTe87{Sc_^Ox#5p)Jba|D`7Lf#SF z@lofn{n8At-bENcph{a8LSMcM0;6OqTzvXGn`xBt1__JyjR*y#Do1+~YM?7H4X<`T zD0n*p>yOB$6I;Br5oOZZFRp%VxEj$uU)sG-bi?6`er}!elh|wP->Wr^t_csvgEwhm zBrCSG=dmB6!4}TF z$IoJ^yNH3Tc}^jEh&D)@KoK%4iEDebEYk|nbxO^F2y_-~=$7shn%(rHWyWtJO&+1ll6K603J&oGsNz|Yv z{wKIiOYvu(ZF4$zhw*5(9eZI}lkpW#_SxcGjU`~5x)-;JjU!L=md3Qn zLtC$s^@kz{EBQ?(gQ@9b!3>5*PrLJr?Hlcm4&kR6?Kmw&GgBwer`7v6;rJiLX+QP$ z-Qf?d9ft`WNcBxQgl^fsNZbq1#K{;G%YVUgzd8C(*pFk!#HH$UHW0>6a zIz9Fo9FV5r(lT(l8O(sle#7;*!x8UPX%&Ltr|(vjuK3q}B_Ib6_qU4EFKQ6sHkKNs z_oX9S5u+<^8%vda!%P0_KoMtAOs>BS;Cm4Ny5MyLUNTILYCyV$xE9Vn;}cJn29Fv? zpsx!VUv((nC1j-H(mVK$3cB-=W7O~+fz>*oGw}|HgM1vM|CtlV)7iyiX^DG+BWD(l zI@O+$duiUDwXT(02>c8ixr%j`xvWPsDUL66E>Y6ZvSwzkB>qk&1V%BGSEf&=FZyF2O;N=PDO_Z^UI|*=rqx9x&}-%upJfRpZ5nc5qCepFgv26A949qCh3Xb z9VQkf^RSuE$>38hiaxU^*lDB}o3;85Z9A`qPtGyF8_@ z?*m^mH#v_(MmC~j%5g?Sua9gxiFzI<>mkMoSv=#%aTY4p$l$GyQZJ4nEs@5>Hc8VD z4Hsj#!zVQG9Iq;M6xHkzCJ>)G*XIdke8i%;6GO1GaG3ZE3VyOd65ct8V^ACM;{cliTi))krIkMub7H zy>WXWqpWS;7|wMxUg~#Vn^n>kuV5(TpnNyucfUv7izm|Hk3`+?c-9!YUV2ifGiJ25 z6KRJB-;q%Da2=3cUDbYeN2y@)!+w|nI@2<=##Y5B-_>XHQG%Ljm9jVCc6o0`-36>~ zP{cL8_>uEwTjHm;A{`YjJW)Tz%3L+f7;A9rJKKs?OCfT6$CxlpXG_7~$FEsGaf=+| zaT!aUGJ0E>WN2Zxf0EU|j35LcTjp#c+vDBbNc3gqrXnB1)ll5d#_X3Z)@H+wdn9$} z!JV~s=;&V%p*356FNmr{SC%)XCJx8A$B&<4xwjGrKfIT;rS?sX!n`yLy?-ZyE*G5? zA|8aCFy)Wh2?<#v`Hc?BGd6!w_4MKv)y&*rGLGRcWos85zbscv=ds}XaXIJ4uVveUH1NzXc(NszzmhMUZ7o3TaXH1H@JRb#jiYjQ||QY$QxnOy)a_}Y1CLl5)T?p;C;-NOR4B1|YZ#9abUKd*D+Yu3aG zxjXiI6uEjWqGD7DyDoI@5z7Rg@G%Zx3ho0~q zXG3=!5s$>*6$MmS<2e=f;T|z5LBK;f!?fHRw5+>0Yi@-5nYTFLxq+3^ElhN*tIoZ9 zv|TVBEa)pu`0b?o>tcIXf}$9dfn#|$v3Mq}-1;=Fb2@h%ZeU0GI}Koiy42jpFWdKS zI(TH78vHP32I|zvrKuNGSL%SpE6I!F{ z6E?^z6Ybuz=@Z0RrpU2@j6O8x7^8}LEQtOR>^w$z_^%@3Dqz%wrTsiq>7HFn!jBW5 zD~WBCIl=@Bc$Do=&o8Cf3-82fe#J(;Ig9AK{;|7g9y+81xt6oF(_|AumW@tM9FW3uM=$!%$>3Tc0G+yB9&(a;-~qKGBm?Vp(;2Z1-1ekfw-1> zH_Y6Z?-IL-v0!S#6~94GMLtnK0niUyiD?E%wV7~vuQLv8pfF%JTp4G{=U%N+a?tT4 z)AsE$e!9cTb*7AG!)rI~|B)qp1$U!Aq)4$*^VSYE!#Aln>8)iA{_Grdy z#x4E@SBPiu^t*%!9}bLtiGL3u(2;>EUe}{vxw=65eeT_E{bEs!=-iGQ^69VP^m80( zGheP=QH@NOrci?LGZtl%!qjqADXMHLUX?-1q9aNh%opPtb?q7M8lJ%Rj^Hw9YtK;R zVEnGJpU3nQ2RSAD%J+4hIcj8X$k!+qSmgG%8jN4R=%#O8Vf91C@BHkAOkLH|FY2NC z?VIpR=;ik1#wE(mR2JrCIJd$aIBOh+awd&?I&&?*U~*tZp0~}#)SGilX#@jn8*8YG zJ$^_B%W7hB6LJ}vbk#J&nl``bIIG-+y2H$_OkZPAJ|+6Xx}22wk5n5@a_)9mH+hvg zZnETDTZZFj70~O*lgm;%YZ!{^1=zQbbvJOZHk(W$qpSURDSqvj3{(HDfoq?^FBP&9 z#oivSPNbc5S4cn<@G7k55u>KU)kX=JAI-=)umdEJ=~MYO!6U5s2N=QnjneS1U>i7^ z-c3yHciX5lml4*$QDA$$dVLCP#@QRLk70ePr9^!xPgk-{?!27{#F84$C7ihF~rY)fXI*9D{@xM$QI90aVj)pd!_d zh#em>kSnN+vl(2nEY8Zmjk|bsWu?R~aSisp_y>6YD1Yt$57cbvv23sz$AqKSeid|Hi*6=)*Z&L zqE$J5nYb;!wTU%7w~(e!UkU|}PRvK^YyX75>Ol5kmp1Eg5GvfM{oG^H0JF=dL=^B8=)k2TJ`xfE%H7^`gZQdS2> zcTWe!eqBG}FOCjk*F;L1HN9&3@Alp=pL-IQ-bfujC4PtJpiC5C!hJ1q8s0rIgJ1(N z5T+gATS3`_r^BNQvg@-Gl)xtOA1Z-8fCdKdl97RNEOR?>V1MZ-)E*yEUi-F#t&Q7Q z_V?`&nL?l;(vG$1zdAlTzz*vpQm`EPrHvN9ApDF4iMcCfW%?5^TUqgeIv1v{1HOR?SjQ;LmS<+% zM+@6M+>7D6IOX!Qam8hnDV~M;<|yNQ9M#Ph#DAQhmN<{HJ22rdmDb&!#s5X0;w^0+ z9iSRQk=~0UFYz6O$n>4SMowRj&a`#=Bkn{*1K_F4MO4e+{3ZTg$tdTq?R|;3q8(Mq zlp$_%Y4mDv{^X7h=}&n-yCGr@Ze#`=a{Sro+iFZ4t{&VkiP<6Yl*zPWq!2uGgq2Jm z@EOD_Pee6_esqH(nUhj2`?y_^33ksjM?q7%SKMnbyV`7~SrwjEmCILHO=o!OpxIBH1P4HM+ z(n;m^mG|%Lz?s06jAF6PPG;xSwvU{@&JJ&^rS?u)NfS`*yP-gVhLE@Bg%%Gh z>ruxa2eziqTn=tO#`^*5=9?0&57qgm6m1{n2*Q6Rp}UDmSY-V7Z={v`*O28urTf_d zH^O|goy?71SP(iQredeZko~`b*e0Ji^iEGT3074fMf z5!hkH(HzVq7X!i7g4f3#x~m3sO^-~#43vT1k(q;}-XBqOWl$!gz+TcT(S}WO7>=#r zt`e#zm^y9&;2Hlbg1%c`jM6u}3g`}RewTRq`P1R2clqo<-i#Z-?a(%S#!&?iew4Jk z!5jW{CYpYOG42^6*Dg`d^8GGw(kN=2u8t-8Zpw~9q+KDn$~Ig>iTQNyPKb0|X*&tz zo3%0{CFa1zK}^T&nkJVa{u}(JFTRuJ-u!-=I&&$Fb9N_RtJ+a}T^TzXpRWnO?F=FC zvJ`q0od4-GLZ{n)a+kXipTYU7ZNL+mzfN_}(dvx0+`ZZdJ6qiI365tiG*M9PtdyNfG+QmbQak(bJ3fep(Roj zukqCj9raMadk|NozxWrpLV1dRjke(t1<3)mZ|zt6 zBG#U~0AKLz)H7&~9au`B5i4)Es(dZ3On&u*SjDl)!mie; zgQg_$r|RJRql56StUfe#c*?oB@gxCPE~zZi<*SEE+z(LJFE^4mghAKUTbvM#d+4sD z!#MOcB-gwba^X_~l?&A`^@5;ZSn;c13R1l*oDtFoMTghmk2+sb=Oww0XFGC!E8kUS z;!*s{vjahVh`*WrsI90NgR6KIg)Vu?6F?NS3t@$cXc3doDr%LMzRlG5ZURJ5#H{eP zVPkKqj$8S6qbs?oCX4bZw4hDQCG60iltXSmIQ|CG;OG_eFc#K1*W%dngu5b9%f8NP z*~`?`s#C1975i`;s+8!n0bGk}fK{aG7~9~SP0{x8uw@IA49DR1XZR`N(Gq7JuaTGL zt94OS)fKijo1qEkdxq6QXjO{N!&9G*65BzTL8`R+p8^R5WK19^PAVZ{G2yHKt?#SKW#HhXM zmtHh4$L7H-E#vzMQ^44{Tu)eH1Oc$t5^_#<3#)`QQ^R@3H8K39FNz8zq=moTyh?l zzn4aqE~f3@(=rdfNY5W-+x>P9Irtf_e`_h9k5jr1B0@r(Q*kA5_#>kjZnHB~T#qY( zxL5+yur|6}ik;n!=)x963HE!thHT5AKC&C-UO)N zbBqccL=||2sQ5Ra>UixW`oDomWF;?=$!^FxwMGIRuT_K!7i}8^q~x+huf;XRqbupa z(yoEXR5fr*j@qB%D4902eDmz6*b=h~ycwnOo-wxZ2G#~Xze};&P{tLeiDKQ(Sf=1-;_ zxc$vXH`53lg?hoZ+~hhN!4-#JI*1*tEJY>u!PV0t!i{*tO!}GKX<>(dnIA&Z?ds|lQodpAA zo8{HAm@?jKe?VmlOs2$1UQfg`p^Qy(GalMLli1?%cCD--)SHKHpVEW050-0W?@GvB zCHI|;tsJk(9(1QszO8-B5vTvHrxNfApRWo3pvrc$cjkB+dHdg`k+W~6t-t?2=~!!6 z+Ekv1xQ7uV_da_PO#uk{h&OIk8&7L!7d*XtD>ioRBeU-!rYA z-0kWiN1h3PFJf=NTEQaj2E>}d6MJH=qL)#Pe$Lg4MQK8FDXyxU&dFf|-TZABCe=1Q zMgR2PO-|o4Ij}c3jm6Cq-}|$CkM3?8xlN?;-ks0x@16*i2P=0t_hlxbHSFuJQ|{p$ z`1Akazz-SfHR119p$^a(Nf2~p{%{^6OBd1(cl2(d=`(^Rf?T{O1nqckO0_04Hz`M{ z%9))ucEQYY72A%(ElxeVBXsu3~! z*MHl%J7SiP?udu=Ef-6_4<&^@I9&!;j%^6c|I*%#TJ7JhY!~-73oLX{f z{5WnroVCs>+?#Y?U->Kxp^BWFD6;ejqXvl=|3Ey%zG6M!9<2|#B@mwS=>cT%tutCrT{dbk}VJuyJw{GCXGyD`1W z+1b6J=WtvZfG>`(V9NRG{Cc6h76P~{lAM22?I=m(qcf}o_~chUn`!7xq<$qaOHF#5 z5%hyB2%BLhR=&%#A6(_vn^+@$iPz`Zjk*s=hu3b)y$-;Gf3!tCZ5;Z^}=s-o@_;IBGaIH(QVKM)X$0H0hslUpaJ1k?05)L6US2GDqg)j$mvR&0wUtHk<_16 zvna;e>JG=oE*;h@9PQvWn@C)V zy4-#ccfLKP=q+5X3}Ok(YQYV!EbDL7dJIwqk+x2ngQx-W!mp0=p~kwI;Wv2g>Hq*`lHHw12 z$D;%cdR7rSGd>8o;XkO6%;RpE*e$Gqe$cDT3WU1f{T$}V)vc#%%jGD+NFnA?ebao! zIFZwr`Kj}nCk$7_nn}^$Gc{|9c=#T}-1+1JcKF%Y%_o03nOnyll+oAzuL=J?`hZ8k zr>?x8CNI60MwkRPAove&a9KMWyc&#ZA=_7jCvRL0Kuo9fu!^UE-L!J4>g`_2JY&M> zh$g|$L_ZIvj==qW#9(L)>_N1l8>>ucU`S)eTbmYq!4~gNLoOW2H8aZ4jaz&#+X78hhi&Kd~pE zXa~X{ducFD%bclkg*__KUp$*8v6rO^=dh}q{n9yVey>G6`)KhvmCNbfga*OL$v1-I zSbusi3___|CRg7=3&;*VXiVktu1>!fz0AV>AhT3UfsHHo0eT&TKm5VSMDb$I;Wj8| z129)n+Qc%$b$^F%@!V$;15(DjN?d>%XZ=z~Ye3b0^-B$2y+BBU=vDxck+-@;U`0uyzo>%vjO?F7BRoly_;RT4N*RFE85b+eO1S+Oc)FUy^viy8n z*L|-+F80LS{ACreiVEoJ(06^@w4Q-&T!X%d%CbS!BR35~J=HOUIX0U$S{ajDGlcMe|Z zgR25JxL%A{bmHyI(;i=YMy-fyppmFGzZEj*lUVOW z7n4a~la9R1($kioJchBU;LL&B%>iqNcB3lW4d2L|Kiq%Z^kpv!R|eBT=jd+A(#TW^ zDoHhhc75%E7g6y%DAHY193rr!Ux8MiwQppRKTM)=w!Y3U@Dk;+hf+t-e~4ro$qkzC zcE{2ukv?9#nB zZz?uTs7_bvo9?HLpWsw3T0CPIb{hqc<8Igz4bJ&NLuBL_%hhUpbeNS#GyZeNj)2I0o4O{|C&M2T)KH?=Fq-ZXlB_A`*9ch)H6ggXgaXLtba z2YrpX_oW+6_jh-UUV_(-(Uy(l7ZvEA{3~I(X4S~YKae@u0#?w}@8a;IGblVNzZ_b8 z8qb2Ocnt(B0F7reZp3SP07ss)&oD^y$%7iQ3soc4eZE?Y)|;!-PEw{{7$jt6>q&Mu zan)>7m9t%w{gV^dCi`aYqpdxA%-EWM)8~%e#dBd+WoLXUDIj$f_hq=)xq;YEAOhBI z&sZF~8Jv^SS6us_+imgZI9r1+Vxi{0!-OtnJZOk_# zzg%Vl2(>i0k%r(fq+ z6T`mW_e7Ojr>p1#lj%U${1_I&Ak0pR)@4~e35TCL`&o@s%eI&=hT~V%qe!$vY}Sp} z{!UykO?O;{K{HgSH%^@6-i)*9DQ8^9R7oSGy{DeR5dw?|!ypwi9UhnN}60 z#B7A_@mgZ|Eblo=;d8^k4k|Czv!kvmKkL&b(OoR9!i=-S7x) z9+;-E!_(0|U2#FG3aGc)sl}t|V@;@R&7Dl!vx`{7Lmz3N$46Q_c7Lg7cy*no${Eu&AyXQ`tT%L{7hI0Ia2|xWt!tZ{70|BD1 z*e3=~ES0+ko`VbjFl^>X8#H1LRK){EUk|Qf;d&yrJqDua3aCFFD%FP%tED*VAwHu% z^z^I1`Xpw2R(!A@em8N%cjXZj%y7TOo&9s0;H9hM)-QPGJZAVvFhCo?0n_Vwe>`3dr0`FwHuD1 zhvyIPggtV(|DaK9@)jAW9c+iqpdB`icidlSqd0^&N6#=AhlSfEOEFO64@oxD?84pn zECjW1RuUJvNae{M+gA-%mDoWtMs;O#F^x$Rwb^n9ADp0*h$fm9@oX4z29qZzFQQF%DzCIV z`p$6Kd=1sEf!Bn;T^9#_8}Wb%mpX4_ws{8`{U!utl?|y0w05Q-NMT>RCN`bPNI*2< zEa6&y$VJl?5!=RptZ8g2h@Bk&?n}u|>|e&0qjd0qe(@YJ4NwP8f}R9*(@^4~m`q|} zSUBp-{gQktRKoA-s+tH?xvL2Wc@-Z&+d!fp+ErCM1Q#ozw^U3Vh3Jb*QagrXN^8P% z8#&qh*E;am&j~mvtb!i`q#x#9zdh3Fjc_0zC+27qS>A@rei~QHZKcVD$x552Ff3v+1L)}{cP5|K|dsQBR^`9#U zach66?77Ib=9K~yG@Pmg-#wf>$vjOHX3zGk%d5w3t~Cg8 z0VQ5@(r(6m{l1?X_&QLL{gmg+ft3?Nk-zQ4f4Ka)&DfqQkS#$FOjHqIP(7~L5JqCYZ%U$Z_-zJOB} zZCnYJxFr6j7Apng($2+nwmjz!_p6$Yd9p2Xs}zJ)X3J-kp{n?<`tOFUqtq`|`Aw)R zZVkdqfy#c(uXYOUw<=~6tch%2?`F!pb!N`VI3`fcDI4wtDl?Stmu@Fe#6#-%Tknn; zH+DT3sh<7_2H0FtC&hM$lHYdPNA>MB;XkaFhZ7l@J&{H(AnR8Q0AX5PgLAy`Nf@P? zg-A^wU+mFcmT;_Nu}~fU!ZFElDvOTqib2iUlq2E?p(|pCePzFMTsEQ_&>T^N01I3b zwt=R8?F#QlXlvL3FC{4mM#)Xt*bb!#?qQXXOIYOm)%A9BJSKu5Ksi>n^)j8NFp8)8 ztn&DNPNKi^_S<$K1(avSTUUFT1 z*F@2}X@+%A7$tt=*#r;o9*@e`DYAt5EbUPIOt~xlc2FBUu$8#+G4PLlG5Tbq&I_=U z==yTz`1LcX0$*UlpZax@Skd^};|Fn-;u@;a+b1tU^q10z+`fcg(Pv$zCWhDV`>laE zU%KB&xl1(uGO$PLj=f)OgW(Ik2yUv`K?g_q@&bZ(^ zF8!9bmtdip;szyfT32lm$95g{EC>JEsRsBSe=NlQVhl#ujjQ=RC=Fo_%aI0k(a(i8 z53+hSa!Q}w@-h{epfI6a^$$0Nog}&^I3W z?LVe%i1>3X3fiIXfA(Pzehsrpgp_}V3MZZ3v9k>qyVIHqe-F24w_LmbRCZn05<0os z5ML-_It1eCupO#y%K)#_Yz}xZbbYoxD(ubh?h0>)^~((5g4pNO)|uey$}u*@<@DV| z4^EzVQ7rjMU{EpaIbZIkx}Tv3>jc4msGXPiSso;sa6#tP5& zF;74?i%`(leg#bwVgPW7bin6+r=Ofmox?_|C9@5$0k0BwC&hlH4;eobsS~Q|o-0ot zr5nHeNwlBsq-hexoFNWkG6?g_^syb)HW$yQboskM^hdfbTpy5?*WUZ6f!5S`hb&>w zsAF_;Z;8q>O$J&Fn&CUjo(VJnGAD)XmCg{vzYv}V%vbJJA^>+UvEE^MI&$h#8b#(0 zHAnM=0U(j*)~zu_2kRUn@&+L&S6;^2!Bqsl|9QxpofJklSt&r2Brf3`80Ck@-Zei6 z0{=ll{VKIdA8d#2xXtkH_yOM?p#fxg17YVEK@HsTYS!iW6Ibp+I8Vhnc|X8}^6J7Bzz8b<<*Lv}CfG z@si>$Stua6twBSG4vui%!MBAvH=Em(PmOQr@2ivMgO#(yR2GKupCNy|PkntPuwL9DzXe~@@C~0`7 zkix;WPNQj?;=mnlh$Oe@KwDcHGoo|I={7po5YfLx5Nd@w8aew`8s)&r)W&ie=f2+M z+n=Qkma|(-1gawL97iu%dnL*-AvVYk(vBOJ|L{dDeHA-6D#seyJ+6GzK7rEhuY3m` z+;u7kXz6B}wjFg+yT)tw6FAqK-_=+4(?eIs$$d~mc=M+*nE~;T>y*Rv8cSD7iW8hf zqrQwAd5J%M^*H;q1fSDEu{toXa-&W#<80b1?{Bdhh}DB_>)C)H`Z-gb>4?5NE#bQT z@apT`4cGlG{DrFiwlSpvJBwdK9t)98v-e}?Y9{juR<--^}jrmC2PjUuL*dg}*i z8&T*QZL*F3&NAZwMreBjX2|<3I0(t631fj1?=!p;XC(GddP>(w>GkVDYoMi}p#3-yCj=v>UcqWi^D!EPvQEp z(L+a19%k_(ce#DV25j>BlxxaM;7YrvR!G75p8cadaFNRV=$5_JhWVxG{pqAg6rgIZ&> zQGoVz#2$}EF3W2JCK|8!@AKI}Q)22BCWwSn4B!sS4#me;gjX!x`NhI`u=rRW0g`iuni`~TT{uO>^9EWgiF zYb{Dwbyw@|?U~t`*_{;wV0QrnK!OIt14tT;eB(RK_zn0q_{1kZ5HB<%jUbkW71+hj z?&zMGp4Q#fU0P;krPj&c??m{$kvDIul9}Dtt8PYwhs)#Pe?NZw_;DI3T8jH3NV9-t z@#2Fi-`9fK%C(0`FaFdzfBk(WR0-4t2i`THHWa0RF0qivVC(E^i$z7n4uwpb4MjmW zKwhz>0|h@f7zy^SzbLzpnU?+$CNoInZ06+$AN7rolRr_Ycql9BrSjaglT_YE@bbA? zS`9&Knc%h^f#V>7;P@XSoo_-E`w`-TSOo0l z0r20mUrAIzhwY7N8_|$EfIHNI*1zgX+Uvg!DDIhFHBN?x8i?|;nrzJ;LSt$>pN(FP zR?9-qok5FN`ZYwzkIjPD(kr7CVagZ*O1oqG4X(j}tV6FAtTlRF!_@5>i;5n~E$6x6 z;-Tv+0sbZE1ZR(;0w5oYzld`@8LhKN8Gcvl_R701Y^`_l8yY|zlxr8@<_wg=AjKBr9 zkmcIidb$U=FR;Pt;iHALyt10+7nkBX^JqTJEi9%lZ`_UR%OYDh&^9+!8 zW=hhVoRZYeZ?l}KsyI+Q7r?)U68Y*Hg9H+cp6PAzwQD9~z+4w~4%7_mfO(Y4*zxxQh`rJ&z@mtCkk)u5?fo#O5-f71UIvf)d0&FH&vStM zFF*J+-MsrC{pQmz;p}JAS2u1Kq8&APKx)C<1PYyfIeW!89RN6Tdua%Lo7KA}!Xj=M z2?Nw-<{W$xTij&Pn14U}%||ga=<33vDt2eiOiiY9rzg_4-+VQ_dHG^`_jS&aC4I#} z@pFNg`gta66v~Pnb7oLACyW*Q0!u44iG zor=rQYU>^%N-jEw1EHEG(}mElH+%Qcbm?#wOw%hE;#N0_!=nIJMp2a(hL)b zm{X|(LDeVU=NuZJvwuByJw@MD{c~8oNhgvj-IDx$YcLCT%+q`j$E3_@%RmxDgjqLfeg$*8?K@Aj!SAGn@ z3@EN0I1n(!NQ+BsJmUQdAg7yUkhqUnskf(_Ur!ny>`TJ~{mcZ0m?bFNE?rqpQ{Lfn z1;Vr8-Hw2jhkb+X`?8YA85H|8ioF_Z>l^9UA76o+zXQ-;Nmr5A-@Z2! zqMKF3JsZq)B0OO1?#~X6vg>AG6x2iZZVlpp!Fg9pc$oVRXzjn)HBE#bR1Ynyi|reT zGS;;Z1jl=eGTnLbFn#jnb@J~_J%Ii&qM&Kk2Ht%6Lb`n6YXU@y(fv)`a)K1GE6IE~)peD~6dnt4sEavu3o<%o*Jgv|+ z?9>YX)}I$offn$;_%_&C0>uQ#L$?JRWTPGmzK)E)lb`sovtQbgoN^;;HJ+VoKbJc< zVJf#}qB~RGl}s5S%CKM1vTyIJ6<8)B7p;t54qlnOa(d+6BxG2FNbVhA2+(J}9+lY?eGbJrOg3@zEi~MF(%5jQv$d!OI;DTd(Mz zN%$hSXTH{>?YavQ5A9bB+I2NflkuJQj5KoR{*}{y%%v02kZPq?RTBuV|=GHFjjm0G}mTw4inX=5kjHh{7P@Vm$x zW(X*6oc?65xBr2PF0vgCiK|djxsL&%LWpKxa3y&0ndk70fpfrO@mH`|M|87<2F{H; z_tV3WkgE zxfEXQeEy67y8TP2D`Rzu^_#`>_)8@RYQNwLa`b`L#zV~q8)T~fU_3Y&E^{xtCnpH#!hdMDd>2iM$9z?GT&`_&XzzBm6I

**1T)w6T+ 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

2;8@}GO(O;Z#V6z)AVlY6I0;Z=4KEFV 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 - h.id should not be h.sourceField("uuid") - } - - response.result.hits.hits - .map( - _.sourceField("name") - ) should contain allOf ("Homer Simpson", "Moe Szyslak", "Barney Gumble") - } - - "Bulk index valid json with an id key but no suffix key" should "work" in { - pClient.jestClient.execute(new CreateIndex.Builder("person2").build()) - val childMapping = new PutMapping.Builder( - "person2", - "child", - "{ \"child\" : { \"_parent\" : {\"type\": \"person\"}, \"properties\" : { \"name\" : {\"type\" : \"string\", \"index\" : \"not_analyzed\"} } } }" - ).build() - pClient.jestClient.execute(childMapping) - - implicit val bulkOptions: BulkOptions = BulkOptions("person2", "person", 1000) - implicit val jclient: JestClient = pClient.jestClient - val indices = pClient.bulk[String](persons.iterator, identity, Some("uuid"), None, None) - refresh(indices) - - indices should contain only "person2" - - blockUntilCount(3, "person2") - - "person2" should haveCount(3) - - val response = client.execute { - search("person2").query(MatchAllQuery()) - } complete () - - response.result.hits.hits.foreach { h => - h.id shouldBe h.sourceField("uuid") - } - - response.result.hits.hits - .map( - _.sourceField("name") - ) should contain allOf ("Homer Simpson", "Moe Szyslak", "Barney Gumble") - - // FIXME elastic >= v 6.x no more multiple Parent / Child relationship allowed within the same index -// val childIndices = -// pClient.bulk[String](children.iterator, identity, None, None, None, None, None, Some("parentId"))( -// jclient, -// BulkOptions("person2", "child", 1000), -// system) -// pClient.refresh("person2") -// -// childIndices should contain only "person2" -// -// blockUntilCount(2, "person2", "child") -// -// "person2" should haveCount(5) - } - - "Bulk index valid json with an id key and a suffix key" should "work" in { - implicit val bulkOptions: BulkOptions = BulkOptions("person", "person", 1000) - implicit val jclient: JestClient = pClient.jestClient - val indices = - pClient.bulk[String](persons.iterator, identity, Some("uuid"), Some("birthDate"), None, None) - refresh(indices) - - indices should contain allOf ("person-1967-11-21", "person-1969-05-09") - - blockUntilCount(2, "person-1967-11-21") - blockUntilCount(1, "person-1969-05-09") - - "person-1967-11-21" should haveCount(2) - "person-1969-05-09" should haveCount(1) - - val response = client.execute { - search("person-1967-11-21", "person-1969-05-09").query(MatchAllQuery()) - } complete () - - response.result.hits.hits.foreach { h => - h.id shouldBe h.sourceField("uuid") - } - - response.result.hits.hits - .map( - _.sourceField("name") - ) should contain allOf ("Homer Simpson", "Moe Szyslak", "Barney Gumble") - } - - "Bulk index invalid json with an id key and a suffix key" should "work" in { - implicit val bulkOptions: BulkOptions = BulkOptions("person_error", "person", 1000) - implicit val jclient: JestClient = pClient.jestClient - intercept[JsonParseException] { - val invalidJson = persons :+ "fail" - pClient.bulk[String](invalidJson.iterator, identity, None, None, None) - } - } - - "Bulk upsert valid json with an id key but no suffix key" should "work" in { - implicit val bulkOptions: BulkOptions = BulkOptions("person4", "person", 1000) - implicit val jclient: JestClient = pClient.jestClient - val indices = - pClient - .bulk[String](personsWithUpsert.iterator, identity, Some("uuid"), None, None, Some(true)) - refresh(indices) - - indices should contain only "person4" - - blockUntilCount(3, "person4") - - "person4" should haveCount(3) - - val response = client.execute { - search("person4").query(MatchAllQuery()) - } complete () - - response.result.hits.hits.foreach { h => - h.id shouldBe h.sourceField("uuid") - } - - response.result.hits.hits - .map( - _.sourceField("name") - ) should contain allOf ("Homer Simpson", "Moe Szyslak", "Barney Gumble2") - } - - "Bulk upsert valid json with an id key and a suffix key" should "work" in { - implicit val bulkOptions: BulkOptions = BulkOptions("person5", "person", 1000) - implicit val jclient: JestClient = pClient.jestClient - val indices = pClient.bulk[String]( - personsWithUpsert.iterator, - identity, - Some("uuid"), - Some("birthDate"), - None, - Some(true) - ) - refresh(indices) - - indices should contain allOf ("person5-1967-11-21", "person5-1969-05-09") - - blockUntilCount(2, "person5-1967-11-21") - blockUntilCount(1, "person5-1969-05-09") - - "person5-1967-11-21" should haveCount(2) - "person5-1969-05-09" should haveCount(1) - - val response = client.execute { - search("person5-1967-11-21", "person5-1969-05-09").query(MatchAllQuery()) - } complete () - - response.result.hits.hits.foreach { h => - h.id shouldBe h.sourceField("uuid") - } - - response.result.hits.hits - .map( - _.sourceField("name") - ) should contain allOf ("Homer Simpson", "Moe Szyslak", "Barney Gumble2") - } - - "Count" should "work" in { - implicit val bulkOptions: BulkOptions = BulkOptions("person6", "person", 1000) - implicit val jclient: JestClient = pClient.jestClient - val indices = - pClient - .bulk[String](personsWithUpsert.iterator, identity, Some("uuid"), None, None, Some(true)) - refresh(indices) - - indices should contain only "person6" - - blockUntilCount(3, "person6") - - "person6" should haveCount(3) - - import scala.collection.immutable.Seq - - pClient.countAsync(JSONQuery("{}", Seq[String]("person6"), Seq[String]())) complete () match { - case Success(s) => s.getOrElse(0d).toInt should ===(3) - case Failure(f) => fail(f.getMessage) - } - } - - "Search" should "work" in { - implicit val bulkOptions: BulkOptions = BulkOptions("person7", "person", 1000) - implicit val jclient: JestClient = pClient.jestClient - val indices = - pClient - .bulk[String](personsWithUpsert.iterator, identity, Some("uuid"), None, None, Some(true)) - refresh(indices) - - indices should contain only "person7" - - blockUntilCount(3, "person7") - - "person7" should haveCount(3) - - pClient.searchAsync[Person](SQLQuery("select * from person7")) assert { - _.size should ===(3) - } - - pClient.searchAsync[Person](SQLQuery("select * from person7 where _id=\"A16\"")) assert { - _.size should ===(1) - } - - } - - "Get all" should "work" in { - implicit val bulkOptions: BulkOptions = BulkOptions("person8", "person", 1000) - implicit val jclient: JestClient = pClient.jestClient - val indices = - pClient - .bulk[String](personsWithUpsert.iterator, identity, Some("uuid"), None, None, Some(true)) - refresh(indices) - - indices should contain only "person8" - - blockUntilCount(3, "person8") - - "person8" should haveCount(3) - - val response = pClient.search[Person]("select * from person8") - - response.size should ===(3) - - } - - "Get" should "work" in { - implicit val bulkOptions: BulkOptions = BulkOptions("person9", "person", 1000) - implicit val jclient: JestClient = pClient.jestClient - val indices = - pClient - .bulk[String](personsWithUpsert.iterator, identity, Some("uuid"), None, None, Some(true)) - refresh(indices) - - indices should contain only "person9" - - blockUntilCount(3, "person9") - - "person9" should haveCount(3) - - val response = pClient.get[Person]("A16", Some("person9")) - - response.isDefined shouldBe true - response.get.uuid shouldBe "A16" - - } - - "Index" should "work" in { - implicit val jclient: JestClient = sClient.jestClient - val uuid = UUID.randomUUID().toString - val sample = Sample(uuid) - val result = sClient.index(sample) - result shouldBe true - - val result2 = sClient.get[Sample](uuid) - result2.isDefined shouldBe true - result2.get.uuid shouldBe uuid - } - - "Update" should "work" in { - implicit val jclient: JestClient = sClient.jestClient - val uuid = UUID.randomUUID().toString - val sample = Sample(uuid) - val result = sClient.update(sample) - result shouldBe true - - val result2 = sClient.get[Sample](uuid) - result2.isDefined shouldBe true - result2.get.uuid shouldBe uuid - } - - "Delete" should "work" in { - implicit val jclient: JestClient = sClient.jestClient - val uuid = UUID.randomUUID().toString - val sample = Sample(uuid) - val result = sClient.index(sample) - result shouldBe true - - val result2 = sClient.delete(sample.uuid, Some("sample"), Some("sample")) - result2 shouldBe true - - val result3 = sClient.get(uuid) - result3.isEmpty shouldBe true - } - - "Index binary data" should "work" in { - implicit val jclient: JestClient = bClient.jestClient - bClient.createIndex("binaries") shouldBe true - val mapping = - """{ - | "test": { - | "properties": { - | "uuid": { - | "type": "keyword", - | "index": true - | }, - | "createdDate": { - | "type": "date" - | }, - | "lastUpdated": { - | "type": "date" - | }, - | "content": { - | "type": "binary" - | }, - | "md5": { - | "type": "keyword" - | } - | } - | } - |} - """.stripMargin - bClient.setMapping("binaries", "test", mapping) shouldBe true - for (uuid <- Seq("png", "jpg", "pdf")) { - val path = - Paths.get(Thread.currentThread().getContextClassLoader.getResource(s"avatar.$uuid").getPath) - import app.softnetwork.utils.ImageTools._ - import app.softnetwork.utils.HashTools._ - import app.softnetwork.utils.Base64Tools._ - val encoded = encodeImageBase64(path).getOrElse("") - val binary = Binary( - uuid, - content = encoded, - md5 = hashStream(new ByteArrayInputStream(decodeBase64(encoded))).getOrElse("") - ) - bClient.index(binary) shouldBe true - bClient.get[Binary](uuid) match { - case Some(result) => - val decoded = decodeBase64(result.content) - val out = Paths.get(s"/tmp/${path.getFileName}") - val fos = Files.newOutputStream(out) - fos.write(decoded) - fos.close() - hashFile(out).getOrElse("") shouldBe binary.md5 - case _ => fail("no result found for \"" + uuid + "\"") - } - } - } -} diff --git a/elastic/testkit/src/test/scala/app/softnetwork/elastic/client/JestProviders.scala b/elastic/testkit/src/test/scala/app/softnetwork/elastic/client/JestProviders.scala deleted file mode 100644 index 5e7a58e2..00000000 --- a/elastic/testkit/src/test/scala/app/softnetwork/elastic/client/JestProviders.scala +++ /dev/null @@ -1,38 +0,0 @@ -package app.softnetwork.elastic.client - -import app.softnetwork.elastic.client.jest.JestProvider -import app.softnetwork.elastic.model.{Binary, Sample} -import app.softnetwork.persistence.ManifestWrapper -import app.softnetwork.persistence.person.model.Person -import com.typesafe.config.Config -import io.searchbox.client.JestClient - -object JestProviders { - - class PersonProvider(es: Config) extends JestProvider[Person] with ManifestWrapper[Person] { - override protected val manifestWrapper: ManifestW = ManifestW() - - override lazy val config: Config = es - - implicit lazy val jestClient: JestClient = - apply(elasticConfig.credentials, elasticConfig.multithreaded) - } - - class SampleProvider(es: Config) extends JestProvider[Sample] with ManifestWrapper[Sample] { - override protected val manifestWrapper: ManifestW = ManifestW() - - override lazy val config: Config = es - - implicit lazy val jestClient: JestClient = - apply(elasticConfig.credentials, elasticConfig.multithreaded) - } - - class BinaryProvider(es: Config) extends JestProvider[Binary] with ManifestWrapper[Binary] { - override protected val manifestWrapper: ManifestW = ManifestW() - - override lazy val config: Config = es - - implicit lazy val jestClient: JestClient = - apply(elasticConfig.credentials, elasticConfig.multithreaded) - } -} diff --git a/elastic/testkit/src/test/scala/app/softnetwork/elastic/model/Binary.scala b/elastic/testkit/src/test/scala/app/softnetwork/elastic/model/Binary.scala deleted file mode 100644 index 55e82460..00000000 --- a/elastic/testkit/src/test/scala/app/softnetwork/elastic/model/Binary.scala +++ /dev/null @@ -1,13 +0,0 @@ -package app.softnetwork.elastic.model - -import app.softnetwork.persistence.model.Timestamped - -import java.time.Instant - -case class Binary( - uuid: String, - var createdDate: Instant = Instant.now(), - var lastUpdated: Instant = Instant.now(), - content: String, - md5: String -) extends Timestamped diff --git a/elastic/testkit/src/test/scala/app/softnetwork/elastic/model/Sample.scala b/elastic/testkit/src/test/scala/app/softnetwork/elastic/model/Sample.scala deleted file mode 100644 index bf9cf5b3..00000000 --- a/elastic/testkit/src/test/scala/app/softnetwork/elastic/model/Sample.scala +++ /dev/null @@ -1,13 +0,0 @@ -package app.softnetwork.elastic.model - -import app.softnetwork.persistence.model.Timestamped - -import java.time.Instant - -/** Created by smanciot on 12/04/2020. - */ -case class Sample( - uuid: String, - var createdDate: Instant = Instant.now(), - var lastUpdated: Instant = Instant.now() -) extends Timestamped diff --git a/elastic/testkit/src/test/scala/app/softnetwork/persistence/person/MySQLPersonToElasticHandlerSpec.scala b/elastic/testkit/src/test/scala/app/softnetwork/persistence/person/MySQLPersonToElasticHandlerSpec.scala deleted file mode 100644 index 1c2c81b4..00000000 --- a/elastic/testkit/src/test/scala/app/softnetwork/persistence/person/MySQLPersonToElasticHandlerSpec.scala +++ /dev/null @@ -1,7 +0,0 @@ -package app.softnetwork.persistence.person - -import app.softnetwork.elastic.scalatest.EmbeddedElasticTestKit - -class MySQLPersonToElasticHandlerSpec - extends MySQLPersonToElasticTestKit - with EmbeddedElasticTestKit diff --git a/elastic/testkit/src/test/scala/app/softnetwork/persistence/person/PersonToElasticHandlerSpec.scala b/elastic/testkit/src/test/scala/app/softnetwork/persistence/person/PersonToElasticHandlerSpec.scala deleted file mode 100644 index c7e0a28c..00000000 --- a/elastic/testkit/src/test/scala/app/softnetwork/persistence/person/PersonToElasticHandlerSpec.scala +++ /dev/null @@ -1,31 +0,0 @@ -package app.softnetwork.persistence.person - -import akka.actor.typed.ActorSystem -import app.softnetwork.elastic.scalatest.EmbeddedElasticTestKit -import app.softnetwork.persistence.person.query.{ - PersonToElasticProcessorStream, - PersonToExternalProcessorStream -} -import app.softnetwork.persistence.query.{InMemoryJournalProvider, InMemoryOffsetProvider} -import app.softnetwork.persistence.scalatest.InMemoryPersistenceTestKit -import com.typesafe.config.Config -import org.slf4j.{Logger, LoggerFactory} - -class PersonToElasticHandlerSpec - extends PersonToElasticTestKit - with InMemoryPersistenceTestKit - with EmbeddedElasticTestKit { - - lazy val log: Logger = LoggerFactory getLogger getClass.getName - - override def person2ExternalProcessorStream: ActorSystem[_] => PersonToExternalProcessorStream = - sys => { - new PersonToElasticProcessorStream with InMemoryJournalProvider with InMemoryOffsetProvider { - lazy val log: Logger = LoggerFactory getLogger getClass.getName - override def config: Config = PersonToElasticHandlerSpec.this.elasticConfig - - override val forTests: Boolean = true - override implicit def system: ActorSystem[_] = sys - } - } -} diff --git a/elastic/testkit/src/test/scala/app/softnetwork/persistence/person/PostgresPersonToElasticHandlerSpec.scala b/elastic/testkit/src/test/scala/app/softnetwork/persistence/person/PostgresPersonToElasticHandlerSpec.scala deleted file mode 100644 index 647e5bb9..00000000 --- a/elastic/testkit/src/test/scala/app/softnetwork/persistence/person/PostgresPersonToElasticHandlerSpec.scala +++ /dev/null @@ -1,7 +0,0 @@ -package app.softnetwork.persistence.person - -import app.softnetwork.elastic.scalatest.EmbeddedElasticTestKit - -class PostgresPersonToElasticHandlerSpec - extends PostgresPersonToElasticTestKit - with EmbeddedElasticTestKit diff --git a/jdbc/build.sbt b/jdbc/build.sbt index c4e17691..9ca9227d 100644 --- a/jdbc/build.sbt +++ b/jdbc/build.sbt @@ -10,7 +10,7 @@ val akkaPersistenceJdbc = Seq( "com.typesafe.slick" %% "slick" % Versions.slick, "com.typesafe.slick" %% "slick-hikaricp" % Versions.slick, "org.postgresql" % "postgresql" % Versions.postgresql, - "com.mysql" % "mysql-connector-j" % "8.0.33" + "com.mysql" % "mysql-connector-j" % Versions.mysql ) libraryDependencies ++= akkaPersistenceJdbc diff --git a/jdbc/src/main/scala/app/softnetwork/persistence/jdbc/query/JdbcEventProcessorOffsets.scala b/jdbc/src/main/scala/app/softnetwork/persistence/jdbc/query/JdbcEventProcessorOffsets.scala index d0c8ed8f..08918b6f 100644 --- a/jdbc/src/main/scala/app/softnetwork/persistence/jdbc/query/JdbcEventProcessorOffsets.scala +++ b/jdbc/src/main/scala/app/softnetwork/persistence/jdbc/query/JdbcEventProcessorOffsets.scala @@ -2,14 +2,14 @@ package app.softnetwork.persistence.jdbc.query import com.typesafe.config.{Config, ConfigFactory} import com.typesafe.scalalogging.StrictLogging -import configs.Configs +import configs.ConfigReader case class JdbcEventProcessorOffsets(schema: String, table: String) object JdbcEventProcessorOffsets extends StrictLogging { def apply(config: Config): JdbcEventProcessorOffsets = { - Configs[JdbcEventProcessorOffsets] - .get( + ConfigReader[JdbcEventProcessorOffsets] + .read( config.withFallback(ConfigFactory.load("softnetwork-jdbc-persistence.conf")), "jdbc-event-processor-offsets" ) diff --git a/jdbc/src/main/scala/app/softnetwork/persistence/jdbc/query/JdbcStateProvider.scala b/jdbc/src/main/scala/app/softnetwork/persistence/jdbc/query/JdbcStateProvider.scala index fcd77887..3002a0c6 100644 --- a/jdbc/src/main/scala/app/softnetwork/persistence/jdbc/query/JdbcStateProvider.scala +++ b/jdbc/src/main/scala/app/softnetwork/persistence/jdbc/query/JdbcStateProvider.scala @@ -243,7 +243,7 @@ trait JdbcStateProvider[T <: Timestamped] ): Boolean = { val action = (states += (uuid, lastUpdated, deleted, state)).map(_ > 0) - db.run(action) complete () match { + db.run(action).complete() match { case Success(value) => log.debug(s"Insert to $tableFullName with $uuid -> $value") value @@ -278,7 +278,7 @@ trait JdbcStateProvider[T <: Timestamped] (uuid, lastUpdated, deleted, state) ) .map(_ > 0) - db.run(action) complete () match { + db.run(action).complete() match { case Success(value) => if (deleted) { log.debug(s"Delete from $tableFullName with $uuid -> $value") @@ -301,7 +301,7 @@ trait JdbcStateProvider[T <: Timestamped] */ def destroy(uuid: String): Boolean = { val action = states.filter(_.uuid === uuid).delete.map(_ > 0) - db.run(action) complete () match { + db.run(action).complete() match { case Success(value) => log.debug(s"Delete from $tableFullName with $uuid -> $value") value @@ -321,7 +321,7 @@ trait JdbcStateProvider[T <: Timestamped] def load(uuid: String): Option[T] = { implicit val manifest: Manifest[T] = manifestWrapper.wrapped val action = states.filter(_.uuid === uuid).result.headOption - db.run(action) complete () match { + db.run(action).complete() match { case Success(value) => value match { case Some(document) => @@ -361,7 +361,7 @@ trait JdbcStateProvider[T <: Timestamped] SELECT state FROM $tableFullName WHERE $query """.as[String].map(_.toList) } - db.run(action) complete () match { + db.run(action).complete() match { case Success(value) => log.debug(s"Search $tableFullName with $query -> $value") value.map(readState) diff --git a/project/Versions.scala b/project/Versions.scala index 3b651723..dfeebef2 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,36 +1,38 @@ object Versions { - val akka = "2.6.20" + val akka = "2.6.20" // TODO 2.6.20 -> 2.8.3 - val akkaHttp = "10.2.10" + val akkaHttp = "10.2.10" // TODO 10.2.10 -> 10.5.3 val akkaHttpJson4s = "1.39.2" //1.37.0 -> 1.39.2 - val akkaHttpSession = "0.7.0" + val akkaHttpSession = "0.7.1" // 0.7.0 -> 0.7.1 val tapir = "1.7.0" val tapirHttpSession = "0.2.0" - val akkaPersistenceJdbc = "5.0.4" + val akkaPersistenceJdbc = "5.0.4" // TODO 5.0.4 -> 5.2.1 - val akkaManagement = "1.1.4" // 1.1.4 -> 1.2 + val akkaManagement = "1.1.4" // TODO 1.1.4 -> 1.4.1 - val postgresql = "42.2.18" + val postgresql = "42.2.18" // TODO 42.2.18 -> 42.7.7 - val scalatest = "3.2.16" + val mysql = "8.4.0" // 8.0.33 -> 8.4.0 - val typesafeConfig = "1.4.2" + val scalatest = "3.2.19" // 3.2.16 -> 3.2.19 - val kxbmap = "0.4.4" + val typesafeConfig = "1.4.3" - val jackson = "2.12.7" // 2.11.4 -> 2.12.7 + val kxbmap = "0.6.1" + + val jackson = "2.19.0" // 2.12.7 -> 2.19.0 val json4s = "4.0.6" // 3.6.12 -> 4.0.6 val scalaLogging = "3.9.2" - val logback = "1.2.3" + val logback = "1.2.3" // TODO 1.2.3 -> 1.5.6 val slf4j = "1.7.36" diff --git a/session/common/build.sbt b/session/common/build.sbt index 2f9393f5..7bd023c6 100644 --- a/session/common/build.sbt +++ b/session/common/build.sbt @@ -13,7 +13,7 @@ val akkaHttpSession: Seq[ModuleID] = Seq( ) libraryDependencies ++= Seq( - "app.softnetwork.protobuf" %% "scalapb-extensions" % "0.1.7" + "app.softnetwork.protobuf" %% "scalapb-extensions" % "0.2.0" ) ++ akkaHttpSession ++ tapirHttpSession Compile / unmanagedResourceDirectories += baseDirectory.value / "src/main/protobuf" diff --git a/session/common/src/main/scala/app/softnetwork/session/model/JwtClaimsEncoder.scala b/session/common/src/main/scala/app/softnetwork/session/model/JwtClaimsEncoder.scala index d94951d8..fc3c39ce 100644 --- a/session/common/src/main/scala/app/softnetwork/session/model/JwtClaimsEncoder.scala +++ b/session/common/src/main/scala/app/softnetwork/session/model/JwtClaimsEncoder.scala @@ -31,7 +31,7 @@ trait JwtClaimsEncoder extends SessionEncoder[JwtClaims] with Completion { ) (updatedJwtClaims.iss match { case Some(iss) => - (loadApiKey(iss) complete ()).toOption.flatten + loadApiKey(iss).complete().toOption.flatten case _ => None }) match { case Some(apiKey) if apiKey.clientSecret.isDefined => @@ -51,7 +51,7 @@ trait JwtClaimsEncoder extends SessionEncoder[JwtClaims] with Completion { else jwtClaims.iss val innerConfig = (maybeClientId match { case Some(clientId) => - (loadApiKey(clientId) complete ()).toOption.flatten.flatMap(_.clientSecret) + loadApiKey(clientId).complete().toOption.flatten.flatMap(_.clientSecret) case _ => None }) match { case Some(clientSecret) => diff --git a/session/testkit/src/main/scala/app/softnetwork/session/scalatest/RefreshableSessionTestKit.scala b/session/testkit/src/main/scala/app/softnetwork/session/scalatest/RefreshableSessionTestKit.scala index 5e86844b..26362a90 100644 --- a/session/testkit/src/main/scala/app/softnetwork/session/scalatest/RefreshableSessionTestKit.scala +++ b/session/testkit/src/main/scala/app/softnetwork/session/scalatest/RefreshableSessionTestKit.scala @@ -19,7 +19,7 @@ trait RefreshableSessionTestKit[T <: SessionData with SessionDataDecorator[T]] value match { case Some(value) => refreshable.refreshTokenManager - .sessionFromValue(value) complete () match { + .sessionFromValue(value).complete() match { case Success(value) => value match { case _ @SessionResult.CreatedFromToken(session) => Some(session) diff --git a/session/testkit/src/main/scala/app/softnetwork/session/scalatest/SessionTestKit.scala b/session/testkit/src/main/scala/app/softnetwork/session/scalatest/SessionTestKit.scala index 6c6d9376..ff01ac6a 100644 --- a/session/testkit/src/main/scala/app/softnetwork/session/scalatest/SessionTestKit.scala +++ b/session/testkit/src/main/scala/app/softnetwork/session/scalatest/SessionTestKit.scala @@ -48,7 +48,7 @@ trait SessionTestKit[T <: SessionData with SessionDataDecorator[T]] } lines += "***** End Client Headers *****" log.info(lines) - request.withHeaders(request.headers ++ clientHeaders: _*) + request.withHeaders(request.headers ++ clientHeaders) } def createSession( From fc7b42c65d21a2a1f59e9491909881f900780767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Sun, 20 Jul 2025 12:47:37 +0200 Subject: [PATCH 2/3] prepare PR --- .github/workflows/build.yml | 8 +- .github/workflows/release.yml | 10 +- build.sbt | 20 +- .../scalatest/CompletionTestKit.scala | 12 +- .../app/softnetwork/persistence/package.scala | 9 +- .../query/PersonToJsonProcessorStream.scala | 2 +- .../scalatest/PersistenceTestKit.scala | 173 +++++++++--------- project/plugins.sbt | 2 +- .../PersistenceScalatestRouteTest.scala | 70 +++++-- 9 files changed, 178 insertions(+), 128 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f790413..fc88c2a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,12 +38,14 @@ jobs: java-version: '8' distribution: 'temurin' # cache: 'sbt' + - name: Setup sbt launcher + uses: sbt/setup-sbt@v1 - name: Run tests & Coverage Report run: sbt coverage test coverageReport - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: - files: common/target/scala-2.12/coverage-report/cobertura.xml,core/target/scala-2.12/coverage-report/cobertura.xml,teskit/target/scala-2.12/coverage-report/cobertura.xml + files: common/target/scala-2.12/coverage-report/cobertura.xml,common/testkit/target/scala-2.12/coverage-report/cobertura.xml,core/target/scala-2.12/coverage-report/cobertura.xml,core/teskit/target/scala-2.12/coverage-report/cobertura.xml,jdbc/target/scala-2.12/coverage-report/cobertura.xml,jdbc/teskit/target/scala-2.12/coverage-report/cobertura.xml,counter/target/scala-2.12/coverage-report/cobertura.xml,kv/target/scala-2.12/coverage-report/cobertura.xml,kv/target/scala-2.12/coverage-report/cobertura.xml,session/testkit/target/scala-2.12/coverage-report/cobertura.xml flags: unittests fail_ci_if_error: true verbose: true @@ -59,5 +61,7 @@ jobs: java-version: '8' distribution: 'temurin' # cache: 'sbt' + - name: Setup sbt launcher + uses: sbt/setup-sbt@v1 - name: Formatting - run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck \ No newline at end of file + run: sbt scalafmtSbtCheck scalafmtCheck Test/scalafmtCheck \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 10f850ab..9e800481 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,17 +35,19 @@ jobs: java-version: '8' distribution: 'temurin' # cache: 'sbt' + - name: Setup sbt launcher + uses: sbt/setup-sbt@v1 - name: Run tests & Coverage Report run: sbt coverage test coverageReport coverageAggregate - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: - files: common/target/scala-2.12/coverage-report/cobertura.xml,common/testkit/target/scala-2.12/coverage-report/cobertura.xml,core/target/scala-2.12/coverage-report/cobertura.xml,core/teskit/target/scala-2.12/coverage-report/cobertura.xml,jdbc/target/scala-2.12/coverage-report/cobertura.xml,jdbc/teskit/target/scala-2.12/coverage-report/cobertura.xml,elastic/target/scala-2.12/coverage-report/cobertura.xml,elastic/teskit/target/scala-2.12/coverage-report/cobertura.xml,counter/target/scala-2.12/coverage-report/cobertura.xml,server/testkit/target/scala-2.12/coverage-report/cobertura.xml,session/testkit/target/scala-2.12/coverage-report/cobertura.xml + files: common/target/scala-2.12/coverage-report/cobertura.xml,common/testkit/target/scala-2.12/coverage-report/cobertura.xml,core/target/scala-2.12/coverage-report/cobertura.xml,core/teskit/target/scala-2.12/coverage-report/cobertura.xml,jdbc/target/scala-2.12/coverage-report/cobertura.xml,jdbc/teskit/target/scala-2.12/coverage-report/cobertura.xml,counter/target/scala-2.12/coverage-report/cobertura.xml,kv/target/scala-2.12/coverage-report/cobertura.xml,kv/target/scala-2.12/coverage-report/cobertura.xml,session/testkit/target/scala-2.12/coverage-report/cobertura.xml flags: unittests fail_ci_if_error: false verbose: true - name: Publish - run: sbt publish + run: sbt + publish lint: runs-on: ubuntu-latest @@ -58,5 +60,7 @@ jobs: java-version: '8' distribution: 'temurin' # cache: 'sbt' + - name: Setup sbt launcher + uses: sbt/setup-sbt@v1 - name: Formatting - run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck \ No newline at end of file + run: sbt scalafmtSbtCheck scalafmtCheck Test/scalafmtCheck \ No newline at end of file diff --git a/build.sbt b/build.sbt index 34e86a4b..630ff75c 100644 --- a/build.sbt +++ b/build.sbt @@ -1,12 +1,18 @@ import app.softnetwork.* -lazy val scala212 = "2.12.20" -lazy val scala213 = "2.13.16" - ///////////////////////////////// // Defaults ///////////////////////////////// +lazy val scala212 = "2.12.20" +lazy val scala213 = "2.13.16" +lazy val javacCompilerVersion = "1.8" +lazy val scalacCompilerOptions = Seq( + "-deprecation", + "-feature", + s"-target:jvm-$javacCompilerVersion" +) + ThisBuild / organization := "app.softnetwork" name := "generic-persistence-api" @@ -17,19 +23,19 @@ lazy val moduleSettings = Seq( crossScalaVersions := Seq(scala212, scala213), scalacOptions ++= { CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, 12)) => Seq("-deprecation", "-feature", "-target:jvm-1.8", "-Ypartial-unification") - case Some((2, 13)) => Seq("-deprecation", "-feature", "-target:jvm-1.8") + case Some((2, 12)) => scalacCompilerOptions :+ "-Ypartial-unification" + case Some((2, 13)) => scalacCompilerOptions case _ => Seq.empty } } ) +ThisBuild / javacOptions ++= Seq("-source", javacCompilerVersion, "-target", javacCompilerVersion) + ThisBuild / scalaVersion := scala212 //ThisBuild / versionScheme := Some("early-semver") -ThisBuild / javacOptions ++= Seq("-source", "1.8", "-target", "1.8") - ThisBuild / resolvers ++= Seq( "Softnetwork Server" at "https://softnetwork.jfrog.io/artifactory/releases/", "Maven Central Server" at "https://repo1.maven.org/maven2", diff --git a/common/testkit/src/main/scala/app/softnetwork/concurrent/scalatest/CompletionTestKit.scala b/common/testkit/src/main/scala/app/softnetwork/concurrent/scalatest/CompletionTestKit.scala index 3edd7d8a..8ee9dc81 100644 --- a/common/testkit/src/main/scala/app/softnetwork/concurrent/scalatest/CompletionTestKit.scala +++ b/common/testkit/src/main/scala/app/softnetwork/concurrent/scalatest/CompletionTestKit.scala @@ -12,8 +12,9 @@ import scala.util.{Failure, Success, Try} import scala.language.reflectiveCalls /** Created by smanciot on 12/04/2021. - */ -trait CompletionTestKit extends Completion with Assertions { _: { def log: Logger } => + */ +trait CompletionTestKit extends Completion with Assertions { + _: {def log: Logger} => implicit class AwaitAssertion[T](future: Future[T])(implicit atMost: Duration = defaultTimeout) { def assert(fun: T => Assertion): Assertion = @@ -42,13 +43,14 @@ trait CompletionTestKit extends Completion with Assertions { _: { def log: Logge var done = false while (tries <= maxTries && !done) { - if (tries > 0) Thread.sleep(sleep * tries) - tries = tries + 1 try { + tries = tries + 1 + log.info(s"Waiting for $explain, try $tries/$maxTries") + Thread.sleep(sleep * tries) done = predicate() } catch { case e: Throwable => - log.warn(s"problem while testing predicate ${e.getMessage}") + log.warn(s"problem while waiting for $explain: ${e.getMessage}") } } diff --git a/core/src/main/scala/app/softnetwork/persistence/package.scala b/core/src/main/scala/app/softnetwork/persistence/package.scala index 7bd34e04..ecc5db2e 100644 --- a/core/src/main/scala/app/softnetwork/persistence/package.scala +++ b/core/src/main/scala/app/softnetwork/persistence/package.scala @@ -8,18 +8,19 @@ import java.time.Instant import scala.language.implicitConversions /** Created by smanciot on 13/04/2020. - */ + */ package object persistence { trait ManifestWrapper[T] { protected case class ManifestW()(implicit val wrapped: Manifest[T]) + protected val manifestWrapper: ManifestW } def generateUUID(key: Option[String] = None): String = key match { case Some(clearText) => sha256(clearText) - case _ => UUID.randomUUID().toString + case _ => UUID.randomUUID().toString } def now(): Date = Date.from(Instant.now()) @@ -29,8 +30,8 @@ package object persistence { } /** Used for akka and elastic persistence ids, one per targeted environment (development, - * production, ...) - */ + * production, ...) + */ val version: String = sys.env.getOrElse("VERSION", PersistenceCoreBuildInfo.version) val environment: String = sys.env.getOrElse( diff --git a/core/testkit/src/main/scala/app/softnetwork/persistence/person/query/PersonToJsonProcessorStream.scala b/core/testkit/src/main/scala/app/softnetwork/persistence/person/query/PersonToJsonProcessorStream.scala index 5a48d600..42151884 100644 --- a/core/testkit/src/main/scala/app/softnetwork/persistence/person/query/PersonToJsonProcessorStream.scala +++ b/core/testkit/src/main/scala/app/softnetwork/persistence/person/query/PersonToJsonProcessorStream.scala @@ -6,7 +6,7 @@ import app.softnetwork.persistence.query.{InMemoryJournalProvider, InMemoryOffse import java.nio.file.{Files, Paths} trait PersonToJsonProcessorStream - extends PersonToExternalProcessorStream + extends PersonToExternalProcessorStream with InMemoryJournalProvider with InMemoryOffsetProvider with JsonProvider[Person] { diff --git a/core/testkit/src/main/scala/app/softnetwork/persistence/scalatest/PersistenceTestKit.scala b/core/testkit/src/main/scala/app/softnetwork/persistence/scalatest/PersistenceTestKit.scala index b81b05fe..1d0d5f54 100644 --- a/core/testkit/src/main/scala/app/softnetwork/persistence/scalatest/PersistenceTestKit.scala +++ b/core/testkit/src/main/scala/app/softnetwork/persistence/scalatest/PersistenceTestKit.scala @@ -23,9 +23,9 @@ import scala.language.implicitConversions import scala.reflect.ClassTag /** Created by smanciot on 04/01/2020. - */ + */ trait PersistenceTestKit - extends PersistenceGuardian + extends PersistenceGuardian with BeforeAndAfterAll with Eventually with CompletionTestKit @@ -61,91 +61,92 @@ trait PersistenceTestKit } /** @return - * roles associated with this node - */ + * roles associated with this node + */ def roles: Seq[String] = Seq.empty - final lazy val akka: String = s""" - |akka { - | stdout-loglevel = off // defaults to WARNING can be disabled with off. The stdout-loglevel is only in effect during system startup and shutdown - | log-dead-letters-during-shutdown = on - | loglevel = debug - | log-dead-letters = on - | log-config-on-start = off // Log the complete configuration at INFO level when the actor system is started - | loggers = ["akka.event.slf4j.Slf4jLogger"] - | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" - |} - | - |clustering.cluster.name = $systemName - | - |akka.cluster.roles = [${roles.mkString(",")}] - | - |akka.discovery { - | config.services = { - | $systemName = { - | endpoints = [ - | { - | host = "$hostname" - | port = $managementPort - | } - | ] - | } - | } - |} - | - |akka.management { - | http { - | hostname = $hostname - | port = $managementPort - | } - | cluster.bootstrap { - | contact-point-discovery { - | service-name = $systemName - | } - | } - |} - | - |akka.remote.artery.canonical.hostname = $hostname - |akka.remote.artery.canonical.port = 0 - | - |akka.coordinated-shutdown.exit-jvm = off - | - |akka.actor.testkit.typed { - | # Factor by which to scale timeouts during tests, e.g. to account for shared - | # build system load. - | timefactor = 1.0 - | - | # Duration to wait in expectMsg and friends outside of within() block - | # by default. - | # Dilated by the timefactor. - | single-expect-default = 10s - | - | # Duration to wait in expectNoMessage by default. - | # Dilated by the timefactor. - | expect-no-message-default = 1000ms - | - | # The timeout that is used as an implicit Timeout. - | # Dilated by the timefactor. - | default-timeout = 5s - | - | # Default timeout for shutting down the actor system (used when no explicit timeout specified). - | # Dilated by the timefactor. - | system-shutdown-default=60s - | - | # Throw an exception on shutdown if the timeout is hit, if false an error is printed to stdout instead. - | throw-on-shutdown-timeout=false - | - | # Duration to wait for all required logging events in LoggingTestKit.expect. - | # Dilated by the timefactor. - | filter-leeway = 3s - | - |} - | - |""".stripMargin + additionalConfig + final lazy val akka: String = + s""" + |akka { + | stdout-loglevel = off // defaults to WARNING can be disabled with off. The stdout-loglevel is only in effect during system startup and shutdown + | log-dead-letters-during-shutdown = on + | loglevel = debug + | log-dead-letters = on + | log-config-on-start = off // Log the complete configuration at INFO level when the actor system is started + | loggers = ["akka.event.slf4j.Slf4jLogger"] + | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" + |} + | + |clustering.cluster.name = $systemName + | + |akka.cluster.roles = [${roles.mkString(",")}] + | + |akka.discovery { + | config.services = { + | $systemName = { + | endpoints = [ + | { + | host = "$hostname" + | port = $managementPort + | } + | ] + | } + | } + |} + | + |akka.management { + | http { + | hostname = $hostname + | port = $managementPort + | } + | cluster.bootstrap { + | contact-point-discovery { + | service-name = $systemName + | } + | } + |} + | + |akka.remote.artery.canonical.hostname = $hostname + |akka.remote.artery.canonical.port = 0 + | + |akka.coordinated-shutdown.exit-jvm = off + | + |akka.actor.testkit.typed { + | # Factor by which to scale timeouts during tests, e.g. to account for shared + | # build system load. + | timefactor = 1.0 + | + | # Duration to wait in expectMsg and friends outside of within() block + | # by default. + | # Dilated by the timefactor. + | single-expect-default = 10s + | + | # Duration to wait in expectNoMessage by default. + | # Dilated by the timefactor. + | expect-no-message-default = 1000ms + | + | # The timeout that is used as an implicit Timeout. + | # Dilated by the timefactor. + | default-timeout = 5s + | + | # Default timeout for shutting down the actor system (used when no explicit timeout specified). + | # Dilated by the timefactor. + | system-shutdown-default=60s + | + | # Throw an exception on shutdown if the timeout is hit, if false an error is printed to stdout instead. + | throw-on-shutdown-timeout=false + | + | # Duration to wait for all required logging events in LoggingTestKit.expect. + | # Dilated by the timefactor. + | filter-leeway = 3s + | + |} + | + |""".stripMargin + additionalConfig /** @return - * additional configuration - */ + * additional configuration + */ def additionalConfig: String = "" lazy val akkaConfig: Config = ConfigFactory.parseString(akka) @@ -159,7 +160,7 @@ trait PersistenceTestKit def typedSystem(): ActorSystem[Nothing] = system /** `PatienceConfig` from [[_root_.akka.actor.testkit.typed.TestKitSettings#DefaultTimeout]] - */ + */ implicit val patience: PatienceConfig = PatienceConfig(Settings.DefaultTimeout, Span(100, org.scalatest.time.Millis)) @@ -174,11 +175,11 @@ trait PersistenceTestKit } /** init and join cluster - */ + */ final def initAndJoinCluster(): Unit = { testKit.spawn(setup(), "guardian") // let the nodes join and become Up - blockUntil("let the nodes join and become Up", 30, 2000)(() => + blockUntil("the nodes join and become Up", 30, 2000)(() => Cluster(system).selfMember.status == MemberStatus.Up ) } diff --git a/project/plugins.sbt b/project/plugins.sbt index e69cd1d1..3a932787 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -18,4 +18,4 @@ addDependencyTreePlugin //addSbtPlugin("com.typesafe.sbt" % "sbt-multi-jvm" % "0.4.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.8") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0") diff --git a/server/testkit/src/main/scala/akka/http/scaladsl/testkit/PersistenceScalatestRouteTest.scala b/server/testkit/src/main/scala/akka/http/scaladsl/testkit/PersistenceScalatestRouteTest.scala index 016ed3c6..d1247b0a 100644 --- a/server/testkit/src/main/scala/akka/http/scaladsl/testkit/PersistenceScalatestRouteTest.scala +++ b/server/testkit/src/main/scala/akka/http/scaladsl/testkit/PersistenceScalatestRouteTest.scala @@ -19,15 +19,16 @@ import org.scalatest.Suite import scala.concurrent.ExecutionContextExecutor /** Created by smanciot on 24/04/2020. - */ + */ trait PersistenceScalatestRouteTest - extends ApiServer + extends ApiServer with ServerTestKit with PersistenceTestKit with PersistenceRouteTest with TestFrameworkInterface with ScalatestUtils - with Json4sSupport { this: Suite with ApiRoutes with Schema => + with Json4sSupport { + this: Suite with ApiRoutes with Schema => override protected def createActorSystem(): ActorSystem = { typedSystem() @@ -73,7 +74,7 @@ trait PersistenceScalatestRouteTest @deprecated("this method has been replaced by findHeader and will be removed", since = "0.3.1.1") def findCookie(name: String): HttpHeader => Option[HttpCookiePair] = { case Cookie(cookies) => cookies.find(_.name == name) - case _ => None + case _ => None } def extractHeaders(headers: Seq[HttpHeader]): Seq[HttpHeader] = { @@ -107,15 +108,15 @@ trait PersistenceScalatestRouteTest } def headerValue(name: String): HttpHeader => Option[String] = { - case Cookie(cookies) => cookies.find(_.name == name).map(_.value) + case Cookie(cookies) => cookies.find(_.name == name).map(_.value) case r: RawHeader if r.name == name => Some(r.value) - case _ => None + case _ => None } def findHeader(name: String): HttpHeader => Option[HttpHeader] = { case c: Cookie if c.cookies.exists(_.name == name) => Some(c) - case other if other.name() == name => Some(other) - case _ => None + case other if other.name() == name => Some(other) + case _ => None } def existHeader(name: String): HttpHeader => Boolean = header => @@ -123,7 +124,7 @@ trait PersistenceScalatestRouteTest } trait InMemoryPersistenceScalatestRouteTest - extends PersistenceScalatestRouteTest + extends PersistenceScalatestRouteTest with InMemoryPersistenceTestKit { _: Suite with ApiRoutes => } @@ -132,7 +133,7 @@ import akka.http.scaladsl.Http import akka.http.scaladsl.client.RequestBuilding import akka.http.scaladsl.model.HttpEntity.ChunkStreamPart import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.headers.{ Host, Upgrade, `Sec-WebSocket-Protocol` } +import akka.http.scaladsl.model.headers.{Host, Upgrade, `Sec-WebSocket-Protocol`} import akka.http.scaladsl.server._ import akka.http.scaladsl.settings.ParserSettings import akka.http.scaladsl.settings.RoutingSettings @@ -142,11 +143,11 @@ import akka.http.scaladsl.util.FastFuture._ import akka.stream.scaladsl.Source import akka.testkit.TestKit import akka.util.ConstantFun -import com.typesafe.config.{ Config, ConfigFactory } +import com.typesafe.config.{Config, ConfigFactory} import scala.collection.immutable import scala.concurrent.duration._ -import scala.concurrent.{ Await, ExecutionContext, Future } +import scala.concurrent.{Await, ExecutionContext, Future} import scala.reflect.ClassTag import scala.util.DynamicVariable @@ -164,11 +165,13 @@ trait PersistenceRouteTest extends RequestBuilding with WSTestRequestBuilding wi .filter(_ != '$') def testConfigSource: String = "" + def testConfig: Config = { val source = testConfigSource val config = if (source.isEmpty) ConfigFactory.empty() else ConfigFactory.parseString(source) config.withFallback(ConfigFactory.load()) } + implicit lazy val system: ActorSystem = createActorSystem() implicit lazy val executor: ExecutionContextExecutor = system.dispatcher implicit lazy val materializer: Materializer = SystemMaterializer(system).materializer @@ -176,6 +179,7 @@ trait PersistenceRouteTest extends RequestBuilding with WSTestRequestBuilding wi def cleanUp(): Unit = TestKit.shutdownActorSystem(system) private val dynRR = new DynamicVariable[RouteTestResult](null) + private def result = if (dynRR.value ne null) dynRR.value else sys.error("This value is only available inside of a `check` construct!") @@ -185,38 +189,57 @@ trait PersistenceRouteTest extends RequestBuilding with WSTestRequestBuilding wi private def responseSafe = if (dynRR.value ne null) dynRR.value.response else "" def handled: Boolean = result.handled + def response: HttpResponse = result.response + def responseEntity: HttpEntity = result.entity + private def rawResponse: HttpResponse = result.rawResponse + def chunks: immutable.Seq[HttpEntity.ChunkStreamPart] = result.chunks + def chunksStream: Source[ChunkStreamPart, Any] = result.chunksStream - def entityAs[T: FromEntityUnmarshaller: ClassTag](implicit timeout: Duration = 1.second): T = { + + def entityAs[T: FromEntityUnmarshaller : ClassTag](implicit timeout: Duration = 1.second): T = { def msg(e: Throwable) = s"Could not unmarshal entity to type '${implicitly[ClassTag[T]]}' for `entityAs` assertion: $e\n\nResponse was: $responseSafe" + Await.result(Unmarshal(responseEntity).to[T].fast.recover[T] { case error => failTest(msg(error)) }, timeout) } - def responseAs[T: FromResponseUnmarshaller: ClassTag](implicit timeout: Duration = 1.second): T = { + + def responseAs[T: FromResponseUnmarshaller : ClassTag](implicit timeout: Duration = 1.second): T = { def msg(e: Throwable) = s"Could not unmarshal response to type '${implicitly[ClassTag[T]]}' for `responseAs` assertion: $e\n\nResponse was: $responseSafe" + Await.result(Unmarshal(response).to[T].fast.recover[T] { case error => failTest(msg(error)) }, timeout) } + def contentType: ContentType = rawResponse.entity.contentType + def mediaType: MediaType = contentType.mediaType + def charsetOption: Option[HttpCharset] = contentType.charsetOption + def charset: HttpCharset = charsetOption getOrElse sys.error("Binary entity does not have charset") + def headers: immutable.Seq[HttpHeader] = rawResponse.headers - def header[T >: Null <: HttpHeader: ClassTag]: Option[T] = rawResponse.header[T](implicitly[ClassTag[T]]) + + def header[T >: Null <: HttpHeader : ClassTag]: Option[T] = rawResponse.header[T](implicitly[ClassTag[T]]) + def header(name: String): Option[HttpHeader] = rawResponse.headers.find(_.is(name.toLowerCase)) + def status: StatusCode = rawResponse.status def closingExtension: String = chunks.lastOption match { case Some(HttpEntity.LastChunk(extension, _)) => extension - case _ => "" + case _ => "" } + def trailer: immutable.Seq[HttpHeader] = chunks.lastOption match { case Some(HttpEntity.LastChunk(_, trailer)) => trailer - case _ => Nil + case _ => Nil } def rejections: immutable.Seq[Rejection] = result.rejections + def rejection: Rejection = { val r = rejections if (r.size == 1) r.head else failTest("Expected a single rejection but got %s (%s)".format(r.size, r)) @@ -265,21 +288,27 @@ trait PersistenceRouteTest extends RequestBuilding with WSTestRequestBuilding wi abstract class TildeArrow[A, B] { type Out + def apply(request: HttpRequest, f: A => B): Out } case class DefaultHostInfo(host: Host, securedConnection: Boolean) + object DefaultHostInfo { implicit def defaultHost: DefaultHostInfo = DefaultHostInfo(Host("example.com"), securedConnection = false) } + object TildeArrow { implicit object InjectIntoRequestTransformer extends TildeArrow[HttpRequest, HttpRequest] { type Out = HttpRequest + def apply(request: HttpRequest, f: HttpRequest => HttpRequest) = f(request) } - implicit def injectIntoRoute(implicit timeout: RouteTestTimeout, defaultHostInfo: DefaultHostInfo): TildeArrow[RequestContext, Future[RouteResult]] { type Out = RouteTestResult } = + + implicit def injectIntoRoute(implicit timeout: RouteTestTimeout, defaultHostInfo: DefaultHostInfo): TildeArrow[RequestContext, Future[RouteResult]] {type Out = RouteTestResult} = new TildeArrow[RequestContext, Future[RouteResult]] { type Out = RouteTestResult + def apply(request: HttpRequest, route: Route): Out = { if (request.method == HttpMethods.HEAD && ServerSettings(system).transparentHeadRequests) failTest("`akka.http.server.transparent-head-requests = on` not supported in PersistenceRouteTest using `~>`. Use `~!>` instead " + @@ -310,13 +339,15 @@ trait PersistenceRouteTest extends RequestBuilding with WSTestRequestBuilding wi abstract class TildeBangArrow[A, B] { type Out + def apply(request: HttpRequest, f: A => B): Out } object TildeBangArrow { - implicit def injectIntoRoute(implicit timeout: RouteTestTimeout, serverSettings: ServerSettings): TildeBangArrow[RequestContext, Future[RouteResult]] { type Out = RouteTestResult } = + implicit def injectIntoRoute(implicit timeout: RouteTestTimeout, serverSettings: ServerSettings): TildeBangArrow[RequestContext, Future[RouteResult]] {type Out = RouteTestResult} = new TildeBangArrow[RequestContext, Future[RouteResult]] { type Out = RouteTestResult + def apply(request: HttpRequest, route: Route): Out = { val routeTestResult = new RouteTestResult(timeout.duration) val responseF = PersistenceRouteTest.runRouteClientServer(request, route, serverSettings) @@ -327,6 +358,7 @@ trait PersistenceRouteTest extends RequestBuilding with WSTestRequestBuilding wi } } } + private[http] object PersistenceRouteTest { def runRouteClientServer(request: HttpRequest, route: Route, serverSettings: ServerSettings)(implicit system: ActorSystem): Future[HttpResponse] = { import system.dispatcher From 8522cc0269a4cb4b98d53b8505536372b971f557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Sun, 20 Jul 2025 12:55:01 +0200 Subject: [PATCH 3/3] update github workflows --- .github/workflows/build.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc88c2a3..7dd99109 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,8 +23,8 @@ permissions: jobs: test: - runs-on: self-hosted -# runs-on: ubuntu-latest +# runs-on: self-hosted + runs-on: ubuntu-latest env: # define Java options for both official sbt and sbt-extras JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e800481..2903fd6a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,8 +20,8 @@ permissions: jobs: release: - runs-on: self-hosted -# runs-on: ubuntu-latest +# runs-on: self-hosted + runs-on: ubuntu-latest env: # define Java options for both official sbt and sbt-extras JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8