From 79b94f99b649c3ff168c01da8c9a51575aec8245 Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Thu, 18 Sep 2014 12:13:02 +0200 Subject: [PATCH 01/14] sbt template using dynamically generated langs.properties Signed-off-by: Joern Bernhardt --- build.gradle | 119 ------------ gradle.properties | 38 ---- gradle/maven.gradle | 68 ------- gradle/setup.gradle | 5 - gradle/vertx.gradle | 225 ----------------------- gradle/wrapper/gradle-wrapper.jar | Bin 46735 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 6 - gradlew | 166 ----------------- gradlew.bat | 90 --------- project/VertxScalaBuild.scala | 178 ++++++++++++++++++ src/main/resources/langs.properties | 2 - 11 files changed, 178 insertions(+), 719 deletions(-) delete mode 100644 build.gradle delete mode 100644 gradle.properties delete mode 100644 gradle/maven.gradle delete mode 100644 gradle/setup.gradle delete mode 100644 gradle/vertx.gradle delete mode 100644 gradle/wrapper/gradle-wrapper.jar delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100755 gradlew delete mode 100755 gradlew.bat create mode 100644 project/VertxScalaBuild.scala delete mode 100644 src/main/resources/langs.properties diff --git a/build.gradle b/build.gradle deleted file mode 100644 index e075e09..0000000 --- a/build.gradle +++ /dev/null @@ -1,119 +0,0 @@ -apply from: "gradle/vertx.gradle" - -/* -Usage: - -./gradlew task_name - -(or gradlew.bat task_name if you have the misfortune to have to use Windows) - -If no task name is specified then the default task 'assemble' is run - -Task names are: - -idea - generate a skeleton IntelliJ IDEA project - -eclipse - generate a skeleton Eclipse IDE project - -assemble - builds the outputs, by default this is the module zip file. It can also include a jar file if produceJar - in gradle.properties is set to true. Outputs are created in build/libs. - if pullInDeps in gradle.properties is set to 'true' then the modules dependencies will be - automatically pulled into a nested mods directory inside the module during the build - -copyMod - builds and copies the module to the local 'mods' directory so you can execute vertx runmod (etc) - directly from the command line - -modZip - creates the module zip into build/libs - -clean - cleans everything up - -test - runs the tests. An nice html test report is created in build/reports/tests (index.html) - -runMod - runs the module. This is similar to executing vertx runmod from the command line except that it does - not use the version of Vert.x installed and on the PATH to run it. Instead it uses the version of Vert.x - that the module was compiled and tested against. - -runModIDEA - run the module from the project resources in IDEA. This allows you to run the module without building it -first! - -runModEclipse - run the module from the project resources in Eclipse. This allows you to run the module without -building it first! - -pullInDeps - pulls in all dependencies of the module into a nested module directory - -uploadArchives - upload the module zip file (and jar if one has been created) to Nexus. You will need to - configure sonatypeUsername and sonatypePassword in ~/.gradle/gradle.properties. - -install - install any jars produced to the local Maven repository (.m2) - - */ - -dependencies { - /* - Add your module jar dependencies here - E.g. - compile "com.foo:foo-lib:1.0.1" - for compile time deps - this will end up in your module too! - testCompile "com.foo:foo-lib:1.0.1" - for test time deps - provided "com.foo:foo-lib:1.0.1" - if you DON'T want it to be packaged in the module zip - */ - - provided "org.scala-lang:scala-library:$scalaVersion" - provided "org.scala-lang:scala-compiler:$scalaVersion" - provided "io.vertx:lang-scala:$scalaLangModVersion" - - compile("com.github.mauricio:postgresql-async_2.10:$asyncDriverVersion") { - exclude group: 'org.scala-lang' - exclude group: 'io.netty' - } - compile("com.github.mauricio:mysql-async_2.10:$asyncDriverVersion") { - exclude group: 'org.scala-lang' - exclude group: 'io.netty' - } -} - -test { - /* Configure which tests are included - include 'org/foo/**' - exclude 'org/boo/**' - */ - -} - -/* -If you're uploading stuff to Maven, Gradle needs to generate a POM. -Please edit the details below. - */ -def configurePom(def pom) { - pom.project { - name rootProject.name - description 'Using MySQL/PostgreSQL async driver as a module for Vert.x' - inceptionYear '2013' - packaging produceJar == 'false' ? 'pom' : 'jar' - - url 'https://github.com/vert-x/mod-mysql-postgresql' - - developers { - developer { - id 'Narigo' - name 'Joern Bernhardt' - email 'jb@campudus.com' - } - } - - scm { - url 'https://github.com/vert-x/mod-mysql-postgresql' - } - - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' - } - } - - properties { - setProperty('project.build.sourceEncoding', 'UTF8') - } - } -} diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 04f3b94..0000000 --- a/gradle.properties +++ /dev/null @@ -1,38 +0,0 @@ -# E.g. your domain name -modowner=io.vertx - -# Your module name -modname=mod-mysql-postgresql - -# Your module version -version=0.3.0-SNAPSHOT - -# The version of mauricios async driver -asyncDriverVersion=0.2.12 - -# The test timeout in seconds -testtimeout=5 - -# Set to true if you want module dependencies to be pulled in and nested inside the module itself -pullInDeps=true - -# Set to true if you want the build to output a jar as well as a module zip file -produceJar=false - -# The version of the Scala module -scalaLangModVersion=1.0.0 - -# The version of Scala to use -scalaVersion=2.10.4 - -# Gradle version -gradleVersion=1.11 - -# The version of Vert.x -vertxVersion=2.1RC3 - -# The version of Vert.x test tools -toolsVersion=2.0.2-final - -# The version of JUnit -junitVersion=4.10 diff --git a/gradle/maven.gradle b/gradle/maven.gradle deleted file mode 100644 index 0d131ba..0000000 --- a/gradle/maven.gradle +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2012 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -apply plugin: 'maven' -apply plugin: 'signing' - -if (!hasProperty('sonatypeUsername')) { - ext.sonatypeUsername = '' -} -if (!hasProperty('sonatypePassword')) { - ext.sonatypePassword = '' -} - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// maven task configuration - -ext.isReleaseVersion = !version.endsWith("SNAPSHOT") - -signing { - required { isReleaseVersion && gradle.taskGraph.hasTask("uploadArchives") } - sign configurations.archives -} - -uploadArchives { - group 'build' - description = "Does a maven deploy of archives artifacts" - - repositories { - mavenDeployer { - // setUniqueVersion(false) - - configuration = configurations.archives - - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: sonatypeUsername, password: sonatypePassword) - } - - snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { - authentication(userName: sonatypeUsername, password: sonatypePassword) - } - - if (isReleaseVersion) { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - } - - configurePom(pom) - } - } -} - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// configuration methods - - - diff --git a/gradle/setup.gradle b/gradle/setup.gradle deleted file mode 100644 index da1fcd7..0000000 --- a/gradle/setup.gradle +++ /dev/null @@ -1,5 +0,0 @@ - -task wrapper(type: Wrapper, description: "Create a Gradle self-download wrapper") { - group = 'Project Setup' - gradleVersion = rootProject.gradleVersion -} \ No newline at end of file diff --git a/gradle/vertx.gradle b/gradle/vertx.gradle deleted file mode 100644 index 55b006d..0000000 --- a/gradle/vertx.gradle +++ /dev/null @@ -1,225 +0,0 @@ -import org.vertx.java.platform.impl.cli.Starter - -/* - * Copyright 2012 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -apply plugin: 'java' -apply plugin: 'scala' -apply plugin: 'idea' -apply plugin: 'eclipse' - -def cpSeparator = System.getProperty("path.separator") - -// We have to explicitly load props from the user home dir - on CI we set -// GRADLE_USER_HOME to a different dir to avoid problems with concurrent builds corrupting -// a shared Maven local and using Gradle wrapper concurrently -loadProperties("${System.getProperty('user.home')}/.gradle/gradle.properties") - -apply from: "gradle/maven.gradle" - -group = modowner -archivesBaseName = modname - -defaultTasks = ['assemble'] - -sourceCompatibility = '1.7' -targetCompatibility = '1.7' - -project.ext.moduleName = "$modowner~$modname~$version" - -configurations { - provided - testCompile.extendsFrom provided -} - -repositories { - if (System.getenv("VERTX_DISABLE_MAVENLOCAL") == null) { - // We don't want to use mavenLocal when running on CI - mavenLocal is only useful in Gradle for - // publishing artifacts locally for development purposes - maven local is also not threadsafe when there - // are concurrent builds - mavenLocal() - } - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } - mavenCentral() -} - -dependencies { - provided "io.vertx:vertx-core:$vertxVersion" - provided "io.vertx:vertx-platform:$vertxVersion" - testCompile "junit:junit:$junitVersion" - testCompile "io.vertx:testtools:$toolsVersion" -} - -// This sets up the classpath for the script itself -buildscript { - - repositories { - if (System.getenv("VERTX_DISABLE_MAVENLOCAL") == null) { - // We don't want to use mavenLocal when running on CI - mavenLocal is only useful in Gradle for - // publishing artifacts locally for development purposes - maven local is also not threadsafe when there - // are concurrent builds - mavenLocal() - } - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } - mavenCentral() - } - - dependencies { - classpath "io.vertx:vertx-core:$vertxVersion" - classpath "io.vertx:vertx-platform:$vertxVersion" - classpath "io.vertx:vertx-hazelcast:$vertxVersion" - classpath files(['src/main/resources']) - } -} - -sourceSets { - main { - compileClasspath = compileClasspath + configurations.provided - } -} - -task copyMod( type:Copy, dependsOn: 'classes', description: 'Assemble the module into the local mods directory' ) { - into "build/mods/$moduleName" - from compileJava - from compileScala - from 'src/main/resources' - into( 'lib' ) { - from configurations.compile - } -} - -task modZip( type: Zip, dependsOn: 'pullInDeps', description: 'Package the module .zip file') { - group = 'vert.x' - classifier = "mod" - description = "Assembles a vert.x module" - destinationDir = project.file('build/libs') - archiveName = "${modname}-${version}" + ".zip" - from copyMod -} - -task sourceJar(type: Jar) { - description = 'Builds a source jar artifact suitable for maven deployment.' - classifier = 'sources' - from sourceSets.main.java -} - -task javadocJar(type: Jar) { - description = 'Builds a javadoc jar artifact suitable for maven deployment.' - classifier = 'javadoc' - from javadoc.destinationDir -} -javadocJar.dependsOn javadoc - -build.dependsOn sourceJar, javadocJar - -artifacts { - archives sourceJar, javadocJar, modZip -} - - -test { - dependsOn copyMod - - // Make sure tests are always run! - outputs.upToDateWhen { false } - - // Show output - testLogging.showStandardStreams = true - - testLogging { exceptionFormat "full" } - - systemProperty 'vertx.mods', "build/mods" -} - -task init(description: 'Create module link and CP file') << { - setSysProps() - doInit() -} - -task runMod(description: 'Run the module') << { - setSysProps() - System.setProperty("vertx.langs.scala", "io.vertx~lang-scala~${scalaLangModVersion}:org.vertx.scala.platform.impl.ScalaVerticleFactory") - // We also init here - this means for single module builds the user doesn't have to explicitly init - - // they can just do runMod - doInit() - args = ['runmod', moduleName] - def args2 = runModArgs.split("\\s+") - args.addAll(args2) - Starter.main(args as String[]) -} - -def doInit() { - File cpFile = new File("vertx_classpath.txt") - if (!cpFile.exists()) { - cpFile.createNewFile(); - String defaultCp = - "src/main/resources\r\n" + - "bin\r\n" + - "out/production/${project.name}\r\n" + - "out/test/${project.name}"; - cpFile << defaultCp; - } - def args = ['create-module-link', moduleName] - Starter.main(args as String[]) -} - -task pullInDeps(dependsOn: copyMod, description: 'Pull in all the module dependencies for the module into the nested mods directory') << { - if (pullInDeps == 'true') { - setSysProps() - def args = ['pulldeps', moduleName] - Starter.main(args as String[]) - } -} - -task fatJar(dependsOn: modZip, description: 'Creates a fat executable jar which contains everything needed to run the module') << { - if (createFatJar == 'true') { - setSysProps() - def args = ['fatjar', moduleName, '-d', 'build/libs'] - Starter.main(args as String[]) - } -} - -def setSysProps() { - System.setProperty("vertx.clusterManagerFactory", "org.vertx.java.spi.cluster.impl.hazelcast.HazelcastClusterManagerFactory") - String modsDir = System.getenv("VERTX_MODS") - if (modsDir == null) { - modsDir = "build/mods"; - } - System.setProperty("vertx.mods", modsDir) -} - -def loadProperties(String sourceFileName) { - def config = new Properties() - def propFile = new File(sourceFileName) - if (propFile.canRead()) { - config.load(new FileInputStream(propFile)) - for (Map.Entry property in config) { - project.ext[property.key] = property.value; - } - } -} - -// Map the 'provided' dependency configuration to the appropriate IDEA visibility scopes. -plugins.withType(IdeaPlugin) { - idea { - module { - scopes.PROVIDED.plus += configurations.provided - scopes.COMPILE.minus += configurations.provided - scopes.TEST.minus += configurations.provided - scopes.RUNTIME.minus += configurations.provided - } - } -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 42d9b0e9c5872910311a1d035995ab8ec466e7ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46735 zcmagF1C%G-vM*R&wr$(CZQK5r?W(RW+qP|V*|u%lt}eX3bMCzRzB6ZLa;=@2D_88b zk%3=C>`;^e0fhzvf`kN0C*u_c`g;NY*X{2G^|#51sS43a$%`|904e^1@P>(hmhUft z>feU?e-g?G$xDfgsi@M+i9g6qPRPp8(a*uj&{0oM&NM1BF0$+%-A~euN=?a4(MZw$ zfIbf~O*t&mrfS6?D>*DO9wi2_?H%nO0skMvrTyEyK>rSB?_}|hDg8SQ%zx8ZI2oDR znEii}qWqK8-O0$o!OZFZ(Zw>r)V%O7>C)du@}Iki+PmA?*c+LWGSQpZ7&$xpM#(|< zGa?4>Sh8u;xG@C4tc2wB5jYUh^9tFB*g#21Rdi*-AnfK3qB>si9`oT(`qaK0KoN@c z_hK3g`~2oeo$xIuGiqND?klq9{`pwezyPNf#GP9BnlRPNcHG+l$n$Gh=R8MB) z=g-P0AYms)@%Cu+Z5ahg9_*EVO22khW_!p7fjAc|L_VKVf}mMqSYdHYaDve20XSDW zJl}uY>o_w{N+bI4VhkOWVm zrZjs`45Hg*h7B;)+3v!N+^1uBSfvtOqat7;H`kG2rkv{&>c7Nb3wQ6qEjtT9Ij5YaLN0GT-Tso4-6)3G85e$o7K8&jc8;MukUR=X}zac3J z@dn*5K-6M26k9@2MS%Z@^RSb(oW zp&dvFT`7NjK@mn1%}|dBtb#e|7YQ2=9tje4m1}F2^q7=rKNbEm<~@rx?((B+ZI-ia z)trI3&`-y~$Le_!?SgEX6n#|m-wKF-WaVMCv=avmK`;Q#;!t&t!8X8Lha-p7Vr(hj zA+J0e$Y*s2Ur8Oj zZ}?L4*_^^;?+PrFqU>_%k60dP9+UlCdDiQa|1ve6|RH&`-Tj zy&xg|3TVjK6d)7N$WJt;lPK4WFoF%nS#>OwgBnSQG`EbElG4>J5cnp%eVOXZUV?<0 zKD4GTMEW%|lZrzqlOU~l)YMMo#yI3r77 zj`UO_`W+?!Ni{K%+S)|>v#~B&R%3fZiK;*mCYfe%q{}%CHF}I};+a3%pTlGW#78hb zLEa@?-!Hd>3_B)WFrU|a{rsLi(Z9Y<9t?n%=VXb4Lmhdg_nFueJpyu((|+NPq{MAF zCI!)s)RP>l)bpZD)M)y}?0LeXg*54N&Hl8-Z?16l{qv^O);<$g-nnn@Q9n@aR)5A- zvg9|)*lep)GeT#d>+S_UV1ubyfu~Bt)`c2m)#*IE(@s}8q2Om>7z(@atHLx_3okP_ zVW#{{dHEp6VjX=g&?;pQO($BfA_@Dqz2i!ps_S)^X;>Fa$1kNz;H^QD1?Dcfkcl?l zKGEM-Dh)HLvJ+*``UE)B3?Ho~VKD0yosBbiDp?|CgWiC4*tdwQrbye+T(_wG^nnh& z0V;gZ1nt|*wQDY2LrCR?rQ!)Cvv%ioHr_SlF+Aw9Ge6lL2^Nr@UnRC4#qs#XPM&Qt z#5Rjm**H%~OXkI@LLUCy-52_k5Q#G-vPxz%r-D@B|rd zGh9q=vP@ZEOQ6f3sUc=QV{yLgvogu|b3!7uD-+R$@fLUkIU&?mOp9!tf+7RFHH@^C zChrW|18RH8&|TVcPM+!;G}f&lu%CqQu_B#v8Uw`4*bT;7$P*ZvhOKV`JIHEnr;b;z z$&UNcWf}H*GahmXS?y+;_L%BwB1%)NzWEysS22C%Da%JO*03?-KM0J2zsQvzH4=M) z=STg`WlCipwR$&aJp;NI<$wNUOLFlvP%iXo!yLDvO!eUs;j;8-@)Ij%nP*AB363=k zo-9^q{eX&h7KKR<|Ke949h`}$G)*{3dwj|0$$j4~)ysFqV$wcABnm?{GA(~~)pk~O ziLP21s{jkWW$Pvya{#F%5{)ma6NC9l{5(Q<7F4T?1sww)V6S{m=&^7E?}>p9^#n|H zW^J!HS&_RZy^Cg!$TP>G#90d(K2GRKCMg7moGfIgGP#Z!_l9_g-mT^@+mkA|oJ`n4 z)dlNxfxEyw3O=-n1467HN2xpL4jmT+doKvp5ObqO2!(aXG-MO=qwQG0HH1exP6|s@ zBVbc4PrLt4Y%&catj82DHoBCkC5x*swRDq2<1cfA+)146OJ$ zmB5=oyNP%Iktpx!fU?ymoO;~!p1H_*;5o@z>-m0rU;qleYYcIVD)SH#!4qfA8ZL|A zV0$Hdhyq75xo4zzN1-NHlP&j<86b}WbyTlmlA4xs(hrO|Bc!+Vz+sucG$z^ZD;C!s z?nvmQVBldWzOiOxq><7czztH(u@?oFLMw@&fd&RCF>4QmJ|Dnafc6o2&Qh#1n`{~s zrRSr`avrvc$SPstu`4Qp8%a7LUN|A2stUMf+K>`Oj$ukgjt3hVH4Q=ur!&)w&vCNB z*Htl9z(F69SM7Taa-b=OehwL_!CZ+B14xKZCWX17#&E63iVa7@UImn(IpY}>p#_b* zNL0&C(}k6lFxDNrzMUT48ta z)zJ!*0c)3l)@76J!x5U1V+|Ztt5?gGKo^v;GSHkVA3G}j8L!1F1Fy_rkqm`nVll$9 zo6e8jE5&$7_qAf;IT;lDQM09BS*{PLFcETl#`6V=$gXulAn{Al1LF3}>h>Dv!cjBJr;uVwzhl~VTWtdBoaL=o9ZV7?Q6>uaS zN{xDvxJ5LiH0Mb|Ocx@Hy~fUB+?c0)_a@f&{Gz?V!RD;G{Q_}#oWdsP)VmRo zRSx@c?ms9o*$22b@UhEP{mat$j#5xGd*odqIAn689|&IxpDPcPKc;bdpKL_IMy%5W z5=AZ#YVq!C;UlsXiXc299MoFhc{K7ruEP-$z!*2rdU|)78^8sye%-N^u{`2tonDU>{`y{Le)P-T4DyLhnPaa9_ce#h zEbD3m$l&Xo8SCK7@l~#Vl>tUTSQT7O>K}--Q6K*ZcZaSP@6yYU>4nk$R2a>bu*RQx zf)M`2>$WrWOR+a~`_ekJUYR(XPBXH3DHyJr-u-oBvc>h!2S?NS}N*5~GVP z$mCRlgbX~Ta2dM!T16&+8{y4pQ5J!(lP+SbDcZpuqB`n`QJ~8X>6GLVIj@%A>)Zq? zfm6g_1d>N+#IduVEo|qMW1KknmImA*V5n{Krdp^|`e!W~jOMIYc1NOqd}u4r(N)Oz zzri3Hd0QxK2p}LwcpxD1|Ex0=ja)2+oSn^VjsLf%OjdvM#?e6IGm*hI0P|D-g(UGaw+pOrEgPzA!hx3-R9!w{!DpKkJo0Xcufl~f~r*BdY1Au&t z5`b;;u%wu}%5Es+ZnM8^d2hF!Y^DGFKH260n%*@)jwxt`u&AdN8gLC4pK(yxFQFAa zF$<)yCeBGF%pav8kFBD#xyFMcw!6KTvs)Ikk>m_hka>j_;E6mbc&!SXk@CRLjok-> zV%R6k>6|RoA@1(|#2~{Rq1p728cY@QA&aP$J{?*qc;&|V8JKC`Fr(r5sExX_|Fxmy zLlJQ!{fgf`!)YgR7f9(h%3~kdO0wGzW=GE9DJ5vL-|i$Lm5SPxmO}>Nb=T?NWfEey7GcLgNhX0- zXYXZxes{U5YrE22P>w2n-dUV+Af8Ug%aIY^U6!osgybo^!1gD=e_3=Vz<)MPiLmh# zC8I{3`^ao5OC?2ydc@)uCZhaq(*Sm@5HK^CR7@~0v)3QEU?QSe zo?6zlNzks#C=-C`0=tkI{^4)_bsF)T+>rY2`qJ0m8QfIgWpcjNxEeYxY`|wPi^G$V zP;vLG%WSD3sUTx6qQVS@^B7C(Ji^54S)6<4HSKa>0*4)@>FB%+=vRzqS)a9=ubAEg zRfMJ;4Z%GoUOGo1AoMmB=%|Sn6`p@;&9*4C~(ivC?nlCNBr3E*Z3$}KiUHo z59wjzoVYtE*?B#?N7@4l54~Y#^;30@LQK~tWg#}Rk0e{akX#fJ09McL8lxZ8fd=n8 zn?A@_bnywgQtGD)cyHtT2zL z56}h`3u0xA2eoJ@+1p?!YGUrCNWcWNN#h^X;mMqS872xbguah&Rsc2+SE^+UaeS!| zUjy3tU8D8D4Jq_rG^g*~@c6Sv!A8)|5WCwKE%1s>+1@tDU&p6;Z8d*uKTBq@!zK84 z)x*SrW#uW<6h3;cM9BMq#s)Mx`*_RjUQvk9|@1CyxD6MT^g$MIRLNL#;NRK`$DjUbiII zsN?t@n=y^ZhIUz7;7i$Q>unfnYZF?{b6#s7VY9bu+`zJlRbF7y=$1tK8x>nV8!( zx_+;*9z*}T#2iBeq|tq@Z7r$2F{#0szM`n5 zBjmg_QqVynsy3L`B+W^w`S#Cbtas@1-E^PkiXLwq#e6%(9|&sB-@ylwME&>q)caR= z(DF9NEwde%JW^cTt~*aYzH1E2Ag@G-1M0wj1?G75rMbGfI(lN!D zhQl{vN51RfSZ{A@ByNYHXu^acHJ|xp4&RX1df5hZ0lh&9gYMj9=l6al=+&OJpR|uI zU}Ii`11`*pb^}Rj>Gx~$v?GwNd^CE^WqO+Z3#cSfB8s}zqb|Oj{1(C8jvueJv;5|y zi4jNvUmIcMJE9Zj)a<*{>sr_KeI1e-ceCWBLn}U)uwy(!+CM#XPGUFhg}D`^_43M6 zsUQ^Qle^{}j9A!;uuwl>%dWqyU+XHlz3P?eXM|n}{^^k%&kvlf{I#sBctAje|Jk}q z**Uuy+1UImu8^$>(K}Kn9@fW)$1?VUqu%bg&2Vd9@|91 zHvLR?YrdfZ`y~%2;FPF)FMgovD03@=@Wps;t zvotCInB4e%+notca!!6uce(f6E~M%c6~KKUC1amW9JvViie=PRJdQlF0lq{t1k}zh z9xbT(q<+>UNbe|~X5N3o1b-=$MR(H%_9Sc@K%DB_fBv5Qh-TfPDy@9n0{X0${mz#D zsqn2R^ewrQ*!&YY=DSLG`-SEd;*nwg!y4=}?n^F%0PJ)`_~}OYHWBDk!v9O9{kwSf zC&cMb)OUmA-?QK4O)-AdJhevgP<+Qgsg##0?akS9T4$=BFgrE(>emVSrH|Zb+ry}rXImS5i|(f$ z3Ldw!eYe;7B6}d8BM|KfS0)wLJmtCbJO%A+YfPu4vew8r=vVdCMTI)kMtm8}z@6Dt zi1i9ON;?hpCK9kEL%tLk9|RDnDpdG5*9^DoQ!M`u+h>wq?P;3W;MA` zB>&}Aj$K{R88fMYvux&Jm6$YkLsDaNW-4tG#)mdM^e`hMEjK@RE8~7i%=kd?&hrw} z`S4XL!!CA(M|~co1-ucmTZwqRT@(Tn?HmnBaOCIKc-d?Dbfs97k#s9`XsI=~N( zy7iS+P}$VAF+V-G6p5%ZqV#OaeJVuivBg$OSYNrIpC6#GGMK$?Qk}}d5d*fgD&jQHKFy*Ae=YJXjBmNG(QL9DheUHr~s)b&sjB|4_wIzMOm9wq4<@AE}ofrO_7=H`yG3)Oc>UN%exgNuH59nf2!`NJ5Ukx2pty^t54@1S(<{QZ zh}N7W_lTkq9_sq-f5q??+#z`h9Vl4}3rG;Cy_OBXL==p?w)VdOswNDhMtQmB%^!JZ531#&PKaR8E5`izF9f`tPD>7`qk>`LDehcPr z>LzOAN3D-G9c{=)oJNq~sE)Ifvk(JOyUO1#=L8oQHI*aO5*|+CzZNYO!EvJgUD$nm zNPx)`4uJFICHlf%_DE2$e2jdQ!FD<#UFcd-qo@``?!Q#Lv{))8552n<70yjrFT4B1 zUE(Cq`MtV)njk2Q$5Q>+7Z}x{-$q?G9SEh!dR2cCh0z+qWF5)o0rR0u^pKiaTD4m{>Qzwdzf55j_Qlz0T zL6bL)h=#TTTY%KGQopB$neC=tWiAXR`4G-_og0zukrdb_^bngYUY5wURODFHa$P=H z{(zW@N%q$;mr>BX2PQO$M?|(wi(&wqA5E^>t5Gz;UJKyE%@5VOw9Bgc&g26=dS@)Q za2u7*(gAi?y!IXix<}@Kg6vX3Dqsn%wgS6HM^gUI+u|zu0^o!7CnlHIX9sCa#blHzSjF2g$4`oJ(N#DT{Y!_YeD2zx zfgHlPV2L3rqb(KnfUo4baap&S4pepX5@wD5*=QR>?k{utDS-txrC_!-8RO-+O|#0$ zw0que$RH4Ic*l`KHO6Op5E+&ZXoM$bC7~KL+h~|njO$T7Kj4^bDv}n|E<{vD;mh&P z4M`)S40&#G+V5UGAm3|II>qtIvD!*iIV5%mVO?=0uGL5^s4ex*?G0M@gE2yjy=V%c z)Pbo`8{VN~qM0r~HS}$#{KWLj=pj7RZvS*YV4`}QhuQ&8g~IdTMngwog=ZWU!{e4s z1*5Qq>wD#VZ(4waN(_;r2qpO@^acM5`m4!fbgM4Ch#?I=90uCCLWTt_hwaf1LI#+- zWN$Nj?OXLS#zemKMg;FiNmKnYX0P3OZLoT8mxcLJ?2P!uX#-VWJdG_tVAS* zT$?d5qsW%D&(*lR764AhL%cxL<~pl*zwkO zKg?9RRc+HB4Qy+r_Yh5rk+5}?my55iQ!z#;7@pfQex5hBsq^7Bl5SQNM2F|VD_L?8 zU_*|N4L~i%WYWS+j+0xu4-@Rs-bQ)_uRB(RzM_f}7d#kncYL6Abe@1wo!@*1exq;8 zROs0Fu+%8j6FG8$QJdG!=$9Sc5MOW!8NCX}bn_-I1O0?JBl00CIV7ekLb^ktV>#T3 zEpdU!XpsN;@Ng)ia7GQ6GOd_bIk{3^#jAkU*MLPWpfGYQi5KrTgbN^PY?6FWW@&0| zhn|9^OD{gJ5oBZ(VH-#V05Zzj(Icx_R3QSeDng?YmJN5I=>pw>`-wO&Z&XEh?`s5| z85w0bT$5L*Ps-buf8sNb*UmzhIJ-!B(WJL8=6MCHkExbE+L?PLFV;lxh7(D`s!z^Z zDbU4ZSEUuR4PexCrAW9%#wB~2E?Ju)Qf{h{qrhQhH)POe7P^uwU3@aA9E8=n-ilde z6d%v9K}`5+log9Mr5CH`+kph|$CKOVO5{j8x--JdN&37N%EYHG(~DC zanu%WZOVJUx}EQRHl@bS^XC*X3W&OQV0uNN|8l43N}dKzF$n4movX~#y7da$2KV(( zgYBR7=HY`@O};N&eS;^IIcj{A_rgt4y+dvC!ll%kM(>K;2Ju!GS@!O$jaqgJ+749% z!~PE1zLDlp5XeI?)RiXyc82rmN-t%4Fq(X2dO#ZObzUA|^hGlO+qvdC7i6?Y!%WRB z+_+tv-FcQS-C5Dn*3rKB86s@kT#VB(PCAP-6Tgg2+acVNJbP@O&*6>*zY8f)~;40?6&_@MYm&UuaWC@t1z zBka}@vd{Q~gQ8ZKkmT#UI*_Dzw5g^~Ykm0$;oZWrBX!(8tCY>NcW2Mc6#g8;@&@+` zH@#4gM0lG|ro#fDps|MgWGG^vO1!t zp=dkWE_h{YXXhEadM0s8z<_T4Qq2)h11i->$Nl1Qzk?{fn5d1f4i3jy|ziIhG5d~n@6&A!M z`EH*5rmAN&8jX4)Y?A6$W;OEqv{w2V<}7+=Lcho?57E9zFq3yXI=yzq+E1AAvhw@7 zQ_e+LSXJ+(sGkfT^|IW-vwnEs7jZ8>n(fk{FM8t6H?U>F^_=1Jk=?7vYfqQv6$3fT zL}tQXz2a`)Y}7YAC0kg!Hbz!2C*%B}N1c%GS=>?H85;3Ppd_pwqpAmoCrSbW6B88q zp&5ZZ>;GsUGN8@09ae-<%r2x0Qf9vCL^!fU>ogF`cX2A#zF*Nvc2PrTH)izFCF*=8 z`i%swfYtqs3m4loIj_U38~xa#mrtS`4Kat6%(7b+`s6*M)TKF39cWT#77_zeXkZQh zZg$+wHUHzWJ{~wj3p=-X6c!7khL7Ts!pXvpCr(IG5e=;j`iTNE-8%lUTJn|0aFV|0 z0@T3-mB-$w+l5M(9ZqUM&Y^qIH&f2%>1!<`R|dkhUO^^m}&*x)@R?rF;TzlNvyifrya1S znS3YT40WI+qFzTj|Fm$VG;=kZyp_`zJv5xthewEAWpR!Lj8lYsV>0Et*iG(Km3q+7 z;L&&FE*t|iXNCEdeA;h;e>%kyo7jW>f75iy|L>W1VS9TQXP3X}cR3?~or;T*lgt0) z=(Kb;)Dijm8_ZP6{I!r11+##L%29n?B)u zuVf4|F$OfcOKv_fe9wCCJ0>Tyxh#&ik&(eoS&h{CGooB=Bwu@DN!=g9 z5Hfu{E=NL{`TIwZ`R_!ICg`v<;u7M}cQa>Ur*cqtp(K{U!c@#Npe!S-!F8rBTGE;; z?9ND`2B(rLYAaKQU&Qh)Z!Ecf#J2*>jIm_oE@+<@m0zCI&^jzK+@?#W5-PBubee6< zqhW53UzG*zJy^OcuPd4K*qG~sYyslto5_~uHsT9wt$`BF%&7UzG4()ea9kGVP!mJr z1HjmNR{>T??@55w%if&%C0%;E3V?Wjo_D{Yn~g1*e#w4lW6(MhlqFTU42=V1}XJ=0yiq+mcs| z6GRJmHRCF5&W<|H6@}XB3|y;Ka~G3@!Hf)fLWRLuj1TG&Q=Fv%PtX`g_^3e+dVM^t zbi4ChFlWYm^qNwN)9IW2U*i zW5k5;{{-cf=2)tkDfFq}LR#{p?Ch);OG|R9-eU1H5Yd$UqVKG+bwo9wd_^{(@(8I7 zPsQ56(?`jqBXj0M3gFA zcF5A?;7(SAMdqkoXk)G8c}=vqlX?+L?Gbas)kFaL_&rl9W`78)iA7v%TT`u=pE(eJ zf59)HuT}n5VAeUzET3|v)<(b7PB(BlXf$oUHXf>Wujt$FnLO#2r#+eHB_Yk1hkBN! z(utI`1T=_UX`T8*(5% zL!TJw%q*E>cl) zQEvhKzes--S;Re8<1pYh;mWt598(8h6!>z0hfhJ)r;*`bfCdj_Ip?Dq-K&9uz(2w@ z5D<^@^A0jrx6UAmhIdFhBiPONATzvK4byc>sgsD!*Id)hM<s`KMt%R%(>10{`NCoKw=KdMLaQgYwM)`4nV?fcG z0IN%f#4o;r)c(y`EeH_|77vA#Z8ZKSryYw-B=4Hg9|C!e8GpA=ykvrl;!+cAvrSeV z`USMr>)nLR)wz&F%N@Q8)^jk}_UGRsgyuRMcS3*9D>S160a5FTYsgxtLg*H-yLCn@2S!-1+QjUQKo4I?d&vE0qs?DD17$;cMzn){gxjdY z#<*2vN10|Gk+Am3d$#?)EwC?;V;dqKp*}lQJU0nz`H0(eJ1?WR+lbo~J1-NrzYzH; zcM!%LIK#X}Uh7Uj!fv-Q#35qB)L57^lh;0pcnQ%3FbA_{c_}H}$1V$ncu|KvIWhYO z?tMvvVuNq*5b@#mP>6h(gA~S|=Lqp(Od8{SwYziU_fCa*V`d_fX29;=7`<-6lpy?l^Uu1@jR(=oAduc-v6%y8D3 z8=9GQ)}%PC)3vr5;P^@n%dHKz+2`>@f_>LDHHz9dB^O4yQ|B3ZOzHG5m#Sd#uqQXx zI`t5bTHDDomsqH5`-ybpSXnxvq`)rTWvv@b=I50GT_&}~@uTOM&OLhYa@+GZ1Cw#( zeyM=Dye+T!Yj)b0l4gUx-zmy8)RnyPIXS*w%nC-TNTtbbSQfR*Bs(&z4!1gxtnY3i*n zDvNZB+VWGeg0;lO8x^0ey9l9pda+C@xqF)+?wBu9xNgKk^zF(^p;KGhdXiJ&EaHuZ zZ>iB~;OV?1lRilkNS4y%xMq~d{b{o4A;$-#?alAoDXIN4;p#jbOLbBy?4*SUe+G5}Wygbtfh#&AS8M9(`1* z*ADz}(*Km0z2LOro<*RCpvV_=h;1mUvtAJ!^qqej=r3N($biwAJzg_k(``iBs)-@* z`p58j}o8T;Xsa^J}QHwKH81oFP^5Ps&+Z?K4u2%0;yiz3> z`-m-wt8Wyi}Qns1qXCgXmzANC%-M&o{{ z!=KZ*$O%Pub;;CZ3M-3k$HE7CA2*hVMy+?S3Uyp$*9=KLelS^h?5FI0ablRl%2`*yQdF>Fc%c5ku7epjU1 zd)WM0qGP6asNg)Ibx&GskdcOdsZeup4AwvIVcFcT-IT?ld9trC$fD_CPi;!#M-8gz zzI007H!HO+FRS3!2yfL5N_wkry0%Me;)$x7z?^==xDJ;{d7v|<=(ZFup>?+3qf(b) z3O`JuM{AS3=Hf45{=IPnV_&vE(ONH<d(PS%wo)JL?D z6?+N_S?w_Gm-Rn_DEIY0mUCd9&*{_>o#cyy!GV9Y@nL**BTmU0*l+)1L3c} znjz>%6aPw~nhL}duhAC$>8IrGr!!rIKg)8u6h0SAVWVafo}$+}ClfR)Z};TK?ntiu z5Hn;W53Sg}z)nACPV%B=@~wE$%X7pG;n@xvHD|daca0>j@HTgr=?<6^I5z%8bMvz}bvWE&JMD42z+PPJoZA#PVN}K;tzpkQPVwC1z)(3$J_Os%>8i zncN6sE$UssC$a#hZ9e98#^^K_4iz0Mhu76k&+^VM^6oDM)7x0r+g9#3vKIDJM69-u zoH@BiUfa|;wUCnqZ}HxMJ8jP+2N&P#K`PuC37{#>D41D)7_8k$2khlUbCABB(Ji^t zA+jJ(mYSjB!<(X@2?vzlyLK)-oY@MXcKyt!t72M%z=>&2h#ez+@{Z;SlxowmidDy1 z_RKNJov3ezHgJxBGZM`U`~heRp=*&(SmRBjR$vxOo=xp2t-(1rux9(<2wT|^euK?r z5A{XH=vtu+by;V$U5)|g#=Jk$76^zgB=N7X>#@dTxZG)3cUoC|kO9o9A?F z_Zb6aD2-Xfbk}##+p31;oP?7x5k&XHv23soD)Mc?vk~$tp-n>T>UJDUy=8f=(Ml~1 z&}a`~&K%G$pkU?DXj(#fJ|Ag^G8!>gd1-7BG*(y!HF#XHcHgv1g2dkWXn~=+d;NL` z@}L>Srq9sCSo`nMUuIi>X-dATPvu)=0U2!XNPE}r+FuX_K^)>(Osmh8akD1W#XH*4 zf*O(af#z46nO;zG_lL3HZ;18|^_GD=RR~|zINd8~i-SyU$xuC*Ume88GSo(AP1FW4 zA1TK~2fKq(a%K;Y1EVCi?a_`J4i?59!p&9|J`A zvgxbfB5*G4Z$X>UZ(lptm*>@wEtJi2=l^_K_@__q?QZBt_7{f2|AnE{{~3l#{9pAu z{|!d}%P;@$l(hzg2hI_e-?!%7*h%BTsJxAZRuE~*(I9bE+?;h0*4(K&Ck2mZCromhldNAV%NR+VY0M|w1Z!BEvhrosY$gvwT& zMri(mU3|0+$J&U-)|uGYaTbDyg9B)OqR`x=%$JEN2-3nopRY-#j{pJuzdmXCJz&je zIX2Vun@fPdb|1z=vJd0)HG%iTOrV=Mda$~7{L7kc@#_M*JKq?y^z*gkvgc@|#q1mJ zS4z&d@7!HqphOLJ^fT-;J{G|R9$&+EuVSsB;Vt)PE56kES@~$1E!n(E2iUU4JRTMn zALBfam&5(2;7yUBUQ7D)y4QNn?B6n%4~)4Nc!k!YC2!=jpe~I(>P04^-3#^u-a2E( zc=izI^1={TMS%=fQh1gU3JMn*qLU%9T)ym4Xd5in>hjT~;*mu0!=Pdd<`A^D@pODA z3lT43xy^1=?_W##1Ch;6MHkDcZ){#RFlA!XpY3FdH_D`wqurwOyLP_A%xwUQs486~ zMcRcXZ{#A|(VX#hajPOxLD1{FtZYf~cv7}+@vhQiB=#ZJyj@@3^{S~X2+DOw8BX7 zLHkKe{K)v~$9tHlm_81Tr>n-i#}ITc z)sTUZ@n-4LTH5!*EF}?3r1U)Ki>;w1BZ*;&Kg06Hwx6bBW<_@tSUXO-5!J-yQPl-$ zPy^%KsB+HC=;D@Jj;XzJxWe^ORq2h!;;b^*A_`Dd)J7LF7EZrACJWc+bcwMD*o?)A zO;Sg3pN}Xn^h%=_#Qsd7F5Tbce{X`SkKk(AJ=W?c`ROGThDhla62+)s%kKw;P-Q9J z>cQ;{ys#C;E#HECD2l+3uzf%ZsNbT%2@K7EE_;{?<SzPT@a@as$pC%?mn2 z016|~$bB$ zQEAg*%ABv+gyg{Cvs4=**~PK(di*^Zi+SMctU80;_dK=sOkXnDxe6jt%VPYS{Co(S zs$K|%rnq%EERlMHmP2Dani{wuo*$Pz5MHWI5;;poM0m9LAZh+k?UQYeR2^X4Y>D2Q z#2Yb&E6cNnJ82%Jxv$wD27z+se0!rT8cDA02p5?p@ip(fWgc^au5CF@F6ODHyvEif z%4F#j)m{UjSi_;Nb(E_@o5F@S*3D~cAk?E`8qcets-GR?uFnx8nLlD)83YH+GZ%IM z`onUVFYhbXk-xgJ;Ddw3TlsgA?%9-jc`E{4~7jZ};ENu>i$CvPNm1Ao>O z`}(-h84g6?rSFagWOYG3n0(*NF4f^0IKMaZm5#^iLWyWQc7dj9W-- zTB(@br@SyMt~Ga+K6{*&N4UX#<~%scQNy>$M&1*R;Ja$KcA0Z-7`WKv%1PsYVOvJN z-%OpiI*I(E@>|Nzd=b8l#{W1C*PKrhBm6iO>X=1Z5;ZJ}bFs%v zUqsEj*4}RC<&9h))3Qwb)ed}a@fW+oC4yCD2^oS%H(F+7^;Phy&zP(Ur#C$1XBQ%# zBZ-qN>4(~iVhTaf_hQn!)m^*Z?NSB_zQioseWKeOn7(^^gzI3S0O3Mk_c76 zJ9WUXsEiajOUH>P6F`gcITzI!@9;;O_giCSiP8LF6Uo+j(46>g|N9*9tePi4b&unkLkmu=U04Hbv(45kb??0SA5d z%a2IIAW^iQRP2jUiV;w zG+Bdn$$v0vWk{;4DBAFg+25!}Z)yp|(ipUQgp!VRcoo|nv4-3hh2(t9BjTV&&x1PW zxHZnePpk@oC8FqpB8;t$B6b8Wh(Itq(4rP*-wAvcEihXY`3hDyUE*bSnJWTcs@^qC zr)y*S@_wv)$=$7Yts}y-{hPL}g(<2aCcYzE-|Kg-BaRu3w?-}uK!I`1I;Vu z-TapP*nvWXp?`NnO6I2IN>(@+UuP%?yd(q1rm$qW$(TIt{NqrYkDt)dHJ(=_ekQ zqA-2{(?VblSOO6g@~cDY%)E@+RVAyMR=+wsk>P;ltFly9!J7;)e`Dc)vycT1gGl5v zfvQpu7hK(v-zCgAPSfx99(hxHt$>L2?1|oZQ;Om6wFV;r3a;BM_KKoky7iH zGvk;rjS<+GHsRu6xfVWQFZfoc+3$Okz6a08dOOXa_h|R;+T|%orgFhou&&H`6u?<| zfO}63&8@1*ZG-CC2`T|!gUd^$Iq!&%p{lFuC?!GDomU8Euy{x2qt(z;u-6cidZ)(a zBR8^g7ftw)Tb{#s_4jKgxBgIzOJ~Rm+u+ghhVpepk?!0r9^B5-Eg#&@%56vpf#qv{ z1cz&9FhTiieMD}Vht3aFOsEOuY0+&Ly*=tvIXx#nCCOQy)7&ZeZsYN|#;RM7;BsRwgjdu^Dzq?5y{;6T#luqo(~qas2VN{ge;0qA$* zm-9`Tvs^XAw7SN}xr9kA;|kR@lxgQ<-R&1Ei^-3wv%|w~VOWnyE^{OAc{lWZn3(jb z$XBq?d`Jqv#csnRTb7Ak{K?g2AG`d+u&J}V0h=hP8SUQ%d4>{Xx-uUBi?Mf%u54?! zg{vwRR_v8z#kP%#ZQHh;RK>RKq+;8)ZQFLf?DL*;?>_rI-)Z;9Xlu2$=8rko9FO|w zPk*kGj|7Nj(||k)BR_Z~=0=TxThq=rLbCg`6dE+fiWV%^vx-^LTBAWZ2F>K_vXjmG z%(|}KRim6cWNc580kN|d)J@%N)wtwQbZz$ePp-VbFr^|H2ul=_Q0ZS2a`b5i4~b7> z7{xj>a}-;4$qqFl4_t;Q5;zOAl0enqXobU^SHLPt5yTA)D&tHBqdOcmd35KBg(0L0 z;#j!oYEbiiB2>KF)TSUO-NFy$bd*$7RsTqqq(YUH4HqZGos}!;E;^Y93#X0Zhv%Qh z#3Inom0s3Cp+`GGWA*y|eq+_qk$~Mv(vd>rs$i9iQ^ea*x{?Rs>6t*mYL^x~d;Lh1 za%(Bx)&%M3q)~hw?yR`N1nKB=2P+>^M2sc(GTwZ61Ua#%D%|1D!dDN4q5DIXt(I_m z-H!EMv@H$x9@c@*8a_b>cTL;F4Mkp#+4!3ujJs1?ah1{Lham`qik@m%QKRLibz`pH zQYREQ<0Zm5`lHHzKe+y`2Pj5vbxB^Up`&_{oWcsQ%EQE=oH_bdd`VWl}5qsz7|(o7c|>a+8W@&5mTu997WNBNccp z&}(rhr-dUE$Ivzwr;hzv`&ny}^piv@`IC9gEg;H`5!uAatmaGt^WYP6*fRN6Iy{)PG zRnEl9fQ8x|cJAhHNowZX%<*Tedu(D;wN?eajk0gEqaX{lq@bEiB021YCf60RBnRiLh?be5F_sdo>}|YueXz$)bfGVBI&j=QcLW|?jGHy97iCKq`~vl| z`ZtO|!%I}6cm#7$E0an@3EuAnMow^a}Q8al#hk0Mxbe7!2 zee8byy!+DP9b2p`9NDvWjy`SCK$us8M(zBf)aj04vk(Junw~ z(uM6zuOUs+7qcAjRDK(+A@^{TK-bE!Gj7q-nZR7Nop~ETX7l|L=-bI+S*d=5Tvz#5 z=SPHeqW(6QK-5f<7+y-eaQQUz=smP1x#ACE;aC=fO=67L=s?wq2>~a&w2;$nR@9)= z1bNzNl0q3mViKn5jbhKB-wp(I7;;CQI^@7GF!>`H#yGJOS*G~;0J)?2Nm}rEzGG&v zii*OP1!tF-h#844vRb+{OK1~ttxZdpXhZjY>8V>Yop=y%vMV3&gA|uuv^S20euea{ z;^=2ckBpKpWnuCMU-q1l`q|3)Y9ArGVva0-&PVcZ&Un?BbV5`>sWDv#K-9kfgPEh3 zj`~CDGZ6o^{u0*tmt+ssf9mj1{fCl{&1W|#wax$0nvowLlO`JzRTL4G92p&<92%Y2 zk-Hz8kiQfgm8Y$ip{1>&|B5dTYPm~?AUrhQg5?Ur-72g5JJI2lX+E}i#$L?~*t_yxCxRO%hO$MjZ zz76qN&KMOIyZ4^u1WQavaZ=br%iMR4>kE zB>nh#dflxQDNZv32MfalLru+!c1Op@_sWQBXh4Z9vO$=RVn6A8WBqs?Ogrb+$P8(Q zU=^UxK0susR-$keUd%WGJ#S4((>SRpDWQGCbl#z*cpk$bxww|G&NgqEc*9+2=*gfS za2=mui43h?#&YX3Py2(Tu3o<+rDny9S+Qh1_N=~Ce$C0DWsugBZDDC>!vm8=RH0ZY z)NmMePU8nZyC$2G^c)|5U064E(`9L&MCO&|naTndRE)w3?tXCA7qj7Dvj)dhUE3%b zoM@0O{&n7>eD0NO{nP}!#^H)~ljmUl`cfxkb?Dr{0^L7S0|nbR{0Boad(f#zymmga zZIOC5XYDz!w$Kzk256l1AT8(Mu)B!1hqw-b6UvTAxMJir_a}i`b>S0?PFOuMec@iNIscffq1#hTPvFXc;kQf9~t&pEB zRBUa(CirRrm(4*85pT@Ykfc18w6hSsfZaa05Vr>Itnb0oO0vt%RRp zJ_Dp_Ao;5@a6#q_B682@f~!50Z=g1prZMHpIR}oJt=Li2(H=o;(?YZ~LHV3|;DoMe zr3UfTIhwuW;34TKSgWUYk-id3XooKy)rN4c@Yb@MRIV=CqGE)xS_Jp;oYn#vEcnil zYiGS!ifq#9B9N#dm@E3xx9nYY!YahI=ucM}e%~1y?1!|ZW3;3-J_dGq2Au?B9?J+q zTa*tE%v5*%MnAs(cO#?MHjFIzw3frq^$*U?Uq<#fYmw2lvp2N+FA*{SFy6mJ!~{P~ z>ePNX?KKCP^H0bie;4dR>UAojGW{lcXG6{TD-cm$quGZ0h6l%J{p8K%+aP=(1iyga zp@mibre#@D<)Gw>nC7|p7>Oxx#gX75hIZqS51@Q<+_^RvhiBxMUlm_eN{JH6H@AJ@ z8jtWZ$XTssEZ!jkh<6mtC#o5|lfRWikoO@XddvpmVSEU>@h!{)4Bb8RIs6sojw;#X zA3hYyr=>Xi(?dLfefh%q?;hgcpH+CYWhVw%xLV2I4bcps5#T2U4v6}Li5ZeMON>kB%^u=v)32`MJ@A4fdeOYT@Wfql zS|tB?n%!1~s2 zB-DGWMrrT66SeII=F(FJxYBvu$^Fh1D9@XlM=CYyfVl>&t-43~nj@5or$(`%LUYJ& zn=C6JOIf6;1+;DM&?QZ}U!E)jRQjxBxO1m%O6E#W#woNK4h>q(F6_)xISOWbdXgT- z^BhPrQp?Su2!5_RhgBBWb*bLo<;l@YzBiS}?z?nxgv6AzAKrNou-4FY%5KPJ>fIHi_-nVGxo94MI*0p_@vJgibOj>VO9!hwvb*kudS?^&P z5G>=;5M8J_p4r?2Kl0w=v-gX_(Re#Xf zLBD+YOCRwcvGm`!{=fFT{3ps}W&HW8jqa!9*MDi}r7Q4$21?|2BMw7d%WQlFK?+h7 zs4~iYh!{kCQ$0S5-tYW54cQXIF@}k!R>9t&QtOu0t|ncr z*I)MAtUFw1Xby0=zmjf8|Nc6PPCJ{UuO!^{WgtyW0(e6%Szn~jD;%6{J!e;{6ONaV zx@5G8;;c*$tw<%zZH{#+a^b&NMMkgTXx_d4natkHMuEF0Ohm17LQ^g*L2-`!QC{`s z-wN)$Lr27nAZQDr^3X=K%0G5RN{xvLnuIwbgAdR_+DgMYAOd3?X^D64G6dzyYuT<@A-uW5@S+{2 zmLzx8Y*_)v^4K4KNWv(IO}qpLN(RpJp(=cjrme^6tFlh^nDiaGb&za!DPr0XM*d@Z z>wvwFw1OXfYI<~G4)UQ}S{JEV!BzS4aQEFT0yDHzC(!#x2pEir%7;UCCSoefFw^6N z;xj&vr-0grT@&=2I%&`)<-9@Z+AobE2t`f!t~osy@VTs&89ahv9lysndT>j?TVrIP~{}@qBace{Bgw32oB=Omk6`4WBvfR{q`z6w03%X z4o0n9qZCCR=rIx<4(Asq^C!;jrZA`zk($>Wcla?$jb7-ZIy_**(<`mZUdyIIz#(+? zb(cWefngbC#Wj^x#RzbRPb-R{nM#CT7F7H3cBbU{)d{PQ(V)=!_$KkFt^8G*55f5Q-ek4{}9$G`O*w8jqC$S0zF+>L$W zXwB5Q0GH<8OK>1Y(b>@uspNS!Nzv4EOb#oqsvHoYm|SCZ8`iqM=5*i^vMRlpVYS>) z>8rZ*ZGT*MU!gCcuF z%#n*PixC||wP9u1eNj`v`iK!D5O{qgJFQ8rDy@00Pn<1H;rG%ev02nZMPzvA@P-rVb>EqHv12}`sMuMr?o$!9rJI0#h@qe&T)@@$!e~mpCJ0{jQdlq;w7=Rv6%d zcT>ez{R<|Eo-@QGYx2l;F`Bpr4_E$*@uV^SZ`s%syz|Y!WuF;#a+^I>FeRr%^Cm*f z|KN==yWn62EUJCOyr3(CoLMl7W^qnb&SE56QmP-d;z$N8NGj`S%U89eC}WcAQccjv zjY{bNSF~!@yx#J}b3oZsl&JJI<(%P6#(nnVCC47zBJqYD#EeT6T=6JmbQvKmNP|YS zlpB~9wg=^=GZ?^qI4Pkj@U_9|@U}|7e1E_DfREa%YH;UOx2^U~JZp8h{`^R9J>4GF z{)%Zgl@(vL^Q;L5e&3Iu=HwLX({f1$>a54ZUjbRFZKKs-X_c43tS;)0{<k(>1wbBJk{I}1rJbYM7&aIJRxqe_V_ zdZ;2!m>_fT*<&j^f}(S5KI_#iPiBt4@wuJ# zmhF-@N)WBpeD_|;)9i$6Q7XqSqsfK>P>;1ByIN5J^l2F|96;U54DZ!>1^vZ%&^eC4 zO_}gH!Qhd%L_1%mX*WGhu`k;hDJDnjBR8RLXXmefZOJNKw(_!on>=umAAL6oub0Th zCpVG-r1P5?fGN%tMbu02htx|FV3om1>PyyUNbQAU4```b2MFkI$w&1e6(LFM*SLT+ z1Q}N;at5N>VmKp>ODWKKuloA_0JOBzuEc zKgtUTE``zTYK|U0RF49q*$M#6YFQ3#_pdtUJ*I3PC#aTIbkfo-+u(hKj9GlW!Y8B? zTXaHCl~6rbqG!U?=IotE5WK!cskeOQW;H=&%IY*6zQEh*c2BeJ;9w>r8R;uhF3LdCiN1Od{l{D3gwY8nJ7jUApQXJ?o{dEaqcMV^m1?bk znr9LM;Aoqbqyp%McTi9%^Bqp+qoS z8iMtXqb;a6n(`H#EzAe%glvD<<78Sdlu8TJ>hrX6qZ*C;=~ zt{}=K#ZQiCb0s0s(eQTq_z6-5yHM(>^2J=ly5Q@@_bdn$;m-?l8Xl^#Q1-0o1RY$v zL-S2>PXeBY1M)8zk6)T^lvY?o8D#29Qkn|cz%~NKy4z$-PG^vD5QmauUd$ zdj1@*fLVu1s6VnvQ4xfD0VXQR^g?y_0;muSn(CV9(_zwhD7Krz@U>)=wWyGLj0^^I z_K%51=BO4f!}Qy@?p40?^+RaUhY1^o?+5{)KDf{lsZkXobc^JViu!U_e8iMz+HfIY z%)F^Lrom9s8+MtTAyv==)OS*e)5)?zXLMwi?f0ba$6*ThIl}w$r=SkXHDhUXGuo}& z7&$#}9g*R8sDxX-E|b+xYv9p21}u^w>Y?X3qbXNkpyR8;enIcIyoAg{^dH9wrsO@j zz9ri}fM2g-Amq+H`L;bSk!mK%DjajsVl3<@FG4bA(iVht1qWT7IasajHJks7Eo?ZS zqwyru0PrBPodIWyqraP)9~vG@nAdRV0M|X!M*QOnpNYxycLu;s3NO>4b=&2O1(>+5 z&tNi*WDK&nZ=|e9tC%_EDBvWmM&gSJB5@F{@IC7_w-?mRJG-xXt&|M5yg~@H;XOi` z2#IL!WTZu$mkg8=?epIVAAiBrTfZ_~H$Lse>N6GQ`G2t!MJx0FB@u2@6qiPlMSef8 zok=+QlK-<04L_%rKRYfk1UEz=7m^n#4zxv_10Y<;V8E{U?(@;-PsoXgW+nNlb%x1e zb?b~ikuD|XA5%}gnr45|bntTCw856{_4xK#>T+691nFwqFhVxRpug>Hq=?C#_Td6uP2>jyp)LCh6vAD8RrD2dLWi5|p(9o*A)##$PACdag zd$t{7dZM^00`$nh>(%tdYq}2W*klV`5waSymt4upb=iU+q4kJcIv|_BLL#izSuSW8 z%qomzU?wrCcpPQtcUdBnvt7eCtdm=wT^sP2neGcs`_?euWFF!P5A7~3iD|WIN zlz*86X4W#5uHW24Kx#=nfm-Mk34Ef9=(V*Euip`OHl=m!JQC}WQZg^t@&ioETQ zXD<0#9!E-JzE)X5nrFXc<*9JL5Ntw4(nC4Aur5+mh1Fg?Uv3z^ z)n`ExJgfVBbYTVRmi;m{cshr>ecNl66MHAC#gpwFQ;p17>`p`hhy07YI3w^eIe4{V zA*fFo%>vxtL~A37>oc8*mi>nw%um0>FxLl0C9LtQU~c%f@`EGF8OTSO_Ve%=WoA`Q z92#j)4zzD0r<*E%gWiPRtmyS0)efd0@+Jy_l)a@K2URv63Z*1_ZFZ6t5Ljf%R4&T^ zYlS|bL&vrMDmQDZ||46FEXJg!c2el4!917aIxFPj&U(t zU9nyx;vrU#NtQjdskU*|@MC&3?S75nl&Dt{zU?Rc9(bv$v=g)5W*v~l{#Q&@9{HtvIS1}*1q^^vljJ^g!hZOTwPoYyBMS+%3&!Q|5 zYK}Z4N24@bCSNIVmoW@Te`K(opNO*^YF$R-S)>Vy=2%wF{WMwkO!CaXe9i#qCS)-6 z6d7mRcpB$+_uk;9c-pzp_6E76ZV%PxF;X1uoBF*(ecI!JX0@fy){_1c`v;_lkKj{X z-uBG*^Ud-Q!=|_75O>?#S!qvdu!MZWIT(Q;o6@izOLx~wczLJ*^zvW^{Q|?)7Nj(Ce50~Wx>@o zm$lve9GHO@m=MMT9a%~7-G0U)Cd3`u+O zT0uS*j26^$3XL%n*Ee1EWk!OR`KD^vs~DU8hSo|3E)1I<8}rh!x?sgNx1pNfKDnHC zsD_gsfiB)GL%W`9>urwCH3Tu{Zu!kRPYe@zzQNXgKP`XWUS3mpu?ShpmL((Cu>A6D zGD;G}OI_y&l`NF=Ycfcg*YBttza_P7s;FWT*T~g_(iZa=S@O}iZ`9yR%V;oj;*|hQ zBYA%1A|aH9$#ogjvSK-F*3vZGVZ`d>Aa9@5(G%&x4NxdGCGC-uow`IRH?8C)A5v-} zcWDd|j%x{L*H-`l!Egk;Q=d&(eR46dcY*^D*xW-Z_O3C%QCXh=u`qXA+6uSOr*C_& z@9m9F)h~f?@7g4(A`=yy$e;sd&)cYm&iYgIDyxIt>FUZ$MVl0|@C>aFqu0!UjfZjh zE>cecoY7#CRwg;8K%I&9L7sTmMbLzy7q5W@ zQZOuK+v7n+qTgY!trw2pv6t{ZX4aXQKLH|WEyUjR0x1C;sjRI zm#6LG>-p&ysp#XEG;^$z=oy%7V^)q(`;0IwWqRYlyt~*`o7J)eXxNs-d0r{CrGwaY-OzetW$}_4yIfPyHP6!-tg7>_u6&h01(Bt17OT?6TzwPWbzn#QrV=##G z0HLB7fH~FJPIB{gZ~|$`Ajpt}wbFTzW3CfEAIgj45WRD4dYhn~OySrpu+X_+p1XOK z{;tH-pK}{C`ehD?--aNE*@M!&P3~O4+7gkP)Ni@E$x{j7>`aqRxyr+P|Bv|zk&V}%9Y@?)m$ z$)p|G*#7y|_=5b972b#-y;>DeDD9p%D`5b9`&x6b`{_>vB^OM{KpQvcTzf(>B=cB&=AM7n&PV+CSDOZhZjgzEgZOXQD7?}{66|y!V zzR6EyAE;=~M`qE0vq2*zBN8I%H)&7u+pbThsbXslWRQwT&5h0ZD%XR?dDG3o#f9;g zz{hO@Ur8KlX0A9+3kCz}{FwbPJoztoqrZ`NrI%A)2G1F>E^~6esQh zbBgQ1vw(el!E9z0J(F?A!|YMohXU!cW){`wvLYOcX^dNcdT6w{eS(j1-=Gp&69AQ> zN~W4EH}ZZ6Nky^2KL2tMc_zNDUmy^S_Ed?)9B2NFI88S^`tH%qG1J&9lfenaZ;CiXHHGA)+d+xNfz)(7MTJWk zN>}soV_X^vYqWsy(T#s$>fepY6v@c(1v{11H9 z;BUD2f5PC(=FXoS)3w}%!JV%DZiwCQx$wS*csAIzMD_Z55VOB5)HKNbBp1Zv24VoSz)!*W(mv_}`t}(Bvw67_|?%Nn*h|ICl_UiUC+|Qd1 zGTJWK4u&(|KQ=xdS87(^H$us7o`0yf(dmk;ZCW5bAb+7cR zZb;4^$t>vJl5H;SQ96Ql5;2}HOPpGNq4|%AlWQqWnkH(FP%GZkNVP;LlFKmIj-b-6E80;X2jvPY-Vh#QAM)Jaxl=c0qT#EDWDxjM9i~T zq*ju5%Wr%pBjtJoN<((SC>Fzv=vYpY1FZ6lf#6c)L0FYH1rP})1;sy%B@X;lMq;=laY2*os>QMDJB(L`xp?Oh zoY#xLFV;U6mYFS-mv98(fesmAppGz+--9iFiWPC%QuXjQR28&s_ivYLj$E9N{+uK_ zj8wKL+hEoVl;Pms@!w-Z~n zfN$-D2CCLr>a<$k2<0b8V=|PXf`kbbj;WYd86^xe8XSCWQDHnFZ0fG9F+n39y)mlN z>K)S22VLs)SCl!2;$}cU_=8e9n^oGT(F|4KZX<{!IxCl76@F?WTmDTaJ$gbsk|bPj zPQ%}P-{`irVO{72V=EGDah-jk15Cqmxt!g8A5T2au)5ExfmL$=yOqxlG)TZ}kM2NvH#?bBkiy#yyv*hXO>^9pstUr{V2 zoHKF$Q=D8^@Zkh~Ko|IO$pyUFjtS{FWc__{{7CWM&%GkkIeO|NTPkRC*`0QzzkaGK z_F!FcTPR+iI-Y7A%-Er>5tgkdI19aGpzgLlWzftwhEqK?|L_Rl7@}VChNe= z$Pp+>kPHjg3qcmUDD4&zl}br19mpPJM^$Z;bYKGRw%wnke75851L2}I9jy!ITuX>) zeHK+$RA~=)+J8ZDyv<1^aJ0)(r{=%fMVg(nl{CCQrfJ&2h-Yn}|Kf#T1kT<~F zd-z_ne9ce9RxryL*Yb^N`#pKe7*sx;xn}BcnC$kP3rFm z#Vs^23*#_t^d@7lpy;3}%-1Euh;nx}%@-4Z8KAcWhalP~7yTK`^^K19jm24bT(t3@ zP&-CMyXDL7gRf@i{e|B<>8>kdjj(Jn1?@+AFY94MfEef~|`Cd`)8 z3xq6Yi2(q(gTM)aXeNwz_uG{RtX&h=YHZh+GJI4$2&frfvX+7R z+McI%)8S7Y9^h32t#lMsfE6|DlEE-@#&@(#+e*#V92I&eVOep9Gk6;1Gex4lFM#nP zXv!nYBJim`aqJupP!hVG*k^WOgyS#iYg17zTy?8!wDE@HRxLp$yVuiW6FrG18brZL z2Fh^yD?%Ec23eU9y;G{}XSzSPf6DS6xhcWS_5eD2k}QYGv)L2}yi*Xh(Qv%|wYs~7 zkRg2A7_~)CSVtyodZq z(}&&1q?V}wFWL!%mRa2Oj3oN_=AP=3jQA8C+eX|&;6ydBg2#)-IzHkh+d}15lsQA; z?vGD2g~nH_MMDNDZ!V^7-0}Ut$IVb)CJT?xoax7B&cy!j73b%4K{G>rhyOcn@NXgX zAMC>P7;)+DpUD0bpx*^Ncw~BVPCLJ2AUW<3$c9nm5TZZub0pAN210{Ceuya(Q@?)w zoSuMOOt&boLwSCb#TMJK7c^q&QU_;?$Ox9V`hObz%Sga?5XHrbQyyoXX$IUQ5GV?V3GkURSv!I)=eN?mGjXNE^OF$e$IpL%!1XxhKVzR zbJ4m;pR3LATflj3Bh7LI4VE_8wSohPG`rFWFx@5+ynH~X7AWjOmHN`2<#zyFsF4`i z0Z~u#R8R2(+8SpSx9?}rw00v7v-_^YIXi@>>m4Lz+Rf}o7)nLoJ&F90WIGqd)hsM{ zP6io`c=%KE8w(bo^78GaWWDMT{O&18Z(;pBog`v>R#Y-@=vS1L=}ld8ir|*^FX$3Y zQ#MX?3L*^emSfha`X}M`evb#q95KhWss%KfJPa$V^3`%N`lxOq5ceX$A~;!clZc@X z5R)C7j`MAUE>k~~xwo>ah#=r>&y22AIC6i|Qa$1XQZZB4z6(SSL^C}ba6NJ;ikQyL zGMocP3oCgfz~Spetct;nxCb}69-E6NX)gE4fzcq|NcvV8Ksn2R2bsHzm%U?96MqcQ z8!K}bv5<_z$sA3phU)s971vJC=hdmh!#{t`$e^dVPHd-}Pgx#QI0b~(=yB|&pD5Z7 zFWUdAcMCGTH*8E;e9OacSADttL}sIeAD-HXk|3fV%1H$xhmV86gI6LbR@o=ETks2@ z>jQBy?Fzku_l^yf-4;oqi-R$w#T#($l}brZ(eV#POO8?!e&T2DzWND!{qM-vzxM2Z zk`&e4brj~3KRkyJ|-cZ1c=rdz(I{2YgWXTwH+zXRd^SdyhCUA0@enIB{EkSrP(b3i8Fl zpY8kGaAH3Pu%;LxkCRBUvks42B$xtnl5+GjC18dbndHk;j?4=1Ng0arN1?^?iEA8Y zcRa-sUw!w^GhGbm0BeTQTxO!r-42t#%)D(H5 z;p6iLh5+gWMPAc+gF1|Q(kXMFYNew_YSNQA=MrK}hf}$*6+f?aBFrNv!)|Ie#aKyR zHsK|k@(3$7>2PN7J&)o1!o6>r^&&0Az;6&?V^@vw7NN=YmPfmxy0<8@3Vf`=5 zh;JZ9(-K56DHbTB)(n>B_N(>VPK+pP#wdu8!)t3zFVx7R>!=%;(x;Ivp=c?hD`HLs zqjC8|f^jZgqVv?K5)Vn|$O`qsmZ>>wbMpq7H3I5t?($00>2*v^0<<&9Y$PeQ^69>7 zvn3m0!h<@#bEzU~pf@QNBkWm@9m%!VyIRAYqp6Ehznj((CBz$ZSfCA!>rd$(lEZ!f zn3PtnVX06-I10UN8=29Hda~?Q)Hi0*F|02f(yO!)MW33Vil;;;%`GB@G8ZtdA%?4$ zAI_N%**611iS8SO;Z&k)uf8qeC2fr!xMotIAeiHt-X%pClljBu2x}}&3FI==aRK%i zRVRv+en&ghTZJtfIgjRie50CjVXRY+}e$IAjeB`z;vy#51uer2Yx6w)u{VaFPl(p%ws56WB_~r^7z!DJvpr9H(w8D#N3F>+EUW^ zG7(a0hV^afYY_&?LHexeIL^e+ZBh1^)A>giZlwiex@#yG&!71l z%qyq4Tw5AM>fm55i&nqrz{3QvZ$~8v36$uOq|Zy!BYD!5LcSHyJ-7q87#R217}ay? z@yXZBVdx6)ldE_u2&jbc5Y?{~1XniQ;aF7E(eMrU#7ioe(CVGkC8zBI{sE7%~!uX0!@>3zv^j}O&;rWgMy^}BV*db z9Z|NxUR-kgyhR{*WuJg3!ODCPal&78?3fvJnQW_YBOcp9;*X=gfxHY=_AB1@TprxVZtHRg%LYrUaRg+#5|LOZ1iXYuG#kL zCq~eqn}pA&ccXqQ?}i3={P`t(zfHGW8A7$e=dR01)2s1|c{ivRx=FcR0`Et9jc$U8 zlVbd`&Y<#~?&x)Zp5(dw?ZZv;1X&nFou^S4%DxWI#e>D&h|fS>huIXLyR71DIlaEb z&7h2;U!UM=2&JqVXO`41FF&@N?Z&E6|4y(%JRn$s$nHL66?0nSC5jalfAYcf}zpI}m^Qc^YMX3|Q*{?c;bjW*J_Nc^Qn*;0+=|zo&+_-hVs<_y4FiZeU-H;##yUufM-`F_|`U>`!fz zoB>kWRm)X&otSVJpH^oGa2g8j+=Cu_V$!B3lIZrvK8O}$E0Po#m z+uk`gXauW5)qnGlxXpLs&=muZBiaTAyX=1%0S<=&QscZriN}W=PLX##_M7eI?Pz<+ zYQD9GsOwk3eT4%1q$E{eoF2I@9lmGKGU1fWR)|Iio%{+|oHY^?Y7#VpA(Be$nGmQ_N1sy9xpyJ&|oI6i4cecVGSPebcS@&ids65Dr#Ur*>& zrzX=O1{>eV-N7u=B1r4MUgipLJ>WLRYZE;pe>`5>_k*Xm^Kn7{zFs=c@4EPtx5p8A zw)Uz9f+O5n+f4V^lJQgL`!a-2BnLG2$mcxoTALLNj|hSuqJe`e&`w=P>6AI@YPs6N zxo}(oC$|NN;fUNg{#^4bhytv@u$wYmrpgFQq^7cgKPHZk6Lzk7PbE`8Rsl$stN16P z@+PrFR)y^5K55-!+5%685$4UJ2BJVk(h+jM9@g$VcQz#@?__U}<{Sk&DlP%-gy?W< z=sivSq;FPYNi(O` z5HEqY$GWbRrxmQ0==N~A9?&<F_j%2W3=4!$TnmYqE#YOX1Y|PG#iTJ>ljw-iK%Akl7$?#+}$Gn99fBPc+Yz}>~*#0XuWBiWE5OPd$;50Ytdgro83^flX4-+Y)J71fIEKeNGw zBr+5C^rJ#(s5;=j4kY8oMcO_iy4z5veevksijBXuKP9?{CVhqByJ9OZ@!hT#4tsd? zSiD9(-hlU;-|vT!dv}pKl#p&Ohc+W;S;%6N`qBum-l>S3&LXyG2fN- z!Wp=`hPH}Jw2f_zP~Qcvl3(m(y`nk~<2*ILd~r@JqLiuP8U2o_$LxXfP}K&0W6OiR zd5A!qnle+Ayj3oTmME25%W#Q{?L*Bhf1`U2AzNZ)%p}s3eOrWD5i`TTdL`v%9hL9~ zCd`KRvxGBTAM2?~WxaFZ(lP#+s9gfv&H;Tx=X@-|`HI9PPPO@`SIEF+;q6nVA#Q*6 zvyoPPGsFYyfb^!i>7;5GApS}%{esoQap&je_B@xuwhXM5sp=~G;aE;yP#;2d<3em- zEbDnS>6%zcFz~vKu2fEDL84Mc65jMkZ@dD~wHy&ZnG&X_30WVh%jH{Lx3oYT0^^5h zRQiL+MKT-wqY3zn}!{jhC_iqCBb z(86TZ7Vq_QRVvZf!`i}rasWX_Dw(cgW`w0fH9{yp1m-A37}LV40F9wWY-%W0b`qm&KyLR?{4&M8XH zQ7kQFfez>`KIsi2eno5^dr`ye`UCS4#G-qHqp&JrdA6p}B_00Qm~++`Hv+9^!-ghp z3iFmqT>IPu$y@lxthe|R_m4`fAp64&y^C)va4I`k`Ox5UdHxx)N;s;kS|6*+)5O~O z*5|&5r4_eS*7qDN6+E6d0Rxk3ic_&tU%U_GgTgwuJpvIS%f?;41o|x_MzmbQg=NS+pUb)3e|5WOmax^E z3q;OUcWs&dx-X&Y?5A8xetLg7s*si#2cGQCBevsC;J#>5fT)4KU>rkIh9KCGac`j znyYHBzbxub^4wrJKu>n-SO6bmZxT5c6lh4@X+>!}#@TQzvcwOJf05oRd|=(n=XDHs zgngh~DqT{EH$-*tWVSl=*gZJ}e(NdwgV1Bvtq1)lY6Z$A4I#(-TMu`m(3{e#{68k^ zoQe%=0-<3)^KP8NoXQN-`P7yPnB%AbhdAcHF~{*40!OyL?M>T$vvcG}c}S1&;&wDl z1l!v^LB(u+ecg-qdSq&vAEWG7j9$2BZ_+^Vx7DAIy)&&zJG|*ie?sanp5{;Ah-2Vd zke6N(?!^dsG_W^oo4oG)F3HT}j-I&PFKE7b~BIqW-8;GhOz{dDRp|+9(9l9nS!v*0QBg}tzE+rvawlW zj+@M)%>;o3*)R7{PuxeSk7oDOg?&@IhQTM%2{&-;OSfDqUeH4<7(i9ZFH4v;NTero7PZXYg20xjZGYt&I3C>tzg2z?5 zVcq+FMRyRn`ykO33$Oh~sW%_ER$XhX#;9i;_uXz>l#pRKpHh8+{?K(u+vOHrn&V<8 z+F8f6o;Sg_S?=g*C|c773TYU{a>$`ENfH|z;jM+v6WTplc8r3iEeoP?7V;+&w>-6c zbwZ3!F&%IkZz-Zt(^RKaTf6O2*ZiFJ?c-2h1cWcTS)?-_G6$GqJTm&p;hCXjM;TNs zhISR#h3?gp6bs~fxj*_u&IeW=Y8g){wqNOFo3#-k>Uhi>p&~qBjp8$ifS(x?aX4w1 zNkR|hXqK5q@u9hUx&5lt(;!7PzM=4=kQI4>(;)BOqV`y(YqMN z)-tR}C)uciq1>GtxghGhn(`lGY`7|6l?s< z8AG;RHz)>p>JtGY^j|&o-yZmHw&(A=^YYtLz%fjo5wr>#ux3vTWxSW-G_3*E(p|rH z{9ELgZlNHaqHe22n>!}VwQZyyUO9;pBpGT&+O)Fs5QCdK3rZa;o0>u?sTw@`lUD5~ zJ8%$=Enz4{sUBIt{o+(^iojREeNSLl{3K|z7FkjGl|_RoPjTDeQ!Z-B>wdm_gRJ@t zB)yubCL67F3UXG`E!^0+!qXBDrU5_hRkNy4A=DVg_C|4hI;dn5yzg+_3+9c}w}s~B zs$*f$rIolL_5MKXx7%Byj_5WIP_(8ODOzM=dT8s?l=~APJ^2`OmPnj(;fC?12O!JN z;n26)ukq9UhuOP&{HR9aBdaexvVDS?FOQkcYOzIbiRl>5kmb!YFWfUXjqxMc>Sd}1*`%Ic&(IR!` zuhPZLKERkUdFw{9DEzi_5d||ygjt4x)QF@muamSO+X{7pD>l5!K^3VZHn?Y)QBpji z<3(Kr?%X?w#^V5R8Nc)~!mk@>$JIjQ#emBuDGD4M*TvrbhbqV4?4ac<%_$QIVV*)t zpe-e<1jU6eMV4 zRFk#wZ4A#@dd+FfsN)jxCDzMSTxUukcPt9ygtb^LAmwYp$8JIcR+W3c?!4k-iO`Mi z+z#QVdCuz0cD^IwLn}!(DXlgENx_-s@*}1C({C9yWh{hxa`1fX5p9*GM>Rt8O$1&K zl^{GLK@-_*QsPsWmkhMBCR6($l?>f&JJ7eNQs)_{2?}~SW4ABMUuiBReJibC+39*k zbRaF*xx^S^qN-U;*gCSmSE}`>E}r89Syqn%^Xx~#ZQcQe)Px-PMTFG>f=Kh|l$Ko? z9t~w11pDIHdx||a{AjG*@zMloK}yx)GD}!93mN_f!!HqEIu9|5e(p9|LMkKDblkqg z(@RV|+;ywuE-m(mDPF3drtgkcd<#V-r^zET`tHqxr1%Q4SEDTboA#)hp95l?Z8O=b zqu%s-%pD0=x*Z70a{7znAu#994?D4SB;?WFnvDr6-O$^3`3bCtj$?qbn06J?j(Z@; z9U0zI9|XOh-jd?-8D%Fzcl)LxhO_0d|5bTk1a(2a_De+ml-=yD*-ZBZ5-mGX@sgLX zBX%C%3^j1|DRkZo4x*%EOw1+aZ#Mvzb$6*#TRS8&8qG8fuC!EEwZl9#_8-U)NWdW(xa_c>KmK4iv35%M`X;Hxw`&nG`<1>Wl<`zS);_|gm{ zPc8QBrJHD}@kpdOs$c0UfVlBq@#-cyEaNmUavMntqq~{jSEgZGtUp_z&J)(AspexhlJ=MEW_7vHqzHn4hyk@Ub~nM z3{eV4Sdv}VlWug+3PC#_tzhrAOhjtP47r48bqW27*6rIIE8S4KF2gH@)lz%<+jlvb zhz<$sA*U+WPDZnM`9-79RYIZ%OKxYTs_e_QgYT}(z$MYNs z)QLRUD-*hLweYa`J$e6d zC-IF)s~Y92NZv?yyytQ}-2H9r+>taN2;QxLE?Ik#xjxqolDA91oN^ns8nyK{_Oeqp zVdIT{IOq|Pw$p4O37uXv$!i|LE?9)z?Wfh~KoMeB_fIsl3^NS*a@l0whQq1W*0UFn zChrO(sYz=F;**C0T&lijLp?2q2l4<#{b7o~c$lEufe7Bf7F{N$m}}f~j-%&0uCF=_sq|%JWF<+x_|d*FB&V(V1|*2lWQ09VYNw^4uvT~9c~J#!__R`zu~Z*` zgnZMb*W6l}FRP^IAa?$PcaF06TUzi&4swgGR?i}`VU2r?S7{fGK;fg1qM%i#Opti< zO)XVE7VSv|boKJLk+aGe*d9Zs=C`>FMz#2{ zx8RT=Q>z|%hR6q@5h2fBqIR7~NZUNEPSrIXB0$K=owG6hS{+7+Tidwlr9aHz4Ml*@ zHdVg;bX%ee|4aCo6=5kdg;ZWa0>!;p)&1CxiTUxW0U^J~EXtMD38m)rl`RKOA*x3l zr%W`R{PNU9rcPHPxbd6CoC}7Sc!pS-@8@bF~pgX!w z+)^Hvhi5~J)j;b0cwP@wQL;%I@gPteor@n04{&*szq`1BF^uum>mWzdI+n%DG)Eyl z`LH90pXe6hSj_ZT2Nlu#ytj4{r%Mt0ku-})*<2CJP@BQ9+iet#SZYqf7$(ZkKzkw^ z!3*||;I%Lc4)S^#VuBA{b1a6gcy>kt?Q)c0f{zOkUhpr9(e%IS#k7CE5j<7V#6cW& z3gd))uPdN14tNW40cX_z2O$zZN2ra#x2^4;Q~>f7HG#uV=scC0HQNrwSgDWwbgB4z zL{bsM(Xc?eqWCO1CSDIKx7BbeYuDb5hbAx4@3tX91cO09%;iXHOI1@bGi-nQ=g;Gq z*Cls)=Z-%E&bM$9lR=~*VyK)!u6(cpF1?Fge{KH44;!O@c=Swx@dMt1&WlQM=fQL9lDm5 zI31@V*NWa0b1qg>Hd1((KH)ZWX{WZdPTrv$o4j5hDz^tOIvw!jLbV=c={=H%y)$5dcsrGi z0;St5Su6krw-$f8edJBzo;!8)YQu}`{#=%lO?;Z^R_q5SIn51(oD_1;jF3Q;@uAS` zoR(0@wt$cs6~*E#JwgJM9?x#|0d9e<*yerlzJO#y+%2qVQ%Bsjfp%xt`z#EvMxt() znSs|6Nn2(c5;Fr_P;_E%Xl?h(nc1gjxiC%or)U~&td~zextFwHV96TXKqFtBkSp=r zus=_+#h$q*(9Sx$-wJCrqW+Lx`QW8d=}e}Bdx0H;R9wov45^)dbi~yF_^u3%M*-fc zgG8u<`t;$-Ai-F3rh6_|qxG~gy88&pzIGKNjL9XT3h0rG7RjQ>(g<LQ*hZ`x0v%PBxv4Og5ZKk(DyB9RvvL{a_M8VF<|yS<;mIB1OzS!!da=4ubzFBl z5%ym+iiy{;8_a{TnInSsXj-_h;c5?kbiS1?%+1O^Rn!!SD643u$=c&6ypP)-9mE>g zk6wS2m32P|v1cJ1*h0|qi{&m+SWO;XYG1uYw)AL`->N2FgHUMjG?dq>dfJKJI2?m6 zFJ^-8aSB3Up#`Z5yu|jjsZq`&B=d69rW&Oh#LVm{)F&l5QM6A=@*@d1-rB29`S2XS zK80Cs^Y&#hz5%M=6*xKeSJGen28Iu941`T=Z0+Rjb-y>!w{>@$(p#GmVa$_pr4l}+ zMr&*FY2WyTutV%N(mOcv)1X!STO;_xoF6H;nz1HtlK1#1Z;*ea-H_x;?Zf<1$b3BV zX+wY^F)C6@AevNH#c&j$EK+x{;!Ri!v@?uzd|)D*T%fd?)y1`QXTX22>g!); zpgfkFZ!uw4F2_8|nx{3EgUU6pKDwW>0@iHGl;yR>AR8ZK2Wj`_MNi|=(Fj$`^09B#oi@9 zixr=<3ENGq-GwvlnJ=}=AkmUtvJZ5UDKNy4%janbH14)GLy(gmJvJ@pikTUwhxU=I zyji@eVql0x?3hO7oki(aJ@>4ynmQP@TK4!uWI7u{!*amG%_( z!Q}eH!YSHeLo0;14XpbyC198<$fqT(ZiLeB@r#3xfIN!fgfUTumROd%07;zq4$0y5 zK6t0(>1Ewuh~DEn=(=LAycM!)*$bw z249wDj6EUbe%!!NMiVUNLH;%_^f;)KZbdF2D;2aDb+;CmPAEjeV@Z%Img>Hc6B7A= zBLTExY&!!JKNeBSp1D0<9S4ncndG4@4#f9MR~=z>6p+g zEB`IQRv(-&Y#cdynK$n7L?hv#TNESw^Ww^0*NGdS-n?sqp2OFwn#jC2aN3txUFT6( zf2r}3c?=0!Fqt`Ks;YP2Wpk_)Arg;nZi<0x`qN*+cq@83L{p}Xh~_2dFalgSZ6f7% zYy&w3s8v{`T3;p>J#LguzCKfiK*(jRc7quSN;357)~pCA9-`*Eno$CgK!@sk@?-(G zF0&FC=psLK>#7n-85%zdwG)OseN@2_=H*;bQsJ)RLP=$7C8=}IfUf9@gc1$vp@NE2 zS6aY+L!_Y~f_U}(yD#4GVz7=JAM9-U~Xmm}S zdQGqRh5;7X_DMIGu^xHs$~%x%R{h7a$7~I$arAby2z@kZTsg+TCtaEva_)VzEwxh^ z@9xA>F%Nj4IV%AJ71PBg{b^wS?g@Nh1{x*bWI4i%?zTK>y_vyst&EoseR>cCA&|GE z^n=F`6_pK)%zZk~=D!N~HH@_j?E*2yAGdm3=&*Ob365j3AbbZ6IQ^wVhZ#?v~v zcf#-(NmsEpoJx$_jhbS^PANwx2S3(^}Ro^ z_QY{Fn}V_j&9`of=XgB%^d^zQcA74l`AOgH58>QFD_B#Ndmm}sq{0)$o$o$ni@$^~CKkG|YRcis`ToM`BXE zf44;BcWL51$t8C`$VTA%jd)FF|Ie@bUb3_W$#tgrx-wC@wvESj##2>f9jZ*Y(r^pi zp>l~{dAN9E2BPnj?W;Jl7=Zt-)U6l0Z5C#gAIg?^j~ke1bOQn1e`D{w_(=V4H|t;K z89RV|AN|xqoxO@khMMF=9phLYqRE{``|rQ>V_PLHRLl=K9h zM20IOn+4>&cN5!Ke8*(hs;!JnRqpiuL#t(a4Clmlf6_v&Ou$xC&R(yp)XX+%39ZpitwCH zQr3BfX4%%e*Q&TM6vJ%pVv|c#W+&z_Ug>DAd)W}LvB;TMc9XjY*}MXTuMmq5@hmKw@Y~P_Adt};v2E_`s$lLw0r z9v2ajLFB`>)17zVmN+Z99y{`2QiQWOy)d8RvS}N*G=(y)dil9I_NC~uxoey_gw?k? zG(ScVB-V-~26}~%JU+5IVi))5YGd(fmqvEz-C|!8c8_iAZuB9yx&(iv!<^1oq^}yi z*+B`(B8XIjoKpbLI1@+to|xsWSIZbfBAE3QoEUAmhw|V6+&8ZTvm1#`WP6=7R6y|` z{5*d0p7#ef+ae{e#GRe=5Cm>w;Qp@V4xD=a|J8A4EM{Vov2s9_Wnf}pS)-=x9uSAn zQB|x`Q}=fjE*^ufoLvnE-hIkuwK6hwm3U+&16YSBQ`TAFY^_ zFmP`7*JL}4_Xp1mB?`cedG^ciSBB@=Qu3c<5I!j}VIg@1I*9Pk2(d84z(Du?IwOt+ zzMm<<{wW2pVEQRV-S-sVb=d!u@YauXaA#EOzb5=Hqknsu&u`Fw33T=Q9Ip+xyqN$}PZ9!#^dEr0XNG4h z)^9O^b6a*$bMxP$iOk}+HUs64x=^)Kpx>hLKYVC+7DmA8EaodB+`eT%EHl@&1eQTe{YFDX(Pi@!74MJ zQ>APE1=Y7o`>*PPm9;x3>CyQMl0S>w!IlVC2ke{-N&in|KX0kRV#2E9oMQ@^{0Z}W zgM5Sj_Qy{J2$Vb8sub6#@%Z_eL$SFjGJ=a{ok{~Hr@(ft%Q)3CmM=LEw6e?jnbFZKHp zz&cf(6VwD>Aowv#{EcZ6wj{7V3+F_w5f_LqJg$Fw-C*~D&yl;ME+Aign7%>(epeW_ z5U|^C=T!B;0_fj+^8Xl1VYigdVfo_!1pB?i_`?PhYyn^`&*x0uNf(%4r|3V`-G3Aa z)*5 \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" -APP_HOME="`pwd -P`" -cd "$SAVED" - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -# Don't use daemon or the cwd will be set to the install directory of the daemon and screw up any vert.x -# Path adjustments for file operations or sendFile -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100755 index 8a0b282..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/project/VertxScalaBuild.scala b/project/VertxScalaBuild.scala new file mode 100644 index 0000000..382982c --- /dev/null +++ b/project/VertxScalaBuild.scala @@ -0,0 +1,178 @@ +import java.nio.charset.StandardCharsets + +import sbt.Keys._ +import sbt._ + +object VertxScalaBuild extends Build { + + val baseSettings = Defaults.defaultSettings ++ Seq( + organization := "io.vertx", + name := "mysql-postgresql", + version := "0.3.0-SNAPSHOT", + scalaVersion := "2.10.4", + crossScalaVersions := Seq("2.10.4", "2.11.2"), + description := "Fully async MySQL / PostgreSQL module for Vert.x" + ) + + lazy val project = Project( + "project", + file("."), + settings = baseSettings ++ Seq( + copyModTask, + zipModTask, + libraryDependencies ++= Dependencies.compile, + libraryDependencies <+= scalaVersion("org.scala-lang" % "scala-compiler" % _), + // Fork JVM to allow Scala in-flight compilation tests to load the Scala interpreter + fork in Test := true, + // Vert.x tests are not designed to run in paralell + parallelExecution in Test := false, + // Adjust test system properties so that scripts are found + javaOptions in Test += "-Dvertx.test.resources=src/test/scripts", + // Adjust test modules directory + javaOptions in Test += "-Dvertx.mods=target/mods", + // Set the module name for tests + javaOptions in Test += s"-Dvertx.modulename=${organization.value}~${name.value}_${getMajor(scalaVersion.value)}~${version.value}", + resourceGenerators in Compile += Def.task { + val file = (resourceManaged in Compile).value / "langs.properties" + val contents = s"scala=io.vertx~lang-scala_${getMajor(scalaVersion.value)}~${Dependencies.Versions.vertxLangScalaVersion}:org.vertx.scala.platform.impl.ScalaVerticleFactory\n.scala=scala\n" + IO.write(file, contents) + Seq(file) + }.taskValue, + copyMod <<= copyMod dependsOn (compile in Compile), + (test in Test) <<= (test in Test) dependsOn copyMod, + (packageBin in Compile) <<= (packageBin in Compile) dependsOn copyMod, + // Publishing settings + publishMavenStyle := true, + pomIncludeRepository := { _ => false}, + publishTo <<= version { (v: String) => + val sonatype = "https://oss.sonatype.org/" + if (v.trim.endsWith("SNAPSHOT")) + Some("Sonatype Snapshots" at sonatype + "content/repositories/snapshots") + else + Some("Sonatype Releases" at sonatype + "service/local/staging/deploy/maven2") + }, + pomExtra := + 2013 + http://vertx.io + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + scm:git:git://github.com/vert-x/mod-mysql-postgresql.git + scm:git:ssh://git@github.com/vert-x/mod-mysql-postgresql.git + hhttps://github.com/vert-x/mod-mysql-postgresql + + + + Narigo + Joern Bernhardt + jb@campudus.com + + + Zwergal + Max Stemplinger + + + ) + ).settings(addArtifact(Artifact("lang-scala", "zip", "zip", "mod"), zipMod).settings: _*) + + val copyMod = TaskKey[Unit]("copy-mod", "Assemble the module into the local mods directory") + val zipMod = TaskKey[File]("zip-mod", "Package the module .zip file") + + lazy val copyModTask = copyMod := { + implicit val log = streams.value.log + val modOwner = organization.value + val modName = name.value + val modVersion = version.value + val scalaMajor = scalaVersion.value.substring(0, scalaVersion.value.lastIndexOf('.')) + val moduleName = s"$modOwner~${modName}_$scalaMajor~$modVersion" + log.info("Create module " + moduleName) + val moduleDir = target.value / "mods" / moduleName + createDirectory(moduleDir) + copyDirectory((classDirectory in Compile).value, moduleDir) + copyDirectory((resourceDirectory in Compile).value, moduleDir) + val libDir = moduleDir / "lib" + createDirectory(libDir) + // Get the runtime classpath to get all dependencies except provided ones + (managedClasspath in Runtime).value foreach { classpathEntry => + copyClasspathFile(classpathEntry, libDir) + } + } + + lazy val zipModTask = zipMod := { + implicit val log = streams.value.log + val modOwner = organization.value + val modName = name.value + val modVersion = version.value + val scalaMajor = scalaVersion.value.substring(0, scalaVersion.value.lastIndexOf('.')) + val moduleName = s"$modOwner~${modName}_$scalaMajor~$modVersion" + log.info("Create ZIP module " + moduleName) + val moduleDir = target.value / "mods" / moduleName + val zipFile = target.value / "zips" / s"$moduleName.zip" + IO.zip(allSubpaths(moduleDir), zipFile) + zipFile + } + + private def getMajor(version: String): String = version.substring(0, version.lastIndexOf('.')) + + private def createDirectory(dir: File)(implicit log: Logger): Unit = { + log.debug(s"Create directory $dir") + IO.createDirectory(dir) + } + + private def copyDirectory(source: File, target: File)(implicit log: Logger): Unit = { + log.debug(s"Copy $source to $target") + IO.copyDirectory(source, target, overwrite = true) + } + + private def copyClasspathFile(cpEntry: Attributed[File], libDir: File)(implicit log: Logger): Unit = { + val sourceFile = cpEntry.data + val targetFile = libDir / sourceFile.getName + log.debug(s"Copy $sourceFile to $targetFile") + IO.copyFile(sourceFile, targetFile) + } + +} + +object Dependencies { + + object Versions { + val vertxVersion = "2.1.2" + val testtoolsVersion = "2.0.3-final" + val hamcrestVersion = "1.3" + val junitInterfaceVersion = "0.10" + val vertxLangScalaVersion = "1.1.0-M1" + val asyncDriverVersion = "0.2.15" + } + + object Compile { + + import Dependencies.Versions._ + + val vertxCore = "io.vertx" % "vertx-core" % vertxVersion % "provided" + val vertxPlatform = "io.vertx" % "vertx-platform" % vertxVersion % "provided" + val vertxTesttools = "io.vertx" % "testtools" % testtoolsVersion % "provided" + val vertxLangScala = "io.vertx" %% "lang-scala" % vertxLangScalaVersion % "provided" + val postgreSqlDriver = "com.github.mauricio" %% "postgresql-async" % asyncDriverVersion % "provided" + val mySqlDriver = "com.github.mauricio" %% "mysql-async" % asyncDriverVersion % "provided" + } + + object Test { + + import Dependencies.Versions._ + + val hamcrest = "org.hamcrest" % "hamcrest-library" % hamcrestVersion % "test" + val junitInterface = "com.novocode" % "junit-interface" % junitInterfaceVersion % "test" + } + + import Dependencies.Compile._ + + val test = List(Test.hamcrest, Test.junitInterface) + + val compile = List(vertxCore, vertxPlatform, vertxTesttools, vertxLangScala, postgreSqlDriver, mySqlDriver) ::: test + +} diff --git a/src/main/resources/langs.properties b/src/main/resources/langs.properties deleted file mode 100644 index 41bb592..0000000 --- a/src/main/resources/langs.properties +++ /dev/null @@ -1,2 +0,0 @@ -scala=io.vertx~lang-scala~1.0.0:org.vertx.scala.platform.impl.ScalaVerticleFactory -.scala=scala From 3ccc05308179d4d474f696ce6b07354555df5e5b Mon Sep 17 00:00:00 2001 From: Max Stemplinger Date: Tue, 27 May 2014 09:50:25 +0200 Subject: [PATCH 02/14] new transaction functionality Signed-off-by: Max Stemplinger --- .../asyncsql/database/ConnectionHandler.scala | 173 +++++++++++++----- .../io/vertx/asyncsql/test/BaseSqlTests.scala | 45 ++++- .../vertx/asyncsql/test/SqlTestVerticle.scala | 2 + 3 files changed, 168 insertions(+), 52 deletions(-) diff --git a/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala b/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala index 3763f52..478a302 100644 --- a/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala +++ b/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala @@ -4,7 +4,7 @@ import scala.collection.JavaConverters.iterableAsScalaIterableConverter import scala.concurrent.Future import org.vertx.scala.core.json.{JsonElement, JsonArray, JsonObject, Json} import org.vertx.scala.core.logging.Logger -import com.github.mauricio.async.db.{ Configuration, Connection, QueryResult, RowData } +import com.github.mauricio.async.db.{Configuration, Connection, QueryResult, RowData} import com.github.mauricio.async.db.postgresql.exceptions.GenericDatabaseException import io.vertx.asyncsql.database.pool.AsyncConnectionPool import org.vertx.scala.mods.ScalaBusMod @@ -16,7 +16,9 @@ import org.vertx.scala.mods.ScalaBusMod.Receive trait ConnectionHandler extends ScalaBusMod { val verticle: Starter + def dbType: String + val config: Configuration val maxPoolSize: Int @@ -25,22 +27,83 @@ trait ConnectionHandler extends ScalaBusMod { lazy val logger: Logger = verticle.logger lazy val pool = AsyncConnectionPool(verticle, dbType, maxPoolSize, config) - def transactionStart: String = "START TRANSACTION;" + def transactionStart: String = "BEGIN;" + def transactionEnd: String = "COMMIT;" + def statementDelimiter: String = ";" import org.vertx.scala.core.eventbus._ - override def receive: Receive = (msg: Message[JsonObject]) => { - case "select" => select(msg.body) - case "insert" => insert(msg.body) - case "prepared" => AsyncReply(sendWithPool(prepared(msg.body))) - case "transaction" => transaction(msg.body) - case "raw" => AsyncReply(sendWithPool(rawCommand(msg.body.getString("command")))) + + private def receiver(withConnectionFn: (Connection => Future[SyncReply]) => Future[SyncReply]): Receive = (msg: Message[JsonObject]) => { + def sendAsyncWithPool(fn: Connection => Future[QueryResult]) = AsyncReply(sendWithPool(withConnectionFn)(fn)) + + { + case "select" => sendAsyncWithPool(rawCommand(selectCommand(msg.body))) + case "insert" => sendAsyncWithPool(rawCommand(insertCommand(msg.body))) + case "prepared" => sendAsyncWithPool(prepared(msg.body)) + case "raw" => sendAsyncWithPool(rawCommand(msg.body.getString("command"))) + case "transaction" => transaction(withConnectionFn)(msg.body) + } + } + + override def receive: Receive = { msg: Message[JsonObject] => + receiver(pool.withConnection)(msg).orElse { + case "start" => startTransaction(msg) + } } + + //------------------ + //New transaction stuff + //TODO reformat when finished + + protected def endTransaction() = { + /* FIXME */ + logger.info("ending transaction!") + } + + protected def startTransaction(msg: Message[JsonObject]) = AsyncReply { + pool.withConnection({ c => + def withConnection[T](fn: Connection => Future[T]): Future[T] = fn(c) + + c.sendQuery(transactionStart) map { _ => + Ok(Json.obj(), Some(ReceiverWithTimeout(transactionQueryReply(withConnection), 500L /* FIXME from config file! */ , endTransaction))) + } + }) + } + + private def transactionQueryReply(withConnection: (Connection => Future[SyncReply]) => Future[SyncReply]): Receive = { msg => + val action = msg.body.getString("action") + + receiver(withConnection)(msg).orElse{ + case "start" => Error("cannot send 'start' action when inside of transaction!") + case "end" => + logger.info("got action end!") + // AsyncReply{withConnection} + Ok() + } + + val opt = pf.lift(action).map({ + case Ok(v, None) => Ok(v, Some(ReceiverWithTimeout(transactionQueryReply(withConnection), 500L /* FIXME from config file! */ , endTransaction))) + case x => x + }: Function[BusModReceiveEnd, BusModReceiveEnd]) + + opt.getOrElse { + case "start" => Error("cannot send 'start' action when inside of transaction!") + case "end" => + logger.info("got action end!") + // AsyncReply{withConnection} + Ok() + }: PartialFunction[String, BusModReceiveEnd] + } + + //------------------ + def close() = pool.close() protected def escapeField(str: String): String = "\"" + str.replace("\"", "\"\"") + "\"" + protected def escapeString(str: String): String = "'" + str.replace("'", "''") + "'" protected def escapeValue(v: Any): String = v match { @@ -58,10 +121,6 @@ trait ConnectionHandler extends ScalaBusMod { } } - protected def select(json: JsonObject): AsyncReply = AsyncReply { - sendWithPool(rawCommand(selectCommand(json))) - } - protected def insertCommand(json: JsonObject): String = { val table = json.getString("table") val fields = json.getArray("fields").asScala @@ -79,51 +138,63 @@ trait ConnectionHandler extends ScalaBusMod { .append(listOfLines.mkString(",")).toString } - protected def insert(json: JsonObject): AsyncReply = AsyncReply { - sendWithPool(rawCommand(insertCommand(json))) + sealed trait CommandType { + val query: Connection => Future[QueryResult] + } + + case class Raw(stmt: String) extends CommandType { + val query = rawCommand(stmt) + } + + case class Prepared(json: JsonObject) extends CommandType { + val query = prepared(json) } - sealed trait CommandType { val query: Connection => Future[QueryResult] } - case class Raw(stmt: String) extends CommandType { val query = rawCommand(stmt) } - case class Prepared(json: JsonObject) extends CommandType { val query = prepared(json) } - - protected def transaction(json: JsonObject): AsyncReply = AsyncReply(pool.withConnection({ c: Connection => - logger.info("TRANSACTION-JSON: " + json.encodePrettily()) - - Option(json.getArray("statements")) match { - case Some(statements) => c.inTransaction { conn: Connection => - val futures = statements.asScala.map { - case js: JsonObject => - js.getString("action") match { - case "select" => Raw(selectCommand(js)) - case "insert" => Raw(insertCommand(js)) - case "prepared" => Prepared(js) - case "raw" => Raw(js.getString("command")) + protected def transaction(withConnection: (Connection => Future[SyncReply]) => Future[SyncReply])(json: JsonObject): AsyncReply = AsyncReply(withConnection({ + c: Connection => + logger.info("TRANSACTION-JSON: " + json.encodePrettily()) + + Option(json.getArray("statements")) match { + case Some(statements) => c.inTransaction { + conn: Connection => + val futures = statements.asScala.map { + case js: JsonObject => + js.getString("action") match { + case "select" => Raw(selectCommand(js)) + case "insert" => Raw(insertCommand(js)) + case "prepared" => Prepared(js) + case "raw" => Raw(js.getString("command")) + } + case _ => throw new IllegalArgumentException("'statements' needs JsonObjects!") } - case _ => throw new IllegalArgumentException("'statements' needs JsonObjects!") + val f = futures.foldLeft(Future[Any]()) { + case (fut, cmd) => fut flatMap (_ => cmd.query(conn)) + } + f map (_ => Ok(Json.obj())) } - val f = futures.foldLeft(Future[Any]()) { case (fut, cmd) => fut flatMap (_ => cmd.query(conn)) } - f map (_ => Ok(Json.obj())) + case None => throw new IllegalArgumentException("No 'statements' field in request!") } - case None => throw new IllegalArgumentException("No 'statements' field in request!") - } })) - - protected def sendWithPool(fn: Connection => Future[QueryResult]): Future[SyncReply] = pool.withConnection({ c: Connection => - fn(c) map buildResults recover { - case x: GenericDatabaseException => - Error(x.errorMessage.message) - case x => - Error(x.getMessage()) - } + + protected def sendWithPool(withConnection: (Connection => Future[SyncReply]) => Future[SyncReply])(fn: Connection => Future[QueryResult]): Future[SyncReply] = withConnection({ + c: Connection => + fn(c) map buildResults recover { + case x: GenericDatabaseException => + Error(x.errorMessage.message) + case x => + Error(x.getMessage()) + } }) - protected def prepared(json: JsonObject): Connection => Future[QueryResult] = { c: Connection => - c.sendPreparedStatement(json.getString("statement"), json.getArray("values").toArray()) + protected def prepared(json: JsonObject): Connection => Future[QueryResult] = { + c: Connection => + c.sendPreparedStatement(json.getString("statement"), json.getArray("values").toArray()) } - protected def rawCommand(command: String): Connection => Future[QueryResult] = { c: Connection => c.sendQuery(command) } + protected def rawCommand(command: String): Connection => Future[QueryResult] = { + c: Connection => c.sendQuery(command) + } private def buildResults(qr: QueryResult): SyncReply = { val result = new JsonObject() @@ -132,12 +203,14 @@ trait ConnectionHandler extends ScalaBusMod { qr.rows match { case Some(resultSet) => - val fields = (new JsonArray() /: resultSet.columnNames) { (arr, name) => - arr.addString(name) + val fields = (new JsonArray() /: resultSet.columnNames) { + (arr, name) => + arr.addString(name) } - val rows = (new JsonArray() /: resultSet) { (arr, rowData) => - arr.add(rowDataToJsonArray(rowData)) + val rows = (new JsonArray() /: resultSet) { + (arr, rowData) => + arr.add(rowDataToJsonArray(rowData)) } result.putArray("fields", fields) diff --git a/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala b/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala index 98fc3e7..cec02ad 100644 --- a/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala +++ b/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala @@ -1,9 +1,12 @@ package io.vertx.asyncsql.test -import scala.concurrent.Future -import org.vertx.scala.core.json.{Json, JsonArray} +import scala.concurrent.{Future, Promise} +import org.vertx.scala.core.json.{JsonObject, Json, JsonArray} import org.vertx.testtools.VertxAssert._ import org.junit.Test +import scala.util.{Success, Failure, Try} +import org.vertx.scala.core.eventbus.Message +import org.vertx.scala.core.FunctionConverters._ trait BaseSqlTests { this: SqlTestVerticle => @@ -281,4 +284,42 @@ trait BaseSqlTests { } } + protected def expectOkMsg(q: JsonObject) = { + val p = Promise[Message[JsonObject]]() + vertx.eventBus.sendWithTimeout(address, q, 500L, { + case Success(reply) => + logger.info("got a reply: " + reply.body().encode()) + p.success(reply) + case Failure(ex) => + logger.error("timeout", ex) + fail(s"got a timeout when expected an ok message ${ex.toString}") + }: Try[Message[JsonObject]] => Unit) + p.future + } + + @Test + def startAndEndTransaction(): Unit = { + expectOkMsg(Json.obj("action" -> "start")) map { msg => + msg.replyWithTimeout(raw("SELECT 15"), 500L, { + case Success(reply) => Option(reply.body().getArray("results")) map { arr => + assertEquals("ok", reply.body().getString("status")) + assertEquals(1, arr.size()) + assertEquals(15, arr + .get[JsonArray](0) + .get[Int](0)) + reply.replyWithTimeout(Json.obj("action" -> "end"), 500L, { + case Success(endReply) => + assertEquals("ok", endReply.body().getString("status")) + testComplete() + case Failure(ex) => + logger.error("timeout", ex) + fail(s"got a timeout when expected end reply ${ex.toString}") + }: Try[Message[JsonObject]] => Unit) + } + case Failure(ex) => + logger.error("timeout", ex) + fail(s"got a timeout when expected reply ${ex.toString}") + }: Try[Message[JsonObject]] => Unit) + } + } } \ No newline at end of file diff --git a/src/test/scala/io/vertx/asyncsql/test/SqlTestVerticle.scala b/src/test/scala/io/vertx/asyncsql/test/SqlTestVerticle.scala index 29ef238..51f70b3 100644 --- a/src/test/scala/io/vertx/asyncsql/test/SqlTestVerticle.scala +++ b/src/test/scala/io/vertx/asyncsql/test/SqlTestVerticle.scala @@ -56,6 +56,8 @@ abstract class SqlTestVerticle extends TestVerticle with BaseVertxIntegrationTes protected def transaction(statements: JsonObject*) = Json.obj("action" -> "transaction", "statements" -> Json.arr(statements: _*)) + protected def newTransaction = Json.obj("action" -> "start") + protected def createTable(tableName: String) = expectOk(raw(createTableStatement(tableName))) map { reply => assertEquals(0, reply.getInteger("rows")) reply From 642e7dec2476538c290debf0961a1801fd1c78ca Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Tue, 27 May 2014 17:38:18 +0200 Subject: [PATCH 03/14] fixed transaction Signed-off-by: Joern Bernhardt --- .../asyncsql/database/ConnectionHandler.scala | 82 +++++++++++-------- .../io/vertx/asyncsql/test/BaseSqlTests.scala | 8 +- 2 files changed, 51 insertions(+), 39 deletions(-) diff --git a/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala b/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala index 478a302..9c865f6 100644 --- a/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala +++ b/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala @@ -1,7 +1,7 @@ package io.vertx.asyncsql.database import scala.collection.JavaConverters.iterableAsScalaIterableConverter -import scala.concurrent.Future +import scala.concurrent.{Promise, Future} import org.vertx.scala.core.json.{JsonElement, JsonArray, JsonObject, Json} import org.vertx.scala.core.logging.Logger import com.github.mauricio.async.db.{Configuration, Connection, QueryResult, RowData} @@ -31,71 +31,81 @@ trait ConnectionHandler extends ScalaBusMod { def transactionEnd: String = "COMMIT;" + def transactionRollback: String = "ROLLBACK;" + def statementDelimiter: String = ";" + private def timeout = 500L /* FIXME from config file! */ + import org.vertx.scala.core.eventbus._ private def receiver(withConnectionFn: (Connection => Future[SyncReply]) => Future[SyncReply]): Receive = (msg: Message[JsonObject]) => { def sendAsyncWithPool(fn: Connection => Future[QueryResult]) = AsyncReply(sendWithPool(withConnectionFn)(fn)) { - case "select" => sendAsyncWithPool(rawCommand(selectCommand(msg.body))) - case "insert" => sendAsyncWithPool(rawCommand(insertCommand(msg.body))) - case "prepared" => sendAsyncWithPool(prepared(msg.body)) - case "raw" => sendAsyncWithPool(rawCommand(msg.body.getString("command"))) - case "transaction" => transaction(withConnectionFn)(msg.body) + case "select" => sendAsyncWithPool(rawCommand(selectCommand(msg.body()))) + case "insert" => sendAsyncWithPool(rawCommand(insertCommand(msg.body()))) + case "prepared" => sendAsyncWithPool(prepared(msg.body())) + case "raw" => sendAsyncWithPool(rawCommand(msg.body().getString("command"))) } } - override def receive: Receive = { msg: Message[JsonObject] => + private def regularReceive: Receive = { msg: Message[JsonObject] => receiver(pool.withConnection)(msg).orElse { case "start" => startTransaction(msg) + case "transaction" => transaction(pool.withConnection)(msg.body()) } } + override def receive: Receive = regularReceive + //------------------ //New transaction stuff - //TODO reformat when finished + private def mapRepliesToTransactionReceive(c: Connection): BusModReply => BusModReply = { + case AsyncReply(receiveEndFuture) => AsyncReply(receiveEndFuture.map(mapRepliesToTransactionReceive(c))) + case Ok(v, None) => Ok(v, Some(ReceiverWithTimeout(inTransactionReceive(c), timeout, () => failTransaction(c)))) + case x => x + } - protected def endTransaction() = { - /* FIXME */ - logger.info("ending transaction!") + private def inTransactionReceive(c: Connection): Receive = { msg: Message[JsonObject] => + def withConnection[T](fn: Connection => Future[T]): Future[T] = fn(c) + + receiver(withConnection)(msg).andThen({ + case x: BusModReply => mapRepliesToTransactionReceive(c)(x) + case x => x + }).orElse { + case "end" => endTransaction(c) + } } protected def startTransaction(msg: Message[JsonObject]) = AsyncReply { - pool.withConnection({ c => - def withConnection[T](fn: Connection => Future[T]): Future[T] = fn(c) - + pool.take().flatMap { c => c.sendQuery(transactionStart) map { _ => - Ok(Json.obj(), Some(ReceiverWithTimeout(transactionQueryReply(withConnection), 500L /* FIXME from config file! */ , endTransaction))) + Ok(Json.obj(), Some(ReceiverWithTimeout(inTransactionReceive(c), timeout, () => failTransaction(c)))) } - }) + } } - private def transactionQueryReply(withConnection: (Connection => Future[SyncReply]) => Future[SyncReply]): Receive = { msg => - val action = msg.body.getString("action") + protected def failTransaction(c: Connection) = { + logger.info("NO REPLY BACK -> FAIL TRANSACTION!") + c.sendQuery(transactionRollback).andThen({ + case _ => pool.giveBack(c) + }) + } - receiver(withConnection)(msg).orElse{ - case "start" => Error("cannot send 'start' action when inside of transaction!") - case "end" => - logger.info("got action end!") - // AsyncReply{withConnection} + protected def endTransaction(c: Connection) = { + logger.info("ending transaction!") + AsyncReply { + (for { + qr <- c.sendQuery(transactionEnd) + _ <- pool.giveBack(c) + } yield { Ok() + }) recover { + case ex => Error("Could not give back connection to pool", "CONNECTION_POOL_EXCEPTION", Json.obj("exception" -> ex)) + } } - - val opt = pf.lift(action).map({ - case Ok(v, None) => Ok(v, Some(ReceiverWithTimeout(transactionQueryReply(withConnection), 500L /* FIXME from config file! */ , endTransaction))) - case x => x - }: Function[BusModReceiveEnd, BusModReceiveEnd]) - - opt.getOrElse { - case "start" => Error("cannot send 'start' action when inside of transaction!") - case "end" => - logger.info("got action end!") - // AsyncReply{withConnection} - Ok() - }: PartialFunction[String, BusModReceiveEnd] } //------------------ diff --git a/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala b/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala index cec02ad..e63e429 100644 --- a/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala +++ b/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala @@ -300,24 +300,26 @@ trait BaseSqlTests { @Test def startAndEndTransaction(): Unit = { expectOkMsg(Json.obj("action" -> "start")) map { msg => + logger.info("Should be in transaction!") msg.replyWithTimeout(raw("SELECT 15"), 500L, { case Success(reply) => Option(reply.body().getArray("results")) map { arr => assertEquals("ok", reply.body().getString("status")) assertEquals(1, arr.size()) assertEquals(15, arr .get[JsonArray](0) - .get[Int](0)) + .get[Number](0).longValue()) + logger.info("First select DONE!") reply.replyWithTimeout(Json.obj("action" -> "end"), 500L, { case Success(endReply) => assertEquals("ok", endReply.body().getString("status")) testComplete() case Failure(ex) => - logger.error("timeout", ex) + logger.error("timeout when waiting for final reply (end transaction)", ex) fail(s"got a timeout when expected end reply ${ex.toString}") }: Try[Message[JsonObject]] => Unit) } case Failure(ex) => - logger.error("timeout", ex) + logger.error("timeout when waiting for SELECT reply", ex) fail(s"got a timeout when expected reply ${ex.toString}") }: Try[Message[JsonObject]] => Unit) } From 588d4f3df54ab8e33958454745fa0c192fc30396 Mon Sep 17 00:00:00 2001 From: Max Stemplinger Date: Tue, 27 May 2014 23:11:35 +0200 Subject: [PATCH 04/14] rollback transaction and new tests Signed-off-by: Max Stemplinger --- .../asyncsql/database/ConnectionHandler.scala | 21 +++++- .../io/vertx/asyncsql/test/BaseSqlTests.scala | 68 ++++++++++++++++++- 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala b/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala index 9c865f6..2b8dc73 100644 --- a/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala +++ b/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala @@ -52,7 +52,7 @@ trait ConnectionHandler extends ScalaBusMod { private def regularReceive: Receive = { msg: Message[JsonObject] => receiver(pool.withConnection)(msg).orElse { - case "start" => startTransaction(msg) + case "begin" => startTransaction(msg) case "transaction" => transaction(pool.withConnection)(msg.body()) } } @@ -75,7 +75,8 @@ trait ConnectionHandler extends ScalaBusMod { case x: BusModReply => mapRepliesToTransactionReceive(c)(x) case x => x }).orElse { - case "end" => endTransaction(c) + case "commit" => commitTransaction(c) + case "rollback" => rollbackTransaction(c) } } @@ -94,7 +95,7 @@ trait ConnectionHandler extends ScalaBusMod { }) } - protected def endTransaction(c: Connection) = { + protected def commitTransaction(c: Connection) = { logger.info("ending transaction!") AsyncReply { (for { @@ -108,6 +109,20 @@ trait ConnectionHandler extends ScalaBusMod { } } + protected def rollbackTransaction(c: Connection) = { + logger.info("rolling back transaction!") + AsyncReply { + (for { + qr <- c.sendQuery(transactionRollback) + _ <- pool.giveBack(c) + } yield { + Ok() + }) recover { + case ex => Error("Could not give back connection to pool", "CONNECTION_POOL_EXCEPTION", Json.obj("exception" -> ex)) + } + } + } + //------------------ def close() = pool.close() diff --git a/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala b/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala index e63e429..bf4bb94 100644 --- a/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala +++ b/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala @@ -299,7 +299,7 @@ trait BaseSqlTests { @Test def startAndEndTransaction(): Unit = { - expectOkMsg(Json.obj("action" -> "start")) map { msg => + expectOkMsg(Json.obj("action" -> "begin")) map { msg => logger.info("Should be in transaction!") msg.replyWithTimeout(raw("SELECT 15"), 500L, { case Success(reply) => Option(reply.body().getArray("results")) map { arr => @@ -309,7 +309,7 @@ trait BaseSqlTests { .get[JsonArray](0) .get[Number](0).longValue()) logger.info("First select DONE!") - reply.replyWithTimeout(Json.obj("action" -> "end"), 500L, { + reply.replyWithTimeout(Json.obj("action" -> "commit"), 500L, { case Success(endReply) => assertEquals("ok", endReply.body().getString("status")) testComplete() @@ -324,4 +324,68 @@ trait BaseSqlTests { }: Try[Message[JsonObject]] => Unit) } } + + private def replyWithTimeout(msg: Message[JsonObject], value: JsonObject): Future[Message[JsonObject]] = { + val p = Promise[Message[JsonObject]] + msg.replyWithTimeout(value, 500L, { + case Success(r) => p.success(r) + case Failure(x) => p.failure(x) + }: Try[Message[JsonObject]] => Unit) + p.future + } + + @Test + def updateInTransaction(): Unit = typeTestInsert { + (for{ + beginReply <- expectOkMsg(Json.obj("action" -> "begin")) + updateReply <- replyWithTimeout(beginReply, raw("UPDATE some_test set email = 'updated@test.com' WHERE name = 'Mr. Test'")) + commitReply <- replyWithTimeout(updateReply, Json.obj("action" -> "commit")) + checkReply <- expectOk(raw("SELECT email FROM some_test WHERE name = 'Mr. Test'")) + } yield { + val results = checkReply.getArray("results") + val mrTest = results.get[JsonArray](0) + assertEquals("updated@test.com", mrTest.get[String](0)) + logger.info("all tests completed") + }) recover { + case ex => + logger.error("timeout when waiting for reply", ex) + fail(s"got a timeout when expected reply ${ex.toString}") + } + } + + @Test + def rollBackTransaction(): Unit = typeTestInsert { + val fieldsArray = Json.arr("name", "email", "is_male", "age", "money", "wedding_date") + (for { + msg <- expectOkMsg(Json.obj("action" -> "begin")) + reply <- replyWithTimeout(msg, raw("UPDATE some_test set email = 'shouldRollback@test.com' WHERE name = 'Mr. Test'")) + checkUpdateReply <- { + assertEquals("ok", reply.body().getString("status")) + replyWithTimeout(reply, raw("SELECT email FROM some_test WHERE name = 'Mr. Test'")) + } + endReply <- { + assertEquals("ok", checkUpdateReply.body().getString("status")) + val results = checkUpdateReply.body().getArray("results") + val mrTest = results.get[JsonArray](0) + assertEquals("shouldRollback@test.com", mrTest.get[String](0)) + + logger.info("Update done, now do rollback") + replyWithTimeout(checkUpdateReply, Json.obj("action" -> "rollback")) + } + checkReply <- { + logger.info("rollback done, now check if everything is like before the update") + assertEquals("ok", endReply.body().getString("status")) + expectOk(select("some_test", fieldsArray)) + } + } yield { + val results = checkReply.getArray("results") + val mrTest = results.get[JsonArray](0) + checkMrTest(mrTest) + logger.info("all tests completed") + }) recover { + case ex => + logger.error("timeout when waiting for reply", ex) + fail(s"got a timeout when expected reply ${ex.toString}") + } + } } \ No newline at end of file From bfe810ec681c19c4d40de5723c432587f176ec5d Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Wed, 28 May 2014 03:22:21 +0200 Subject: [PATCH 05/14] new transactions Signed-off-by: Joern Bernhardt --- .../asyncsql/database/ConnectionHandler.scala | 58 +++++++------------ 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala b/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala index 2b8dc73..7a759b8 100644 --- a/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala +++ b/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala @@ -13,6 +13,7 @@ import org.vertx.scala.core.Vertx import org.vertx.scala.platform.Container import io.vertx.asyncsql.Starter import org.vertx.scala.mods.ScalaBusMod.Receive +import scala.util.{Failure, Success} trait ConnectionHandler extends ScalaBusMod { val verticle: Starter @@ -27,9 +28,9 @@ trait ConnectionHandler extends ScalaBusMod { lazy val logger: Logger = verticle.logger lazy val pool = AsyncConnectionPool(verticle, dbType, maxPoolSize, config) - def transactionStart: String = "BEGIN;" + def transactionBegin: String = "BEGIN;" - def transactionEnd: String = "COMMIT;" + def transactionCommit: String = "COMMIT;" def transactionRollback: String = "ROLLBACK;" @@ -52,16 +53,13 @@ trait ConnectionHandler extends ScalaBusMod { private def regularReceive: Receive = { msg: Message[JsonObject] => receiver(pool.withConnection)(msg).orElse { - case "begin" => startTransaction(msg) + case "begin" => beginTransaction(msg) case "transaction" => transaction(pool.withConnection)(msg.body()) } } override def receive: Receive = regularReceive - - //------------------ - //New transaction stuff private def mapRepliesToTransactionReceive(c: Connection): BusModReply => BusModReply = { case AsyncReply(receiveEndFuture) => AsyncReply(receiveEndFuture.map(mapRepliesToTransactionReceive(c))) case Ok(v, None) => Ok(v, Some(ReceiverWithTimeout(inTransactionReceive(c), timeout, () => failTransaction(c)))) @@ -75,55 +73,41 @@ trait ConnectionHandler extends ScalaBusMod { case x: BusModReply => mapRepliesToTransactionReceive(c)(x) case x => x }).orElse { - case "commit" => commitTransaction(c) case "rollback" => rollbackTransaction(c) + case "commit" => commitTransaction(c) } } - protected def startTransaction(msg: Message[JsonObject]) = AsyncReply { + protected def beginTransaction(msg: Message[JsonObject]) = AsyncReply { pool.take().flatMap { c => - c.sendQuery(transactionStart) map { _ => + c.sendQuery(transactionBegin) map { _ => Ok(Json.obj(), Some(ReceiverWithTimeout(inTransactionReceive(c), timeout, () => failTransaction(c)))) } } } - protected def failTransaction(c: Connection) = { - logger.info("NO REPLY BACK -> FAIL TRANSACTION!") - c.sendQuery(transactionRollback).andThen({ - case _ => pool.giveBack(c) - }) + private def endQuery(c: Connection, s: String) = c.sendQuery(s) andThen { + case _ => pool.giveBack(c) } - protected def commitTransaction(c: Connection) = { - logger.info("ending transaction!") - AsyncReply { - (for { - qr <- c.sendQuery(transactionEnd) - _ <- pool.giveBack(c) - } yield { - Ok() - }) recover { - case ex => Error("Could not give back connection to pool", "CONNECTION_POOL_EXCEPTION", Json.obj("exception" -> ex)) - } - } + protected def failTransaction(c: Connection) = { + logger.warn("Rolling back transaction, due to no reply") + endQuery(c, transactionRollback) } - protected def rollbackTransaction(c: Connection) = { + protected def rollbackTransaction(c: Connection) = AsyncReply { logger.info("rolling back transaction!") - AsyncReply { - (for { - qr <- c.sendQuery(transactionRollback) - _ <- pool.giveBack(c) - } yield { - Ok() - }) recover { - case ex => Error("Could not give back connection to pool", "CONNECTION_POOL_EXCEPTION", Json.obj("exception" -> ex)) - } + endQuery(c, transactionRollback).map(_ => Ok()).recover { + case ex => Error("Could not rollback transaction", "ROLLBACK_FAILED", Json.obj("exception" -> ex)) } } - //------------------ + protected def commitTransaction(c: Connection) = AsyncReply { + logger.info("ending transaction with commit!") + endQuery(c, transactionCommit).map(_ => Ok()).recover { + case ex => Error("Could not commit transaction", "COMMIT_FAILED", Json.obj("exception" -> ex)) + } + } def close() = pool.close() From 9ad2f00d413e6ff31a7fb3ea4cd68b45f11296e2 Mon Sep 17 00:00:00 2001 From: Max Stemplinger Date: Wed, 28 May 2014 14:55:22 +0200 Subject: [PATCH 06/14] added transaction timeout to config. updated readme. Signed-off-by: Max Stemplinger --- README.md | 135 ++++++++++++++---- .../scala/io/vertx/asyncsql/Starter.scala | 5 +- .../asyncsql/database/ConnectionHandler.scala | 7 +- .../database/MySqlConnectionHandler.scala | 2 +- .../PostgreSqlConnectionHandler.scala | 2 +- 5 files changed, 113 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 5624f30..9bf4932 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This Vert.x module uses the https://github.com/mauricio/postgresql-async drivers ## Requirements -* Vert.x 2.1+ (with Scala language module v1.0) +* Vert.x 2.1+ (with Scala language module v1.0.1+) * A working PostgreSQL or MySQL server * For testing PostgreSQL: A 'testdb' database on a local PostgreSQL install and a user called 'vertx' * For testing MySQL: A 'testdb' database on a local MySQL install and a user called 'root' @@ -102,10 +102,111 @@ Creates a prepared statement and lets you fill the `?` with values. { "action" : "prepared", "statement" : "SELECT * FROM some_test WHERE name=? AND money > ?", - "values" : ["John", 1000] + "values" : ["Mr. Test", 15] } + +### raw - Raw commands + +Use this action to send arbitrary commands to the database. You should be able to submit any query or insertion with this command. + +Here is an example for creating a table in PostgreSQL: + + { + "action" : "raw", + "command" : "CREATE TABLE some_test ( + id SERIAL, + name VARCHAR(255), + email VARCHAR(255), + is_male BOOLEAN, + age INT, + money FLOAT, + wedding_date DATE + );" + } + +And if you want to drop it again, you can send the following: + + { + "action" : "raw", + "command" : "DROP TABLE some_test;" + } + +### Transactions + +These commands let you begin a transaction and send an arbitrary number of statements within the started transaction. You can then commit or rollback the transaction. +Nested transactions are not possible until now! + +Remember to reply to the messages after you send the `begin` command. Look in the docs how this works (e.g. for Java: [http://vertx.io/core_manual_java.html#replying-to-messages](http://vertx.io/core_manual_java.html#replying-to-messages)). +With replying to the messages, the module is able to send all statements within the same transaction. If you don't reply within the `timeoutTransaction` interval, the transaction will automatically fail and rollback. + +#### transaction begin + +This command starts a transaction. You get an Ok message back to which you can then reply with more statements. + + { + "action" : "begin" + } + +#### transaction commit + +To commit a transaction you have to send the `commit` command. + + { + "action" : "commit" + } + +#### transaction rollback + +To rollback a transaction you have to send the `rollback` command. + + { + "action" : "rollback" + } + +#### Example for a transaction + +Here is a small example on how a transaction works. + + { + "action" : "begin" + } + +This will start the transaction. You get this response: -### transaction + { + "status" : "ok" + } + +You can then reply to this message with the commands `select`, `prepared`, `insert` and `raw`. +A possible reply could be this: + + { + "action" : "raw", + "command" : "UPDATE some_test SET email = 'foo@bar.com' WHERE id = 1" + } + +You get a reply back depending on the statement you sent. In this case the answer would be: + + { + "status" : "ok", + "rows" : 1, + "message" : "UPDATE 1" + } + +If you want to make more statements you just have to reply to this message again with the next statement. +When you have done all statements you can `commit` or `rollback` the transaction. + + { + "action" : "commit" + } + +If everything worked, the last answer will be: + + { + "status" : "ok" + } + +#### old transaction command (deprecated, use the new transaction mechanism with begin and commit) Takes several statements and wraps them into a single transaction for the server to process. Use `statement : [...actions...]` to create such a transaction. Only `select`, `insert` and `raw` commands are allowed right now. @@ -129,33 +230,7 @@ Takes several statements and wraps them into a single transaction for the server } ] } - -### raw - Raw commands - -Use this action to send arbitrary commands to the database. You should be able to do submit any query or insertion with this command. - -Here is an example for creating a table in PostgreSQL: - - { - "action" : "raw", - "command" : "CREATE TABLE some_test ( - id SERIAL, - name VARCHAR(255), - email VARCHAR(255), - is_male BOOLEAN, - age INT, - money FLOAT, - wedding_date DATE - );" - } - -And if you want to drop it again, you can send the following: - - { - "action" : "raw", - "command" : "DROP TABLE some_test;" - } - + ## Planned actions You can always use `raw` to do anything on the database. If the statement is a query, it will return its results just like a `select`. diff --git a/src/main/scala/io/vertx/asyncsql/Starter.scala b/src/main/scala/io/vertx/asyncsql/Starter.scala index 45edb5c..17780a6 100644 --- a/src/main/scala/io/vertx/asyncsql/Starter.scala +++ b/src/main/scala/io/vertx/asyncsql/Starter.scala @@ -25,10 +25,11 @@ class Starter extends Verticle { val dbType = getDatabaseType(config) val configuration = getConfiguration(config, dbType) val maxPoolSize = config.getInteger("maxPoolSize", 10) + val transactionTimeout = config.getLong("transactionTimeout", 500L) handler = dbType match { - case "postgresql" => new PostgreSqlConnectionHandler(this, configuration, maxPoolSize) - case "mysql" => new MySqlConnectionHandler(this, configuration, maxPoolSize) + case "postgresql" => new PostgreSqlConnectionHandler(this, configuration, maxPoolSize, transactionTimeout) + case "mysql" => new MySqlConnectionHandler(this, configuration, maxPoolSize, transactionTimeout) } vertx.eventBus.registerHandler(address, handler) diff --git a/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala b/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala index 7a759b8..c369e32 100644 --- a/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala +++ b/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala @@ -22,6 +22,7 @@ trait ConnectionHandler extends ScalaBusMod { val config: Configuration val maxPoolSize: Int + val transactionTimeout: Long lazy val vertx: Vertx = verticle.vertx lazy val container: Container = verticle.container @@ -36,8 +37,6 @@ trait ConnectionHandler extends ScalaBusMod { def statementDelimiter: String = ";" - private def timeout = 500L /* FIXME from config file! */ - import org.vertx.scala.core.eventbus._ private def receiver(withConnectionFn: (Connection => Future[SyncReply]) => Future[SyncReply]): Receive = (msg: Message[JsonObject]) => { @@ -62,7 +61,7 @@ trait ConnectionHandler extends ScalaBusMod { private def mapRepliesToTransactionReceive(c: Connection): BusModReply => BusModReply = { case AsyncReply(receiveEndFuture) => AsyncReply(receiveEndFuture.map(mapRepliesToTransactionReceive(c))) - case Ok(v, None) => Ok(v, Some(ReceiverWithTimeout(inTransactionReceive(c), timeout, () => failTransaction(c)))) + case Ok(v, None) => Ok(v, Some(ReceiverWithTimeout(inTransactionReceive(c), transactionTimeout, () => failTransaction(c)))) case x => x } @@ -81,7 +80,7 @@ trait ConnectionHandler extends ScalaBusMod { protected def beginTransaction(msg: Message[JsonObject]) = AsyncReply { pool.take().flatMap { c => c.sendQuery(transactionBegin) map { _ => - Ok(Json.obj(), Some(ReceiverWithTimeout(inTransactionReceive(c), timeout, () => failTransaction(c)))) + Ok(Json.obj(), Some(ReceiverWithTimeout(inTransactionReceive(c), transactionTimeout, () => failTransaction(c)))) } } } diff --git a/src/main/scala/io/vertx/asyncsql/database/MySqlConnectionHandler.scala b/src/main/scala/io/vertx/asyncsql/database/MySqlConnectionHandler.scala index 36c97b2..f5b9d76 100644 --- a/src/main/scala/io/vertx/asyncsql/database/MySqlConnectionHandler.scala +++ b/src/main/scala/io/vertx/asyncsql/database/MySqlConnectionHandler.scala @@ -4,7 +4,7 @@ import org.vertx.scala.platform.Verticle import com.github.mauricio.async.db.Configuration import io.vertx.asyncsql.Starter -class MySqlConnectionHandler(val verticle: Starter, val config: Configuration, val maxPoolSize: Int) extends ConnectionHandler { +class MySqlConnectionHandler(val verticle: Starter, val config: Configuration, val maxPoolSize: Int, val transactionTimeout: Long) extends ConnectionHandler { override val dbType: String = "mysql" override protected def escapeField(str: String): String = "`" + str.replace("`", "\\`") + "`" diff --git a/src/main/scala/io/vertx/asyncsql/database/PostgreSqlConnectionHandler.scala b/src/main/scala/io/vertx/asyncsql/database/PostgreSqlConnectionHandler.scala index 1b5337d..45d40d1 100644 --- a/src/main/scala/io/vertx/asyncsql/database/PostgreSqlConnectionHandler.scala +++ b/src/main/scala/io/vertx/asyncsql/database/PostgreSqlConnectionHandler.scala @@ -4,6 +4,6 @@ import org.vertx.scala.platform.Verticle import com.github.mauricio.async.db.Configuration import io.vertx.asyncsql.Starter -class PostgreSqlConnectionHandler(val verticle: Starter, val config: Configuration, val maxPoolSize: Int) extends ConnectionHandler { +class PostgreSqlConnectionHandler(val verticle: Starter, val config: Configuration, val maxPoolSize: Int, val transactionTimeout: Long) extends ConnectionHandler { override val dbType: String = "postgresql" } \ No newline at end of file From 355f1c60c1dd8773ff7517aab6ba79a3699ab9a8 Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Fri, 4 Jul 2014 15:38:51 +0200 Subject: [PATCH 07/14] incremented versions, more tests, fixed rollback after error reply Signed-off-by: Joern Bernhardt --- .../asyncsql/database/ConnectionHandler.scala | 1 + .../io/vertx/asyncsql/test/BaseSqlTests.scala | 641 ++++++++++-------- .../vertx/asyncsql/test/mysql/MySqlTest.scala | 7 +- 3 files changed, 352 insertions(+), 297 deletions(-) diff --git a/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala b/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala index c369e32..3c06c3e 100644 --- a/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala +++ b/src/main/scala/io/vertx/asyncsql/database/ConnectionHandler.scala @@ -62,6 +62,7 @@ trait ConnectionHandler extends ScalaBusMod { private def mapRepliesToTransactionReceive(c: Connection): BusModReply => BusModReply = { case AsyncReply(receiveEndFuture) => AsyncReply(receiveEndFuture.map(mapRepliesToTransactionReceive(c))) case Ok(v, None) => Ok(v, Some(ReceiverWithTimeout(inTransactionReceive(c), transactionTimeout, () => failTransaction(c)))) + case Error(msg, id, v, None) => Error(msg, id, v, Some(ReceiverWithTimeout(inTransactionReceive(c), transactionTimeout, () => failTransaction(c)))) case x => x } diff --git a/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala b/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala index bf4bb94..a695125 100644 --- a/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala +++ b/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala @@ -11,39 +11,100 @@ import org.vertx.scala.core.FunctionConverters._ trait BaseSqlTests { this: SqlTestVerticle => - private def withTable[X](tableName: String)(fn: => Future[X]) = { - (for { - _ <- createTable(tableName) - sth <- fn - _ <- dropTable(tableName) - } yield sth) recoverWith { - case x => - dropTable(tableName) map (_ => throw x) - } + protected def isMysql: Boolean = false + + private def failedTest: PartialFunction[Throwable, Unit] = { + case ex: Throwable => + logger.warn("failed in test", ex) + fail("test failed. see warning above") } - private def asyncTableTest[X](tableName: String)(fn: => Future[X]) = asyncTest(withTable(tableName)(fn)) + private def sendWithTimeout(json: JsonObject): Future[(Message[JsonObject], JsonObject)] = { + val p = Promise[(Message[JsonObject], JsonObject)]() + vertx.eventBus.sendWithTimeout(address, json, 5000, { + case Success(reply) => p.success(reply, reply.body()) + case Failure(ex) => p.failure(ex) + }: Try[Message[JsonObject]] => Unit) + p.future + } - private def typeTestInsert[X](fn: => Future[X]) = asyncTableTest("some_test") { - expectOk(insert("some_test", - new JsonArray( """["name","email","is_male","age","money","wedding_date"]"""), - new JsonArray( """[["Mr. Test","test@example.com",true,15,167.31,"2024-04-01"], - ["Ms Test2","test2@example.com",false,43,167.31,"1997-12-24"]]"""))) flatMap { - _ => - fn - } + private def replyWithTimeout(msg: Message[JsonObject], json: JsonObject): Future[(Message[JsonObject], JsonObject)] = { + val p = Promise[(Message[JsonObject], JsonObject)]() + msg.replyWithTimeout(json, 5000, { + case Success(reply) => p.success(reply, reply.body()) + case Failure(ex) => p.failure(ex) + }: Try[Message[JsonObject]] => Unit) + p.future } - @Test - def simpleConnection(): Unit = asyncTest { - expectOk(raw("SELECT 0")) map { - reply => - val res = reply.getArray("results") - assertEquals(1, res.size()) - assertEquals(0, res.get[JsonArray](0).get[Number](0).intValue()) - } + private def checkOkay(json: JsonObject)(msg: (Message[JsonObject], JsonObject)): (Message[JsonObject], JsonObject) = { + assertEquals(s"should get 'ok' back when sending ${json.encode()}, but got ${msg._2.encode()}", + "ok", msg._2.getString("status")) + (msg._1, msg._2) + } + + private def checkError(json: JsonObject)(msg: (Message[JsonObject], JsonObject)): (Message[JsonObject], JsonObject) = { + assertEquals(s"should get an 'error' back when sending ${json.encode()}, but got ${msg._2.encode()}", + "error", msg._2.getString("status")) + (msg._1, msg._2) + } + + private def sendOk(json: JsonObject): Future[(Message[JsonObject], JsonObject)] = + sendWithTimeout(json) map checkOkay(json) + + private def sendFail(json: JsonObject): Future[(Message[JsonObject], JsonObject)] = + sendWithTimeout(json) map checkError(json) + + private def replyOk(msg: Message[JsonObject], json: JsonObject): Future[(Message[JsonObject], JsonObject)] = + replyWithTimeout(msg, json) map checkOkay(json) + + private def replyFail(msg: Message[JsonObject], json: JsonObject): Future[(Message[JsonObject], JsonObject)] = + replyWithTimeout(msg, json) map checkError(json) + + private def setupTableTest(): Future[_] = for { + (msg, reply) <- sendOk(raw(createTableStatement("some_test"))) + } yield { + assertEquals(0, reply.getInteger("rows")) + } + + private def setupTypeTest(): Future[_] = for { + _ <- setupTableTest() + (msg, reply) <- sendOk(insert("some_test", + Json.fromArrayString( """["name","email","is_male","age","money","wedding_date"]"""), + Json.fromArrayString( + """[["Mr. Test","test@example.com",true,15,167.31,"2024-04-01"], + | ["Ms Test2","test2@example.com",false,43,167.31,"1997-12-24"]]""".stripMargin))) + } yield () + + private def checkSameFields(arr1: JsonArray, arr2: JsonArray) = { + import scala.collection.JavaConversions._ + arr1.foreach(elem => assertTrue(arr2.contains(elem))) + } + + private def checkMrTest(mrTest: JsonArray) = { + assertEquals("Mr. Test", mrTest.get[String](0)) + assertEquals("test@example.com", mrTest.get[String](1)) + assertTrue(mrTest.get[Any](2) match { + case b: Boolean => b + case i: Number => i.intValue() == 1 + case x => false + }) + assertEquals(15, mrTest.get[Number](3).intValue()) + assertEquals(167.31, mrTest.get[Number](4).doubleValue(), 0.0001) + // FIXME check date conversion + // assertEquals("2024-04-01", mrTest.get[JsonObject](5)) } + @Test + def simpleConnection(): Unit = (for { + (msg, reply) <- sendOk(raw("SELECT 0")) + } yield { + val res = reply.getArray("results") + assertEquals(1, res.size()) + assertEquals(0, res.get[JsonArray](0).get[Number](0).intValue()) + testComplete() + }) recover failedTest + @Test def poolSize(): Unit = asyncTest { val n = 10 @@ -65,327 +126,317 @@ trait BaseSqlTests { } @Test - def multipleFields(): Unit = asyncTest { - expectOk(raw("SELECT 1 a, 0 b")) map { - reply => - val res = reply.getArray("results") - assertEquals(1, res.size()) - val firstElem = res.get[JsonArray](0) - assertEquals(1, firstElem.get[Number](0).intValue()) - assertEquals(0, firstElem.get[Number](1).intValue()) - } - } + def multipleFields(): Unit = (for { + (msg, reply) <- sendOk(raw("SELECT 1 a, 0 b")) + } yield { + val res = reply.getArray("results") + assertEquals(1, res.size()) + val firstElem = res.get[JsonArray](0) + assertEquals(1, firstElem.get[Number](0).intValue()) + assertEquals(0, firstElem.get[Number](1).intValue()) + testComplete() + }) recover failedTest @Test - def multipleFieldsOrder(): Unit = typeTestInsert { - import scala.collection.JavaConverters._ - expectOk(raw("SELECT is_male, age, email, money, name FROM some_test WHERE is_male = true")) map { - reply => - val receivedFields = reply.getArray("fields") - val results = reply.getArray("results").get[JsonArray](0) - - assertEquals(1, reply.getInteger("rows")) - - val columnNamesList = receivedFields.asScala.toList - - assertEquals("Mr. Test", results.get(columnNamesList.indexOf("name"))) - assertEquals("test@example.com", results.get(columnNamesList.indexOf("email"))) - assertEquals(15, results.get[Int](columnNamesList.indexOf("age"))) - assertTrue(results.get[Any](columnNamesList.indexOf("is_male")) match { - case b: Boolean => b - case i: Number => i.intValue() == 1 - case x => false - }) - assertEquals(167.31, results.get[Number](columnNamesList.indexOf("money")).doubleValue(), 0.01) - } - } + def multipleFieldsOrder(): Unit = + (for { + _ <- setupTypeTest() + (msg, reply) <- sendOk(raw("SELECT is_male, age, email, money, name FROM some_test WHERE is_male = true")) + } yield { + import collection.JavaConverters._ + val receivedFields = reply.getArray("fields") + val results = reply.getArray("results").get[JsonArray](0) + + assertEquals(1, reply.getInteger("rows")) + + val columnNamesList = receivedFields.asScala.toList + + assertEquals("Mr. Test", results.get(columnNamesList.indexOf("name"))) + assertEquals("test@example.com", results.get(columnNamesList.indexOf("email"))) + assertEquals(15, results.get[Int](columnNamesList.indexOf("age"))) + assertTrue(results.get[Any](columnNamesList.indexOf("is_male")) match { + case b: Boolean => b + case i: Number => i.intValue() == 1 + case x => false + }) + assertEquals(167.31, results.get[Number](columnNamesList.indexOf("money")).doubleValue(), 0.01) + testComplete() + }) recover failedTest @Test - def createAndDropTable(): Unit = asyncTest { - createTable("some_test") flatMap (_ => dropTable("some_test")) map { - reply => - assertEquals(0, reply.getInteger("rows")) + def createAndDropTable(): Unit = (for { + (msg, dropIfExistsReply) <- sendOk(raw("DROP TABLE IF EXISTS some_test;")) + (msg, createReply) <- sendOk(raw("CREATE TABLE some_test (id SERIAL, name VARCHAR(255));")) + (msg, insertReply) <- sendOk(raw("INSERT INTO some_test (name) VALUES ('tester');")) + (msg, selectReply) <- sendOk(raw("SELECT name FROM some_test")) + (msg, dropReply) <- { + assertEquals("tester", try { + selectReply.getArray("results").get[JsonArray](0).get[String](0) + } catch { + case ex: Throwable => fail(s"Should be able to get a result before drop, but got ${selectReply.encode()}") + }) + sendOk(raw("DROP TABLE some_test;")) } - } + (msg, selectReply) <- sendFail(raw("SELECT name FROM some_test")) + } yield { + val error = selectReply.getString("message") + assertTrue(s"Not the right error message $error", + error.contains("some_test") && (error.contains("doesn't exist") || error.contains("does not exist"))) + testComplete() + }) recover failedTest @Test - def insertCorrect(): Unit = asyncTableTest("some_test") { - expectOk(insert("some_test", new JsonArray( """["name","email"]"""), new JsonArray( """[["Test","test@example.com"],["Test2","test2@example.com"]]"""))) - } + def insertCorrectWithMissingValues(): Unit = (for { + _ <- setupTableTest() + _ <- sendOk(insert("some_test", + Json.fromArrayString( """["name","email"]"""), + Json.fromArrayString( """[["Test","test@example.com"], + | ["Test2","test2@example.com"]]""".stripMargin))) + } yield testComplete()) recover failedTest @Test - def insertNullValues(): Unit = asyncTableTest("some_test") { - expectOk(insert("some_test", new JsonArray( """["name","email"]"""), new JsonArray( """[[null,"test@example.com"],[null,"test2@example.com"]]"""))) - } + def insertNullValues(): Unit = (for { + _ <- setupTableTest() + _ <- sendOk(insert("some_test", + Json.fromArrayString( """["name","email"]"""), + Json.fromArrayString( """[[null,"test@example.com"], + | [null,"test2@example.com"]]""".stripMargin))) + } yield testComplete()) recover failedTest @Test - def insertTypeTest(): Unit = typeTestInsert { - Future.successful() - } + def insertTypeTest(): Unit = (for { + _ <- setupTypeTest() + } yield testComplete()) recover failedTest @Test - def insertMaliciousDataTest(): Unit = asyncTableTest("some_test") { - // If this SQL injection works, the drop table of asyncTableTest would throw an exception - expectOk(insert("some_test", - new JsonArray( """["name","email","is_male","age","money","wedding_date"]"""), - new JsonArray( """[["Mr. Test","test@example.com",true,15,167.31,"2024-04-01"], - ["Ms Test2','some@example.com',false,15,167.31,'2024-04-01');DROP TABLE some_test;--","test2@example.com",false,43,167.31,"1997-12-24"]]"""))) - } + def insertMaliciousDataTest(): Unit = (for { + _ <- setupTableTest() + (msg, insertReply) <- sendOk(insert("some_test", + Json.fromArrayString( """["name","email","is_male","age","money","wedding_date"]"""), + Json.fromArrayString( + """[["Mr. Test","test@example.com",true,15,167.31,"2024-04-01"], + | ["Ms Test2','some@example.com',false,15,167.31,'2024-04-01');DROP TABLE some_test;--","test2@example.com",false,43,167.31,"1997-12-24"]]""".stripMargin))) + (msg, selectReply) <- sendOk(raw("SELECT * FROM some_test")) + } yield { + assertEquals(2, selectReply.getArray("results").size()) + testComplete() + }) recover failedTest @Test - def insertUniqueProblem(): Unit = asyncTableTest("some_test") { - expectError(insert("some_test", new JsonArray( """["name","email"]"""), new JsonArray( """[["Test","test@example.com"],["Test","test@example.com"]]"""))) map { - reply => - logger.info("expected error: " + reply.encode()) - } - } + def insertUniqueProblem(): Unit = (for { + _ <- setupTableTest() + (msg, reply) <- sendFail(insert("some_test", + Json.fromArrayString( """["name","email"]"""), + Json.fromArrayString( + """[["Test","test@example.com"], + | ["Test","test@example.com"]]""".stripMargin))) + } yield testComplete()) recover failedTest @Test - def selectWithoutFields(): Unit = typeTestInsert { - expectOk(select("some_test")) map { - reply => - val receivedFields = reply.getArray("fields") - logger.info("received: " + receivedFields.encode()) - - def assertFieldName(field: String) = { - assertTrue("fields should contain '" + field + "'", receivedFields.contains(field)) - } - assertFieldName("id") - assertFieldName("name") - assertFieldName("email") - assertFieldName("is_male") - assertFieldName("age") - assertFieldName("money") - assertFieldName("wedding_date") - val moneyField = receivedFields.toArray().indexOf("money") - - val mrTest = reply.getArray("results").get[JsonArray](0) - assertTrue(mrTest.contains("Mr. Test")) - assertTrue(mrTest.contains("test@example.com")) - assertTrue(mrTest.contains(true) || mrTest.contains(1)) - assertTrue(mrTest.contains(15)) - assertEquals(167.31, mrTest.get[Number](moneyField).doubleValue(), 0.0001) + def selectWithoutFields(): Unit = (for { + _ <- setupTypeTest() + (msg, reply) <- sendOk(select("some_test")) + } yield { + val receivedFields = reply.getArray("fields") + logger.info("received: " + receivedFields.encode()) + + def assertFieldName(field: String) = { + assertTrue("fields should contain '" + field + "'", receivedFields.contains(field)) } - } + assertFieldName("id") + assertFieldName("name") + assertFieldName("email") + assertFieldName("is_male") + assertFieldName("age") + assertFieldName("money") + assertFieldName("wedding_date") + val moneyField = receivedFields.toArray.indexOf("money") + + val mrTest = reply.getArray("results").get[JsonArray](0) + assertTrue(mrTest.contains("Mr. Test")) + assertTrue(mrTest.contains("test@example.com")) + assertTrue(mrTest.contains(true) || mrTest.contains(1)) + assertTrue(mrTest.contains(15)) + assertEquals(167.31, mrTest.get[Number](moneyField).doubleValue(), 0.0001) + testComplete() + }) recover failedTest @Test - def selectEverything(): Unit = typeTestInsert { + def selectEverything(): Unit = { val fieldsArray = Json.arr("name", "email", "is_male", "age", "money", "wedding_date") - expectOk(select("some_test", fieldsArray)) map { - reply => - val receivedFields = reply.getArray("fields") - checkSameFields(fieldsArray, receivedFields) - val results = reply.getArray("results") - val mrTest = results.get[JsonArray](0) - checkMrTest(mrTest) - } - } - - private def checkSameFields(arr1: JsonArray, arr2: JsonArray) = { - import scala.collection.JavaConversions._ - arr1.foreach(elem => assertTrue(arr2.contains(elem))) - } - - private def checkTestPerson(mrOrMrs: JsonArray) = { - mrOrMrs.get[String](0) match { - case "Mr. Test" => checkMrTest(mrOrMrs) - case "Mrs. Test" => checkMrsTest(mrOrMrs) - } + (for { + _ <- setupTypeTest() + (msg, reply) <- sendOk(select("some_test", fieldsArray)) + } yield { + val receivedFields = reply.getArray("fields") + checkSameFields(fieldsArray, receivedFields) + val results = reply.getArray("results") + val mrTest = results.get[JsonArray](0) + checkMrTest(mrTest) + testComplete() + }) recover failedTest } - private def checkMrTest(mrTest: JsonArray) = { - assertEquals("Mr. Test", mrTest.get[String](0)) - assertEquals("test@example.com", mrTest.get[String](1)) - assertTrue(mrTest.get[Any](2) match { - case b: Boolean => b - case i: Number => i.intValue() == 1 - case x => false - }) - assertEquals(15, mrTest.get[Number](3).intValue()) - assertEquals(167.31, mrTest.get[Number](4).doubleValue(), 0.0001) - // FIXME check date conversion - // assertEquals("2024-04-01", mrTest.get[JsonObject](5)) - } + @Test + def selectFiltered(): Unit = { + val fieldsArray = Json.arr("name", "email") - private def checkMrsTest(mrsTest: JsonArray) = { - assertEquals("Mrs. Test", mrsTest.get[String](0)) - assertEquals("test2@example.com", mrsTest.get[String](1)) - assertEquals(false, mrsTest.get[Boolean](2)) - assertEquals(43L, mrsTest.get[Long](3)) - assertEquals(167.31, mrsTest.get[Number](4).doubleValue(), 0.0001) - // FIXME check date conversion - // assertEquals("1997-12-24", mrsTest.get[JsonObject](5)) + (for { + _ <- setupTypeTest() + (msg, reply) <- sendOk(select("some_test", fieldsArray)) + } yield { + val receivedFields = reply.getArray("fields") + assertEquals(s"arrays ${fieldsArray.encode()} and ${receivedFields.encode()} should match", + fieldsArray, receivedFields) + assertEquals(2, reply.getInteger("rows")) + val results = reply.getArray("results") + val mrOrMrs = results.get[JsonArray](0) + mrOrMrs.get[String](0) match { + case "Mr. Test" => + assertEquals("Mr. Test", mrOrMrs.get[String](0)) + assertEquals("test@example.com", mrOrMrs.get[String](1)) + case "Mrs. Test" => + assertEquals("Mrs. Test", mrOrMrs.get[String](0)) + assertEquals("test2@example.com", mrOrMrs.get[String](1)) + } + testComplete() + }) recover failedTest } @Test - def selectFiltered(): Unit = typeTestInsert { - val fieldsArray = new JsonArray( """["name","email"]""") - expectOk(select("some_test", fieldsArray)) map { - reply => - val receivedFields = reply.getArray("fields") - assertTrue("arrays " + fieldsArray.encode() + " and " + receivedFields.encode() + - " should match", fieldsArray == receivedFields) - // assertEquals(2, reply.getInteger("rows")) - val results = reply.getArray("results") - val mrOrMrs = results.get[JsonArray](0) - mrOrMrs.get[String](0) match { - case "Mr. Test" => - assertEquals("Mr. Test", mrOrMrs.get[String](0)) - assertEquals("test@example.com", mrOrMrs.get[String](1)) - case "Mrs. Test" => - assertEquals("Mrs. Test", mrOrMrs.get[String](0)) - assertEquals("test2@example.com", mrOrMrs.get[String](1)) - } - } - } + def preparedSelect(): Unit = (for { + _ <- setupTypeTest() + (msg, reply) <- sendOk(prepared("SELECT email FROM some_test WHERE name=? AND age=?", Json.arr("Mr. Test", 15))) + } yield { + val receivedFields = reply.getArray("fields") + assertEquals(Json.arr("email"), receivedFields) + assertEquals(1, reply.getInteger("rows")) + assertEquals("test@example.com", reply.getArray("results").get[JsonArray](0).get[String](0)) + testComplete() + }) recover failedTest @Test - def preparedSelect(): Unit = typeTestInsert { - expectOk(prepared("SELECT email FROM some_test WHERE name=? AND age=?", Json.arr("Mr. Test", 15))) map { - reply => - val receivedFields = reply.getArray("fields") - assertEquals(Json.arr("email"), receivedFields) - // assertEquals(1, reply.getInteger("rows")) - assertEquals("test@example.com", reply.getArray("results").get[JsonArray](0).get[String](0)) - } - } + def simpleTransaction(): Unit = (for { + _ <- setupTypeTest() + (msg, transactionReply) <- sendOk( + transaction( + insert("some_test", Json.arr("name", "email", "is_male", "age", "money"), + Json.arr(Json.arr("Mr. Test jr.", "test3@example.com", true, 5, 2))), + raw("UPDATE some_test SET age=6 WHERE name = 'Mr. Test jr.'"))) + (msg, reply) <- sendOk(raw("SELECT SUM(age) FROM some_test WHERE is_male = true")) + } yield { + val results = reply.getArray("results") + assertEquals(1, results.size()) + assertEquals(21, results.get[JsonArray](0).get[Number](0).intValue()) + testComplete() + }) recover failedTest @Test - def transaction(): Unit = typeTestInsert { - (for { - a <- expectOk( - transaction( - insert("some_test", Json.arr("name", "email", "is_male", "age", "money"), - Json.arr(Json.arr("Mr. Test jr.", "test3@example.com", true, 5, 2))), - raw("UPDATE some_test SET age=6 WHERE name = 'Mr. Test jr.'"))) - b <- expectOk(raw("SELECT SUM(age) FROM some_test WHERE is_male = true")) - } yield b) map { - reply => - val results = reply.getArray("results") - assertEquals(1, results.size()) - assertEquals(21, results.get[JsonArray](0).get[Number](0).intValue()) - } - } + def transactionWithPreparedStatement(): Unit = (for { + _ <- setupTypeTest() + (msg, transactionReply) <- sendOk( + transaction( + insert("some_test", Json.arr("name", "email", "is_male", "age", "money"), + Json.arr(Json.arr("Mr. Test jr.", "test3@example.com", true, 5, 2))), + prepared("UPDATE some_test SET age=? WHERE name=?", Json.arr(6, "Mr. Test jr.")))) + (msg, reply) <- sendOk(raw("SELECT SUM(age) FROM some_test WHERE is_male = true")) + } yield { + val results = reply.getArray("results") + assertEquals(1, results.size()) + assertEquals(21, results.get[JsonArray](0).get[Number](0).intValue()) + testComplete() + }) recover failedTest @Test - def transactionWithPreparedStatement(): Unit = typeTestInsert { - (for { - a <- expectOk( - transaction( - insert("some_test", Json.arr("name", "email", "is_male", "age", "money"), - Json.arr(Json.arr("Mr. Test jr.", "test3@example.com", true, 5, 2))), - prepared("UPDATE some_test SET age=? WHERE name=?", Json.arr(6, "Mr. Test jr.")))) - b <- expectOk(raw("SELECT SUM(age) FROM some_test WHERE is_male = true")) - } yield b) map { - reply => - val results = reply.getArray("results") - assertEquals(1, results.size()) - assertEquals(21, results.get[JsonArray](0).get[Number](0).intValue()) + def startAndEndTransaction(): Unit = (for { + (msg, beginReply) <- sendOk(Json.obj("action" -> "begin")) + (msg, selectReply) <- replyOk(msg, raw("SELECT 15")) + (msg, commitReply) <- { + val arr = selectReply.getArray("results") + assertEquals("ok", selectReply.getString("status")) + assertEquals(1, arr.size()) + assertEquals(15, arr.get[JsonArray](0).get[Number](0).longValue()) + + replyOk(msg, Json.obj("action" -> "commit")) } - } + } yield testComplete()) recover failedTest - protected def expectOkMsg(q: JsonObject) = { - val p = Promise[Message[JsonObject]]() - vertx.eventBus.sendWithTimeout(address, q, 500L, { - case Success(reply) => - logger.info("got a reply: " + reply.body().encode()) - p.success(reply) - case Failure(ex) => - logger.error("timeout", ex) - fail(s"got a timeout when expected an ok message ${ex.toString}") - }: Try[Message[JsonObject]] => Unit) - p.future - } @Test - def startAndEndTransaction(): Unit = { - expectOkMsg(Json.obj("action" -> "begin")) map { msg => - logger.info("Should be in transaction!") - msg.replyWithTimeout(raw("SELECT 15"), 500L, { - case Success(reply) => Option(reply.body().getArray("results")) map { arr => - assertEquals("ok", reply.body().getString("status")) - assertEquals(1, arr.size()) - assertEquals(15, arr - .get[JsonArray](0) - .get[Number](0).longValue()) - logger.info("First select DONE!") - reply.replyWithTimeout(Json.obj("action" -> "commit"), 500L, { - case Success(endReply) => - assertEquals("ok", endReply.body().getString("status")) - testComplete() - case Failure(ex) => - logger.error("timeout when waiting for final reply (end transaction)", ex) - fail(s"got a timeout when expected end reply ${ex.toString}") - }: Try[Message[JsonObject]] => Unit) - } - case Failure(ex) => - logger.error("timeout when waiting for SELECT reply", ex) - fail(s"got a timeout when expected reply ${ex.toString}") - }: Try[Message[JsonObject]] => Unit) - } - } + def updateInTransaction(): Unit = (for { + _ <- setupTypeTest() + (msg, beginReply) <- sendOk(Json.obj("action" -> "begin")) + (msg, updateReply) <- replyOk(msg, raw("UPDATE some_test set email = 'updated@test.com' WHERE name = 'Mr. Test'")) + (msg, commitReply) <- replyOk(msg, Json.obj("action" -> "commit")) + (msg, checkReply) <- sendOk(raw("SELECT email FROM some_test WHERE name = 'Mr. Test'")) + } yield { + val results = checkReply.getArray("results") + val mrTest = results.get[JsonArray](0) + assertEquals("updated@test.com", mrTest.get[String](0)) + logger.info("all tests completed") + testComplete() + }) recover failedTest - private def replyWithTimeout(msg: Message[JsonObject], value: JsonObject): Future[Message[JsonObject]] = { - val p = Promise[Message[JsonObject]] - msg.replyWithTimeout(value, 500L, { - case Success(r) => p.success(r) - case Failure(x) => p.failure(x) - }: Try[Message[JsonObject]] => Unit) - p.future - } + @Test + def violateForeignKey(): Unit = (for { + (msg, beginResult) <- sendOk(Json.obj("action" -> "begin")) + (msg, _) <- replyOk(msg, raw("DROP TABLE IF EXISTS test_one;")) + (msg, _) <- replyOk(msg, raw("DROP TABLE IF EXISTS test_two;")) + (msg, _) <- replyOk(msg, raw( """CREATE TABLE test_one ( + | id SERIAL, + | name VARCHAR(255), + | PRIMARY KEY (id) + |);""".stripMargin)) + (msg, _) <- replyOk(msg, raw( + s"""CREATE TABLE test_two ( + | id SERIAL, + | name VARCHAR(255), + | one_id BIGINT ${if (isMysql) "UNSIGNED" else ""} NOT NULL, + | PRIMARY KEY (id) + |);""".stripMargin)) + (msg, _) <- replyOk(msg, raw( + """ALTER TABLE test_two ADD CONSTRAINT test_two_one_id_fk + |FOREIGN KEY (one_id) + |REFERENCES test_one (id);""".stripMargin)) + (msg, _) <- replyOk(msg, raw("INSERT INTO test_one (name) VALUES ('first'),('second');")) + (msg, setupResult) <- replyOk(msg, raw("INSERT INTO test_two (name, one_id) VALUES ('twoone', 1);")) + (msg, insertViolatedResult) <- replyFail(msg, raw("INSERT INTO test_two (name, one_id) VALUES ('twothree', 3);")) + (msg, rollbackResult) <- replyOk(msg, raw("ROLLBACK;")) + } yield testComplete()) recover failedTest @Test - def updateInTransaction(): Unit = typeTestInsert { - (for{ - beginReply <- expectOkMsg(Json.obj("action" -> "begin")) - updateReply <- replyWithTimeout(beginReply, raw("UPDATE some_test set email = 'updated@test.com' WHERE name = 'Mr. Test'")) - commitReply <- replyWithTimeout(updateReply, Json.obj("action" -> "commit")) - checkReply <- expectOk(raw("SELECT email FROM some_test WHERE name = 'Mr. Test'")) - } yield { - val results = checkReply.getArray("results") - val mrTest = results.get[JsonArray](0) - assertEquals("updated@test.com", mrTest.get[String](0)) - logger.info("all tests completed") - }) recover { - case ex => - logger.error("timeout when waiting for reply", ex) - fail(s"got a timeout when expected reply ${ex.toString}") - } - } + def wrongQueryInTransaction(): Unit = (for { + _ <- setupTypeTest() + (msg, beginReply) <- sendOk(Json.obj("action" -> "begin")) + (msg, updateReply) <- replyWithTimeout(msg, raw("this is a bad raw query for sql")) + } yield { + assertEquals("error", updateReply.getString("status")) + testComplete() + }) recover failedTest @Test - def rollBackTransaction(): Unit = typeTestInsert { + def rollBackTransaction(): Unit = { val fieldsArray = Json.arr("name", "email", "is_male", "age", "money", "wedding_date") (for { - msg <- expectOkMsg(Json.obj("action" -> "begin")) - reply <- replyWithTimeout(msg, raw("UPDATE some_test set email = 'shouldRollback@test.com' WHERE name = 'Mr. Test'")) - checkUpdateReply <- { - assertEquals("ok", reply.body().getString("status")) - replyWithTimeout(reply, raw("SELECT email FROM some_test WHERE name = 'Mr. Test'")) - } - endReply <- { - assertEquals("ok", checkUpdateReply.body().getString("status")) - val results = checkUpdateReply.body().getArray("results") + _ <- setupTypeTest() + (msg, beginReply) <- sendOk(Json.obj("action" -> "begin")) + (msg, reply) <- replyOk(msg, raw("UPDATE some_test set email = 'shouldRollback@test.com' WHERE name = 'Mr. Test'")) + (msg, checkUpdateReply) <- replyOk(msg, raw("SELECT email FROM some_test WHERE name = 'Mr. Test'")) + (msg, endReply) <- { + val results = checkUpdateReply.getArray("results") val mrTest = results.get[JsonArray](0) assertEquals("shouldRollback@test.com", mrTest.get[String](0)) logger.info("Update done, now do rollback") - replyWithTimeout(checkUpdateReply, Json.obj("action" -> "rollback")) - } - checkReply <- { - logger.info("rollback done, now check if everything is like before the update") - assertEquals("ok", endReply.body().getString("status")) - expectOk(select("some_test", fieldsArray)) + replyOk(msg, Json.obj("action" -> "rollback")) } + (msg, checkReply) <- sendOk(select("some_test", fieldsArray)) } yield { val results = checkReply.getArray("results") val mrTest = results.get[JsonArray](0) checkMrTest(mrTest) - logger.info("all tests completed") - }) recover { - case ex => - logger.error("timeout when waiting for reply", ex) - fail(s"got a timeout when expected reply ${ex.toString}") - } + logger.info("rolled back nicely") + testComplete() + }) recover failedTest } } \ No newline at end of file diff --git a/src/test/scala/io/vertx/asyncsql/test/mysql/MySqlTest.scala b/src/test/scala/io/vertx/asyncsql/test/mysql/MySqlTest.scala index d7416d3..0530126 100644 --- a/src/test/scala/io/vertx/asyncsql/test/mysql/MySqlTest.scala +++ b/src/test/scala/io/vertx/asyncsql/test/mysql/MySqlTest.scala @@ -1,14 +1,17 @@ package io.vertx.asyncsql.test.mysql import org.vertx.scala.core.json.Json -import io.vertx.asyncsql.test.{ BaseSqlTests, SqlTestVerticle } +import io.vertx.asyncsql.test.{BaseSqlTests, SqlTestVerticle} class MySqlTest extends SqlTestVerticle with BaseSqlTests { val address = "campudus.asyncdb" val config = Json.obj("address" -> address, "connection" -> "MySQL", "maxPoolSize" -> 3) + override def isMysql = true + override def doBefore() = expectOk(raw("DROP TABLE IF EXISTS `some_test`")) + override def getConfig = config override def createTableStatement(tableName: String) = """ @@ -22,6 +25,6 @@ CREATE TABLE """ + tableName + """ ( wedding_date DATE, PRIMARY KEY (id) ); -""" + """ } \ No newline at end of file From 6ee339289e85086c6e9fd44530cf6284dcee543d Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Fri, 4 Jul 2014 15:47:40 +0200 Subject: [PATCH 08/14] small fix for tests. should never drop table test_one before test_two when foreign key applies Signed-off-by: Joern Bernhardt --- src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala b/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala index a695125..3a097c5 100644 --- a/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala +++ b/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala @@ -380,8 +380,8 @@ trait BaseSqlTests { @Test def violateForeignKey(): Unit = (for { (msg, beginResult) <- sendOk(Json.obj("action" -> "begin")) - (msg, _) <- replyOk(msg, raw("DROP TABLE IF EXISTS test_one;")) (msg, _) <- replyOk(msg, raw("DROP TABLE IF EXISTS test_two;")) + (msg, _) <- replyOk(msg, raw("DROP TABLE IF EXISTS test_one;")) (msg, _) <- replyOk(msg, raw( """CREATE TABLE test_one ( | id SERIAL, | name VARCHAR(255), From 45577793dbb13c225fe7452a58dd74fda497d968 Mon Sep 17 00:00:00 2001 From: Max Stemplinger Date: Tue, 16 Sep 2014 09:47:23 +0200 Subject: [PATCH 09/14] date, datetime and timestamp tests --- .../io/vertx/asyncsql/test/BaseSqlTests.scala | 36 ++++++++++++++++--- .../vertx/asyncsql/test/SqlTestVerticle.scala | 9 ++++- .../vertx/asyncsql/test/mysql/MySqlTest.scala | 30 ++++++++++++++-- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala b/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala index 3a097c5..e7256c6 100644 --- a/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala +++ b/src/test/scala/io/vertx/asyncsql/test/BaseSqlTests.scala @@ -13,7 +13,7 @@ trait BaseSqlTests { protected def isMysql: Boolean = false - private def failedTest: PartialFunction[Throwable, Unit] = { + protected def failedTest: PartialFunction[Throwable, Unit] = { case ex: Throwable => logger.warn("failed in test", ex) fail("test failed. see warning above") @@ -49,10 +49,10 @@ trait BaseSqlTests { (msg._1, msg._2) } - private def sendOk(json: JsonObject): Future[(Message[JsonObject], JsonObject)] = + protected def sendOk(json: JsonObject): Future[(Message[JsonObject], JsonObject)] = sendWithTimeout(json) map checkOkay(json) - private def sendFail(json: JsonObject): Future[(Message[JsonObject], JsonObject)] = + protected def sendFail(json: JsonObject): Future[(Message[JsonObject], JsonObject)] = sendWithTimeout(json) map checkError(json) private def replyOk(msg: Message[JsonObject], json: JsonObject): Future[(Message[JsonObject], JsonObject)] = @@ -439,4 +439,32 @@ trait BaseSqlTests { testComplete() }) recover failedTest } -} \ No newline at end of file + + @Test + def dateTest(): Unit = (for { + _ <- setupTableTest() + (msg, insertReply) <- sendOk(raw("INSERT INTO some_test (name, wedding_date) VALUES ('tester', '2015-04-04');")) + (msg, reply) <- sendOk(prepared("SELECT wedding_date FROM some_test WHERE name=?", Json.arr("tester"))) + } yield { + val receivedFields = reply.getArray("fields") + assertEquals(Json.arr("wedding_date"), receivedFields) + assertEquals("2015-04-04", reply.getArray("results").get[JsonArray](0).get[String](0)) + testComplete() + }) recover failedTest + + @Test + def timestampTest(): Unit = (for { + (m, r) <- sendOk(raw("DROP TABLE IF EXISTS date_test")) + (msg, r2) <- sendOk(raw(createDateTable("timestamp"))) + (msg, insertReply) <- sendOk(raw("INSERT INTO date_test (test_date) VALUES ('2015-04-04T10:04:00.000');")) + (msg, reply) <- sendOk(raw("SELECT test_date FROM date_test")) + } yield { + val receivedFields = reply.getArray("fields") + assertEquals(Json.arr("test_date"), receivedFields) + logger.info("date is: " + reply.getArray("results").get[JsonArray](0).get[String](0)) + assertEquals("2015-04-04T10:04:00.000", reply.getArray("results").get[JsonArray](0).get[String](0)) + testComplete() + }) recover failedTest + +} + diff --git a/src/test/scala/io/vertx/asyncsql/test/SqlTestVerticle.scala b/src/test/scala/io/vertx/asyncsql/test/SqlTestVerticle.scala index 51f70b3..da6428c 100644 --- a/src/test/scala/io/vertx/asyncsql/test/SqlTestVerticle.scala +++ b/src/test/scala/io/vertx/asyncsql/test/SqlTestVerticle.scala @@ -15,7 +15,7 @@ abstract class SqlTestVerticle extends TestVerticle with BaseVertxIntegrationTes val p = Promise[Unit] container.deployModule(System.getProperty("vertx.modulename"), getConfig(), 1, { deploymentID: AsyncResult[String] => if (deploymentID.failed()) { - logger.info(deploymentID.cause()) + logger.info(s"Deployment failed, cause: ${deploymentID.cause()}") p.failure(deploymentID.cause()) } assertTrue("deploymentID should not be null", deploymentID.succeeded()) @@ -67,6 +67,13 @@ abstract class SqlTestVerticle extends TestVerticle with BaseVertxIntegrationTes reply } + protected def createDateTable(dateDataType :String) = s""" + | CREATE TABLE date_test ( + | id SERIAL, + | test_date $dateDataType + | ); + """.stripMargin + protected def createTableStatement(tableName: String) = """ DROP TABLE IF EXISTS """ + tableName + """; CREATE TABLE """ + tableName + """ ( diff --git a/src/test/scala/io/vertx/asyncsql/test/mysql/MySqlTest.scala b/src/test/scala/io/vertx/asyncsql/test/mysql/MySqlTest.scala index 0530126..4dc621b 100644 --- a/src/test/scala/io/vertx/asyncsql/test/mysql/MySqlTest.scala +++ b/src/test/scala/io/vertx/asyncsql/test/mysql/MySqlTest.scala @@ -1,7 +1,9 @@ package io.vertx.asyncsql.test.mysql -import org.vertx.scala.core.json.Json import io.vertx.asyncsql.test.{BaseSqlTests, SqlTestVerticle} +import org.junit.Test +import org.vertx.scala.core.json._ +import org.vertx.testtools.VertxAssert._ class MySqlTest extends SqlTestVerticle with BaseSqlTests { @@ -14,6 +16,14 @@ class MySqlTest extends SqlTestVerticle with BaseSqlTests { override def getConfig = config + override def createDateTable(dateDataType: String) = s""" + | CREATE TABLE date_test ( + | id INT NOT NULL AUTO_INCREMENT, + | test_date $dateDataType, + | PRIMARY KEY(id) + | ); + """.stripMargin + override def createTableStatement(tableName: String) = """ CREATE TABLE """ + tableName + """ ( id INT NOT NULL AUTO_INCREMENT, @@ -24,7 +34,21 @@ CREATE TABLE """ + tableName + """ ( money FLOAT, wedding_date DATE, PRIMARY KEY (id) -); - """ +);""" + + @Test + def datetimeTest(): Unit = + (for { + (m, r) <- sendOk(raw("DROP TABLE IF EXISTS date_test")) + (msg, r2) <- sendOk(raw(createDateTable("datetime"))) + (msg, insertReply) <- sendOk(raw("INSERT INTO date_test (test_date) VALUES ('2015-04-04');")) + (msg, reply) <- sendOk(raw("SELECT test_date FROM date_test")) + } yield { + val receivedFields = reply.getArray("fields") + assertEquals(Json.arr("test_date"), receivedFields) + logger.info("date is: " + reply.getArray("results").get[JsonArray](0).get[String](0)); + assertEquals("2015-04-04T00:00:00.000", reply.getArray("results").get[JsonArray](0).get[String](0)) + testComplete() + }) recover failedTest } \ No newline at end of file From 717bfedb1d358d6d98abd82bd5a128044f0468e0 Mon Sep 17 00:00:00 2001 From: Max Stemplinger Date: Thu, 18 Sep 2014 12:45:09 +0200 Subject: [PATCH 10/14] added max as developer Signed-off-by: Max Stemplinger --- src/main/resources/mod.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/mod.json b/src/main/resources/mod.json index 244b82f..0fb4ad7 100644 --- a/src/main/resources/mod.json +++ b/src/main/resources/mod.json @@ -9,7 +9,7 @@ "author": "Joern Bernhardt", // Optional fields - //"developers": ["Other Dev 1", "Other Dev 2"], + "developers": ["Max Stemplinger"], "keywords": ["db", "database", "mysql", "postgresql"], "homepage": "https://github.com/vert-x/mod-mysql-postgresql" } \ No newline at end of file From 9dde61d62ed5fc5f8507c54d954f4e86e1775d7d Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Thu, 18 Sep 2014 15:03:20 +0200 Subject: [PATCH 11/14] small fixes to have same url/name as gradle build Signed-off-by: Joern Bernhardt --- project/VertxScalaBuild.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/VertxScalaBuild.scala b/project/VertxScalaBuild.scala index 382982c..5f22fbd 100644 --- a/project/VertxScalaBuild.scala +++ b/project/VertxScalaBuild.scala @@ -7,7 +7,7 @@ object VertxScalaBuild extends Build { val baseSettings = Defaults.defaultSettings ++ Seq( organization := "io.vertx", - name := "mysql-postgresql", + name := "mod-mysql-postgresql", version := "0.3.0-SNAPSHOT", scalaVersion := "2.10.4", crossScalaVersions := Seq("2.10.4", "2.11.2"), @@ -64,7 +64,7 @@ object VertxScalaBuild extends Build { scm:git:git://github.com/vert-x/mod-mysql-postgresql.git scm:git:ssh://git@github.com/vert-x/mod-mysql-postgresql.git - hhttps://github.com/vert-x/mod-mysql-postgresql + https://github.com/vert-x/mod-mysql-postgresql From 0096720edb05c1965aec1e5324aeff2e0139c5db Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Fri, 19 Sep 2014 15:16:07 +0200 Subject: [PATCH 12/14] fix artifactId and exclude some transitive dependencies Signed-off-by: Joern Bernhardt --- project/VertxScalaBuild.scala | 59 ++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/project/VertxScalaBuild.scala b/project/VertxScalaBuild.scala index 5f22fbd..2a6f11a 100644 --- a/project/VertxScalaBuild.scala +++ b/project/VertxScalaBuild.scala @@ -3,25 +3,31 @@ import java.nio.charset.StandardCharsets import sbt.Keys._ import sbt._ -object VertxScalaBuild extends Build { +object Variables { + val org = "io.vertx" + val name = "mod-mysql-postgresql" + val version = "0.3.0-SNAPSHOT" + val scalaVersion = "2.10.4" + val crossScalaVersions = Seq("2.10.4", "2.11.2") + val description = "Fully async MySQL / PostgreSQL module for Vert.x" +} - val baseSettings = Defaults.defaultSettings ++ Seq( - organization := "io.vertx", - name := "mod-mysql-postgresql", - version := "0.3.0-SNAPSHOT", - scalaVersion := "2.10.4", - crossScalaVersions := Seq("2.10.4", "2.11.2"), - description := "Fully async MySQL / PostgreSQL module for Vert.x" - ) +object VertxScalaBuild extends Build { lazy val project = Project( "project", file("."), - settings = baseSettings ++ Seq( + settings = Seq( + organization := Variables.org, + name := Variables.name, + version := Variables.version, + scalaVersion := Variables.scalaVersion, + crossScalaVersions := Variables.crossScalaVersions, + description := Variables.description, copyModTask, zipModTask, - libraryDependencies ++= Dependencies.compile, - libraryDependencies <+= scalaVersion("org.scala-lang" % "scala-compiler" % _), + libraryDependencies := Dependencies.compile, + // libraryDependencies <+= scalaVersion("org.scala-lang" % "scala-compiler" % _), // Fork JVM to allow Scala in-flight compilation tests to load the Scala interpreter fork in Test := true, // Vert.x tests are not designed to run in paralell @@ -35,11 +41,12 @@ object VertxScalaBuild extends Build { resourceGenerators in Compile += Def.task { val file = (resourceManaged in Compile).value / "langs.properties" val contents = s"scala=io.vertx~lang-scala_${getMajor(scalaVersion.value)}~${Dependencies.Versions.vertxLangScalaVersion}:org.vertx.scala.platform.impl.ScalaVerticleFactory\n.scala=scala\n" - IO.write(file, contents) + IO.write(file, contents, StandardCharsets.UTF_8) Seq(file) }.taskValue, - copyMod <<= copyMod dependsOn (compile in Compile), + copyMod <<= copyMod dependsOn (copyResources in Compile), (test in Test) <<= (test in Test) dependsOn copyMod, + zipMod <<= zipMod dependsOn copyMod, (packageBin in Compile) <<= (packageBin in Compile) dependsOn copyMod, // Publishing settings publishMavenStyle := true, @@ -78,7 +85,7 @@ object VertxScalaBuild extends Build { ) - ).settings(addArtifact(Artifact("lang-scala", "zip", "zip", "mod"), zipMod).settings: _*) + ).settings(addArtifact(Artifact(Variables.name, "zip", "zip", "mod"), zipMod).settings: _*) val copyMod = TaskKey[Unit]("copy-mod", "Assemble the module into the local mods directory") val zipMod = TaskKey[File]("zip-mod", "Package the module .zip file") @@ -88,7 +95,7 @@ object VertxScalaBuild extends Build { val modOwner = organization.value val modName = name.value val modVersion = version.value - val scalaMajor = scalaVersion.value.substring(0, scalaVersion.value.lastIndexOf('.')) + val scalaMajor = getMajor(scalaVersion.value) val moduleName = s"$modOwner~${modName}_$scalaMajor~$modVersion" log.info("Create module " + moduleName) val moduleDir = target.value / "mods" / moduleName @@ -108,7 +115,7 @@ object VertxScalaBuild extends Build { val modOwner = organization.value val modName = name.value val modVersion = version.value - val scalaMajor = scalaVersion.value.substring(0, scalaVersion.value.lastIndexOf('.')) + val scalaMajor = getMajor(scalaVersion.value) val moduleName = s"$modOwner~${modName}_$scalaMajor~$modVersion" log.info("Create ZIP module " + moduleName) val moduleDir = target.value / "mods" / moduleName @@ -155,24 +162,32 @@ object Dependencies { val vertxCore = "io.vertx" % "vertx-core" % vertxVersion % "provided" val vertxPlatform = "io.vertx" % "vertx-platform" % vertxVersion % "provided" - val vertxTesttools = "io.vertx" % "testtools" % testtoolsVersion % "provided" val vertxLangScala = "io.vertx" %% "lang-scala" % vertxLangScalaVersion % "provided" - val postgreSqlDriver = "com.github.mauricio" %% "postgresql-async" % asyncDriverVersion % "provided" - val mySqlDriver = "com.github.mauricio" %% "mysql-async" % asyncDriverVersion % "provided" + val postgreSqlDriver = ("com.github.mauricio" %% "postgresql-async" % asyncDriverVersion % "compile").excludeAll( + ExclusionRule(organization = "org.scala-lang"), + ExclusionRule(organization = "io.netty"), + ExclusionRule(organization = "org.slf4j") + ) + val mySqlDriver = ("com.github.mauricio" %% "mysql-async" % asyncDriverVersion % "compile"). excludeAll( + ExclusionRule(organization = "org.scala-lang"), + ExclusionRule(organization = "io.netty"), + ExclusionRule(organization = "org.slf4j") + ) } object Test { import Dependencies.Versions._ + val vertxTesttools = "io.vertx" % "testtools" % testtoolsVersion % "test" val hamcrest = "org.hamcrest" % "hamcrest-library" % hamcrestVersion % "test" val junitInterface = "com.novocode" % "junit-interface" % junitInterfaceVersion % "test" } import Dependencies.Compile._ - val test = List(Test.hamcrest, Test.junitInterface) + val test = List(Test.vertxTesttools, Test.hamcrest, Test.junitInterface) - val compile = List(vertxCore, vertxPlatform, vertxTesttools, vertxLangScala, postgreSqlDriver, mySqlDriver) ::: test + val compile = List(vertxCore, vertxPlatform, vertxLangScala, postgreSqlDriver, mySqlDriver) ::: test } From 2edfe197a7199f20f649e548abb9d4d2c37bd9ad Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Sat, 20 Sep 2014 00:34:09 +0200 Subject: [PATCH 13/14] refactored build script for easier c&p between projects Signed-off-by: Joern Bernhardt --- project/VertxScalaBuild.scala | 140 ++++++++++++++++------------------ 1 file changed, 66 insertions(+), 74 deletions(-) diff --git a/project/VertxScalaBuild.scala b/project/VertxScalaBuild.scala index 2a6f11a..167db95 100644 --- a/project/VertxScalaBuild.scala +++ b/project/VertxScalaBuild.scala @@ -10,6 +10,70 @@ object Variables { val scalaVersion = "2.10.4" val crossScalaVersions = Seq("2.10.4", "2.11.2") val description = "Fully async MySQL / PostgreSQL module for Vert.x" + + val vertxVersion = "2.1.2" + val testtoolsVersion = "2.0.3-final" + val hamcrestVersion = "1.3" + val junitInterfaceVersion = "0.10" + val vertxLangScalaVersion = "1.1.0-M1" + val asyncDriverVersion = "0.2.15" + + val pomExtra = + 2013 + http://vertx.io + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + scm:git:git://github.com/vert-x/mod-mysql-postgresql.git + scm:git:ssh://git@github.com/vert-x/mod-mysql-postgresql.git + https://github.com/vert-x/mod-mysql-postgresql + + + + Narigo + Joern Bernhardt + jb@campudus.com + + + Zwergal + Max Stemplinger + ms@campudus.com + + + +} + +object Dependencies { + + import Variables._ + + val test = List( + "io.vertx" % "testtools" % testtoolsVersion % "test", + "org.hamcrest" % "hamcrest-library" % hamcrestVersion % "test", + "com.novocode" % "junit-interface" % junitInterfaceVersion % "test" + ) + + val compile = List( + "io.vertx" % "vertx-core" % vertxVersion % "provided", + "io.vertx" % "vertx-platform" % vertxVersion % "provided", + "io.vertx" %% "lang-scala" % vertxLangScalaVersion % "provided", + "com.github.mauricio" %% "postgresql-async" % asyncDriverVersion % "compile" excludeAll( + ExclusionRule(organization = "org.scala-lang"), + ExclusionRule(organization = "io.netty"), + ExclusionRule(organization = "org.slf4j") + ), + "com.github.mauricio" %% "mysql-async" % asyncDriverVersion % "compile" excludeAll( + ExclusionRule(organization = "org.scala-lang"), + ExclusionRule(organization = "io.netty"), + ExclusionRule(organization = "org.slf4j") + ) + ) ::: test + } object VertxScalaBuild extends Build { @@ -40,7 +104,7 @@ object VertxScalaBuild extends Build { javaOptions in Test += s"-Dvertx.modulename=${organization.value}~${name.value}_${getMajor(scalaVersion.value)}~${version.value}", resourceGenerators in Compile += Def.task { val file = (resourceManaged in Compile).value / "langs.properties" - val contents = s"scala=io.vertx~lang-scala_${getMajor(scalaVersion.value)}~${Dependencies.Versions.vertxLangScalaVersion}:org.vertx.scala.platform.impl.ScalaVerticleFactory\n.scala=scala\n" + val contents = s"scala=io.vertx~lang-scala_${getMajor(scalaVersion.value)}~${Variables.vertxLangScalaVersion}:org.vertx.scala.platform.impl.ScalaVerticleFactory\n.scala=scala\n" IO.write(file, contents, StandardCharsets.UTF_8) Seq(file) }.taskValue, @@ -58,32 +122,7 @@ object VertxScalaBuild extends Build { else Some("Sonatype Releases" at sonatype + "service/local/staging/deploy/maven2") }, - pomExtra := - 2013 - http://vertx.io - - - Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.html - repo - - - - scm:git:git://github.com/vert-x/mod-mysql-postgresql.git - scm:git:ssh://git@github.com/vert-x/mod-mysql-postgresql.git - https://github.com/vert-x/mod-mysql-postgresql - - - - Narigo - Joern Bernhardt - jb@campudus.com - - - Zwergal - Max Stemplinger - - + pomExtra := Variables.pomExtra ) ).settings(addArtifact(Artifact(Variables.name, "zip", "zip", "mod"), zipMod).settings: _*) @@ -144,50 +183,3 @@ object VertxScalaBuild extends Build { } } - -object Dependencies { - - object Versions { - val vertxVersion = "2.1.2" - val testtoolsVersion = "2.0.3-final" - val hamcrestVersion = "1.3" - val junitInterfaceVersion = "0.10" - val vertxLangScalaVersion = "1.1.0-M1" - val asyncDriverVersion = "0.2.15" - } - - object Compile { - - import Dependencies.Versions._ - - val vertxCore = "io.vertx" % "vertx-core" % vertxVersion % "provided" - val vertxPlatform = "io.vertx" % "vertx-platform" % vertxVersion % "provided" - val vertxLangScala = "io.vertx" %% "lang-scala" % vertxLangScalaVersion % "provided" - val postgreSqlDriver = ("com.github.mauricio" %% "postgresql-async" % asyncDriverVersion % "compile").excludeAll( - ExclusionRule(organization = "org.scala-lang"), - ExclusionRule(organization = "io.netty"), - ExclusionRule(organization = "org.slf4j") - ) - val mySqlDriver = ("com.github.mauricio" %% "mysql-async" % asyncDriverVersion % "compile"). excludeAll( - ExclusionRule(organization = "org.scala-lang"), - ExclusionRule(organization = "io.netty"), - ExclusionRule(organization = "org.slf4j") - ) - } - - object Test { - - import Dependencies.Versions._ - - val vertxTesttools = "io.vertx" % "testtools" % testtoolsVersion % "test" - val hamcrest = "org.hamcrest" % "hamcrest-library" % hamcrestVersion % "test" - val junitInterface = "com.novocode" % "junit-interface" % junitInterfaceVersion % "test" - } - - import Dependencies.Compile._ - - val test = List(Test.vertxTesttools, Test.hamcrest, Test.junitInterface) - - val compile = List(vertxCore, vertxPlatform, vertxLangScala, postgreSqlDriver, mySqlDriver) ::: test - -} From 8d93b3750cb4075ea0fcce1f319a309163ab36d5 Mon Sep 17 00:00:00 2001 From: Joern Bernhardt Date: Sat, 20 Sep 2014 01:46:44 +0200 Subject: [PATCH 14/14] added sbt shell script from mod-lang-scala Signed-off-by: Joern Bernhardt --- sbt | 525 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 525 insertions(+) create mode 100755 sbt diff --git a/sbt b/sbt new file mode 100755 index 0000000..08e5882 --- /dev/null +++ b/sbt @@ -0,0 +1,525 @@ +#!/usr/bin/env bash +# +# A more capable sbt runner, coincidentally also called sbt. +# Author: Paul Phillips + +# todo - make this dynamic +declare -r sbt_release_version="0.13.6" +declare -r sbt_unreleased_version="0.13.6" +declare -r buildProps="project/build.properties" + +declare sbt_jar sbt_dir sbt_create sbt_version +declare scala_version sbt_explicit_version +declare verbose noshare batch trace_level log_level +declare sbt_saved_stty debugUs + +echoerr () { echo >&2 "$@"; } +vlog () { [[ -n "$verbose" ]] && echoerr "$@"; } + +# spaces are possible, e.g. sbt.version = 0.13.0 +build_props_sbt () { + [[ -r "$buildProps" ]] && \ + grep '^sbt\.version' "$buildProps" | tr '=' ' ' | awk '{ print $2; }' +} + +update_build_props_sbt () { + local ver="$1" + local old="$(build_props_sbt)" + + [[ -r "$buildProps" ]] && [[ "$ver" != "$old" ]] && { + perl -pi -e "s/^sbt\.version\b.*\$/sbt.version=${ver}/" "$buildProps" + grep -q '^sbt.version[ =]' "$buildProps" || printf "\nsbt.version=%s\n" "$ver" >> "$buildProps" + + vlog "!!!" + vlog "!!! Updated file $buildProps setting sbt.version to: $ver" + vlog "!!! Previous value was: $old" + vlog "!!!" + } +} + +set_sbt_version () { + sbt_version="${sbt_explicit_version:-$(build_props_sbt)}" + [[ -n "$sbt_version" ]] || sbt_version=$sbt_release_version + export sbt_version +} + +# restore stty settings (echo in particular) +onSbtRunnerExit() { + [[ -n "$sbt_saved_stty" ]] || return + vlog "" + vlog "restoring stty: $sbt_saved_stty" + stty "$sbt_saved_stty" + unset sbt_saved_stty +} + +# save stty and trap exit, to ensure echo is reenabled if we are interrupted. +trap onSbtRunnerExit EXIT +sbt_saved_stty="$(stty -g 2>/dev/null)" +vlog "Saved stty: $sbt_saved_stty" + +# this seems to cover the bases on OSX, and someone will +# have to tell me about the others. +get_script_path () { + local path="$1" + [[ -L "$path" ]] || { echo "$path" ; return; } + + local target="$(readlink "$path")" + if [[ "${target:0:1}" == "/" ]]; then + echo "$target" + else + echo "${path%/*}/$target" + fi +} + +die() { + echo "Aborting: $@" + exit 1 +} + +make_url () { + version="$1" + + case "$version" in + 0.7.*) echo "http://simple-build-tool.googlecode.com/files/sbt-launch-0.7.7.jar" ;; + 0.10.* ) echo "$sbt_launch_repo/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; + 0.11.[12]) echo "$sbt_launch_repo/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; + *) echo "$sbt_launch_repo/org.scala-sbt/sbt-launch/$version/sbt-launch.jar" ;; + esac +} + +init_default_option_file () { + local overriding_var="${!1}" + local default_file="$2" + if [[ ! -r "$default_file" && "$overriding_var" =~ ^@(.*)$ ]]; then + local envvar_file="${BASH_REMATCH[1]}" + if [[ -r "$envvar_file" ]]; then + default_file="$envvar_file" + fi + fi + echo "$default_file" +} + +declare -r cms_opts="-XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC" +declare -r jit_opts="-XX:ReservedCodeCacheSize=256m -XX:+TieredCompilation" +declare -r default_jvm_opts_common="-Xms512m -Xmx1536m -Xss2m $jit_opts $cms_opts" +declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" +declare -r latest_28="2.8.2" +declare -r latest_29="2.9.3" +declare -r latest_210="2.10.4" +declare -r latest_211="2.11.2" + +declare -r script_path="$(get_script_path "$BASH_SOURCE")" +declare -r script_name="${script_path##*/}" + +# some non-read-onlies set with defaults +declare java_cmd="java" +declare sbt_opts_file="$(init_default_option_file SBT_OPTS .sbtopts)" +declare jvm_opts_file="$(init_default_option_file JVM_OPTS .jvmopts)" +declare sbt_launch_repo="http://typesafe.artifactoryonline.com/typesafe/ivy-releases" + +# pull -J and -D options to give to java. +declare -a residual_args +declare -a java_args +declare -a scalac_args +declare -a sbt_commands + +# args to jvm/sbt via files or environment variables +declare -a extra_jvm_opts extra_sbt_opts + +# if set, use JAVA_HOME over java found in path +[[ -e "$JAVA_HOME/bin/java" ]] && java_cmd="$JAVA_HOME/bin/java" + +# directory to store sbt launchers +declare sbt_launch_dir="$HOME/.sbt/launchers" +[[ -d "$sbt_launch_dir" ]] || mkdir -p "$sbt_launch_dir" +[[ -w "$sbt_launch_dir" ]] || sbt_launch_dir="$(mktemp -d -t sbt_extras_launchers.XXXXXX)" + +java_version () { + local version=$("$java_cmd" -version 2>&1 | grep -e 'java version' | awk '{ print $3 }' | tr -d \") + vlog "Detected Java version: $version" + echo "${version:2:1}" +} + +# MaxPermSize critical on pre-8 jvms but incurs noisy warning on 8+ +default_jvm_opts () { + local v="$(java_version)" + if [[ $v -ge 8 ]]; then + echo "$default_jvm_opts_common" + else + echo "-XX:MaxPermSize=384m $default_jvm_opts_common" + fi +} + +build_props_scala () { + if [[ -r "$buildProps" ]]; then + versionLine="$(grep '^build.scala.versions' "$buildProps")" + versionString="${versionLine##build.scala.versions=}" + echo "${versionString%% .*}" + fi +} + +execRunner () { + # print the arguments one to a line, quoting any containing spaces + vlog "# Executing command line:" && { + for arg; do + if [[ -n "$arg" ]]; then + if printf "%s\n" "$arg" | grep -q ' '; then + printf >&2 "\"%s\"\n" "$arg" + else + printf >&2 "%s\n" "$arg" + fi + fi + done + vlog "" + } + + [[ -n "$batch" ]] && exec /dev/null; then + curl --fail --silent "$url" --output "$jar" + elif which wget >/dev/null; then + wget --quiet -O "$jar" "$url" + fi + } && [[ -r "$jar" ]] +} + +acquire_sbt_jar () { + sbt_url="$(jar_url "$sbt_version")" + sbt_jar="$(jar_file "$sbt_version")" + + [[ -r "$sbt_jar" ]] || download_url "$sbt_url" "$sbt_jar" +} + +usage () { + cat < display stack traces with a max of frames (default: -1, traces suppressed) + -debug-inc enable debugging log for the incremental compiler + -no-colors disable ANSI color codes + -sbt-create start sbt even if current directory contains no sbt project + -sbt-dir path to global settings/plugins directory (default: ~/.sbt/) + -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11+) + -ivy path to local Ivy repository (default: ~/.ivy2) + -no-share use all local caches; no sharing + -offline put sbt in offline mode + -jvm-debug Turn on JVM debugging, open at the given port. + -batch Disable interactive mode + -prompt Set the sbt prompt; in expr, 's' is the State and 'e' is Extracted + + # sbt version (default: sbt.version from $buildProps if present, otherwise $sbt_release_version) + -sbt-force-latest force the use of the latest release of sbt: $sbt_release_version + -sbt-version use the specified version of sbt (default: $sbt_release_version) + -sbt-dev use the latest pre-release version of sbt: $sbt_unreleased_version + -sbt-jar use the specified jar as the sbt launcher + -sbt-launch-dir directory to hold sbt launchers (default: ~/.sbt/launchers) + -sbt-launch-repo repo url for downloading sbt launcher jar (default: $sbt_launch_repo) + + # scala version (default: as chosen by sbt) + -28 use $latest_28 + -29 use $latest_29 + -210 use $latest_210 + -211 use $latest_211 + -scala-home use the scala build at the specified directory + -scala-version use the specified version of scala + -binary-version use the specified scala version when searching for dependencies + + # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) + -java-home alternate JAVA_HOME + + # passing options to the jvm - note it does NOT use JAVA_OPTS due to pollution + # The default set is used if JVM_OPTS is unset and no -jvm-opts file is found + $(default_jvm_opts) + JVM_OPTS environment variable holding either the jvm args directly, or + the reference to a file containing jvm args if given path is prepended by '@' (e.g. '@/etc/jvmopts') + Note: "@"-file is overridden by local '.jvmopts' or '-jvm-opts' argument. + -jvm-opts file containing jvm args (if not given, .jvmopts in project root is used if present) + -Dkey=val pass -Dkey=val directly to the jvm + -J-X pass option -X directly to the jvm (-J is stripped) + + # passing options to sbt, OR to this runner + SBT_OPTS environment variable holding either the sbt args directly, or + the reference to a file containing sbt args if given path is prepended by '@' (e.g. '@/etc/sbtopts') + Note: "@"-file is overridden by local '.sbtopts' or '-sbt-opts' argument. + -sbt-opts file containing sbt args (if not given, .sbtopts in project root is used if present) + -S-X add -X to sbt's scalacOptions (-S is stripped) +EOM +} + +addJava () { + vlog "[addJava] arg = '$1'" + java_args=( "${java_args[@]}" "$1" ) +} +addSbt () { + vlog "[addSbt] arg = '$1'" + sbt_commands=( "${sbt_commands[@]}" "$1" ) +} +setThisBuild () { + vlog "[addBuild] args = '$@'" + local key="$1" && shift + addSbt "set $key in ThisBuild := $@" +} + +addScalac () { + vlog "[addScalac] arg = '$1'" + scalac_args=( "${scalac_args[@]}" "$1" ) +} +addResidual () { + vlog "[residual] arg = '$1'" + residual_args=( "${residual_args[@]}" "$1" ) +} +addResolver () { + addSbt "set resolvers += $1" +} +addDebugger () { + addJava "-Xdebug" + addJava "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1" +} +setScalaVersion () { + [[ "$1" == *"-SNAPSHOT" ]] && addResolver 'Resolver.sonatypeRepo("snapshots")' + addSbt "++ $1" +} + +process_args () +{ + require_arg () { + local type="$1" + local opt="$2" + local arg="$3" + + if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then + die "$opt requires <$type> argument" + fi + } + while [[ $# -gt 0 ]]; do + case "$1" in + -h|-help) usage; exit 1 ;; + -v) verbose=true && shift ;; + -d) addSbt "--debug" && shift ;; + -w) addSbt "--warn" && shift ;; + -q) addSbt "--error" && shift ;; + -x) debugUs=true && shift ;; + -trace) require_arg integer "$1" "$2" && trace_level="$2" && shift 2 ;; + -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; + -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;; + -no-share) noshare=true && shift ;; + -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;; + -sbt-dir) require_arg path "$1" "$2" && sbt_dir="$2" && shift 2 ;; + -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;; + -offline) addSbt "set offline := true" && shift ;; + -jvm-debug) require_arg port "$1" "$2" && addDebugger "$2" && shift 2 ;; + -batch) batch=true && shift ;; + -prompt) require_arg "expr" "$1" "$2" && setThisBuild shellPrompt "(s => { val e = Project.extract(s) ; $2 })" && shift 2 ;; + + -sbt-create) sbt_create=true && shift ;; + -sbt-jar) require_arg path "$1" "$2" && sbt_jar="$2" && shift 2 ;; + -sbt-version) require_arg version "$1" "$2" && sbt_explicit_version="$2" && shift 2 ;; + -sbt-force-latest) sbt_explicit_version="$sbt_release_version" && shift ;; + -sbt-dev) sbt_explicit_version="$sbt_unreleased_version" && shift ;; + -sbt-launch-dir) require_arg path "$1" "$2" && sbt_launch_dir="$2" && shift 2 ;; + -sbt-launch-repo) require_arg path "$1" "$2" && sbt_launch_repo="$2" && shift 2 ;; + -scala-version) require_arg version "$1" "$2" && setScalaVersion "$2" && shift 2 ;; + -binary-version) require_arg version "$1" "$2" && setThisBuild scalaBinaryVersion "\"$2\"" && shift 2 ;; + -scala-home) require_arg path "$1" "$2" && setThisBuild scalaHome "Some(file(\"$2\"))" && shift 2 ;; + -java-home) require_arg path "$1" "$2" && java_cmd="$2/bin/java" && shift 2 ;; + -sbt-opts) require_arg path "$1" "$2" && sbt_opts_file="$2" && shift 2 ;; + -jvm-opts) require_arg path "$1" "$2" && jvm_opts_file="$2" && shift 2 ;; + + -D*) addJava "$1" && shift ;; + -J*) addJava "${1:2}" && shift ;; + -S*) addScalac "${1:2}" && shift ;; + -28) setScalaVersion "$latest_28" && shift ;; + -29) setScalaVersion "$latest_29" && shift ;; + -210) setScalaVersion "$latest_210" && shift ;; + -211) setScalaVersion "$latest_211" && shift ;; + + *) addResidual "$1" && shift ;; + esac + done +} + +# process the direct command line arguments +process_args "$@" + +# skip #-styled comments and blank lines +readConfigFile() { + while read line; do + [[ $line =~ ^# ]] || [[ -z $line ]] || echo "$line" + done < "$1" +} + +# if there are file/environment sbt_opts, process again so we +# can supply args to this runner +if [[ -r "$sbt_opts_file" ]]; then + vlog "Using sbt options defined in file $sbt_opts_file" + while read opt; do extra_sbt_opts+=("$opt"); done < <(readConfigFile "$sbt_opts_file") +elif [[ -n "$SBT_OPTS" && ! ("$SBT_OPTS" =~ ^@.*) ]]; then + vlog "Using sbt options defined in variable \$SBT_OPTS" + extra_sbt_opts=( $SBT_OPTS ) +else + vlog "No extra sbt options have been defined" +fi + +[[ -n "${extra_sbt_opts[*]}" ]] && process_args "${extra_sbt_opts[@]}" + +# reset "$@" to the residual args +set -- "${residual_args[@]}" +argumentCount=$# + +# set sbt version +set_sbt_version + +# only exists in 0.12+ +setTraceLevel() { + case "$sbt_version" in + "0.7."* | "0.10."* | "0.11."* ) echoerr "Cannot set trace level in sbt version $sbt_version" ;; + *) setThisBuild traceLevel $trace_level ;; + esac +} + +# set scalacOptions if we were given any -S opts +[[ ${#scalac_args[@]} -eq 0 ]] || addSbt "set scalacOptions in ThisBuild += \"${scalac_args[@]}\"" + +# Update build.properties on disk to set explicit version - sbt gives us no choice +[[ -n "$sbt_explicit_version" ]] && update_build_props_sbt "$sbt_explicit_version" +vlog "Detected sbt version $sbt_version" + +[[ -n "$scala_version" ]] && vlog "Overriding scala version to $scala_version" + +# no args - alert them there's stuff in here +(( argumentCount > 0 )) || { + vlog "Starting $script_name: invoke with -help for other options" + residual_args=( shell ) +} + +# verify this is an sbt dir or -create was given +[[ -r ./build.sbt || -d ./project || -n "$sbt_create" ]] || { + cat <